目录
模拟抢火车票的过程
代码示例
thread.cc
Thread.hpp
运行结果
分析原因
tickets减到-2的本质
解决抢票出错的方案
临界资源的概念
原子性的概念
加锁
定义
初始化
销毁
代码形式如下
代码示例1:
代码示例2:
总结
如何看待锁
申请失败将会阻塞
pthread_mutex_tyrlock
互斥锁实现原理
锁是如何实现互斥的?
封装加锁组件(RAII风格)
Mutex.hpp
mythread.cc
RAII风格
可重入与线程的关系(浅谈)
可重入与线程安全联系
可重入与线程安全区别
死锁
概念
提出问题
死锁的四个必要条件
如何破坏死锁
概念:
线程同步
本质
先谈生产者消费者模型
"321"原则
生产者消费者模型的特点
条件变量
概念
定义
具体操作
所用函数
定义的返回值
代码示例:
唤醒方式
模拟抢火车票的过程
代码示例
thread.cc
#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include "Thread.hpp"
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// 共享资源, 火车票
int tickets = 10000;
// 就需要尽可能的让多个线程交叉执行
// 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测
// 如果可以,就发生切换。
// 1、多个执行流进行安全访问的共享资源 - 临界资源?
// 2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码很小的一部分
// 3、想让多个线程串行访问共享资源 -- 互斥
// 4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性
// 一个对资源进行操作,如果只用一条汇编语句就能完成 -- 原子性
// 反之:不是原子的 -- 当前理解,方便表述
// 解决方案:加锁
void *getTicket(void *args)
{
// 获取线程的名字
std::string username = static_cast<const char *>(args);
while (true)
{
// pthread_mutex_lock(&lock);
if (tickets > 0)
{
usleep(1000);
// 只有票数大于0才值得抢
std::cout << username << "正在抢票" << tickets-- << std::endl;
// 用这段时间来模拟真实的抢票要花费的时间
// pthread_mutex_unlock(&lock);
}
else
{
// pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
int main()
{
std::unique_ptr<Thread> thread1(new Thread(getTicket, (void *)"user1", 1));
std::unique_ptr<Thread> thread2(new Thread(getTicket, (void *)"user2", 2));
std::unique_ptr<Thread> thread3(new Thread(getTicket, (void *)"user3", 3));
std::unique_ptr<Thread> thread4(new Thread(getTicket, (void *)"user4", 4));
thread1->join();
thread2->join();
thread3->join();
thread4->join();
return 0;
}
Thread.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <functional>
#include <cassert>
#include <cstring>
#include <string>
class Thread;
// 上下文
class Context
{
public:
Thread *this_;
void *args_;
public:
Context():this_(nullptr),args_(nullptr)
{}
~Context()
{}
};
class Thread
{
public:
typedef std::function<void *(void *)> func_t;
const int num = 1024;
public:
Thread(func_t func, void *args, int number)
: func_(func), args_(args)
{
// name_="thread-";
// name_+=std::to_string(number);
char buffer[num];
snprintf(buffer, sizeof buffer, "thread-%d", number);
name_ = buffer;
// 异常 == if : 意料之外用异常或者if判断
// assert:意料之中用assert
Context *ctx=new Context();
ctx->this_=this;
ctx->args_=args_;
int n = pthread_create(&tid_, nullptr, start_routine,ctx); // TODO
assert(n == 0);
(void)n;
// 编译debug的方式发布的时候是存在的,release方式发布,
// assert就不存在l,n就是一个定义,
// 但是没有使用的变量,有些编译器下会有warning
}
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
// 类内成员,有缺省参数!在第一参数中包含了一个this指针
static void *start_routine(void *args)
{
Context *ctx=static_cast<Context*>(args);
void *ret=ctx->this_->run(ctx->args_);
delete ctx;
return ret;
// 静态方法不能调用成员方法或者成员变量
// return func_(args_);
}
void join()
{
int n = pthread_join(tid_, nullptr);
assert(n == 0);
(void)n;
}
void *run(void *args)
{
return func_(args);
}
~Thread()
{
// do northing
}
private:
std::string name_;
func_t func_;
void *args_;
pthread_t tid_;
};
运行结果
user2正在抢票16
user4正在抢票15
user1正在抢票14
user3正在抢票13
user2正在抢票12
user4正在抢票11
user1正在抢票10
user3正在抢票9
user2正在抢票8
user4正在抢票7
user1正在抢票6
user3正在抢票5
user2正在抢票4
user4正在抢票3
user1正在抢票2
user3正在抢票1
user2正在抢票0
user4正在抢票-1
user1正在抢票-2
分析原因
很明显,我们只有10000张票,可是运行结果中抢到了-2。比如这个那么这多出的3张票对应的人将没地方坐。那么为什么会出现这种问题呢,下面将来解答。
抢票出错的本质:
多个线程交叉执行!
多个线程交叉执行本质:
调度器频繁发生线程调度与切换。
线程一般在什么时候发生切换呢?
1、时间片到了
2、来了更高优先级的线程,线程等待的时候。
线程是什么时候检测上面的问题呢?
从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就发生切换。
判断票是否大于0的本质逻辑是:
if(tickets>0)
1、读取内存数据CPU内的寄存器中
2、进行判断
tickets--的本质逻辑是:
1、读取数据
2、更改数据
3、写回数据
对变量进行++,或者--,在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:
1、从内存中读取数据到CPU内的寄存器中
2、在寄存器中让CPU进行对应的算逻运算
3、写回新的结果到内存中变量的位置
tickets减到-2的本质
全局变量
int g_val=1000;寄存器只有一套,因此不同的线程在寄存器的存放的上下文为各个线程私有
线程A在做完 1 和 2 后 带着自己的上下文切走了
1000->999
线程B在做完1 和 2 后也带着自己的上下文切走了结果现在线程A切换回来了(切回来之后需要让自己的上下文给重新写入寄存器) 又把数据改为999了。
我们定义的全局变量,在没有保护的时候,往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生了数据不一致问题!
解决抢票出错的方案
我们可以使用加锁的方式来避免这种情况发生
临界资源的概念
1、多个执行流进行安全访问的共享资源 - 临界资源
2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码的很小的一部分
3、想让多个线程串行访问资源 -- 互斥
4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性
注意:并不是上述所有代码都属于临界区,而是访问临界资源的代码是临界区 。临界区的代码往往粒度(代码长度)很小。
原子性的概念
原子性:一个资源进行的操作,如果只用一条汇编就能完成 -- 就是原子的 反之不是。
加锁
定义
可以把这个锁定义为全局的/静态的 -- 不用初始化和销毁
直接定义为 :
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
代码形式如下
加锁
临界区
解锁
加锁和解锁之间的区域是临界区(也就是需要加锁的地方)。
代码示例1:
只展示了getTicket函数部分(包括加锁和解锁)
void *getTicket(void *args)
{
// 获取线程的名字
std::string username = static_cast<const char *>(args);
while (true)
{
// 加锁
pthread_mutex_lock(&lock);
// 判断的本质:
// 1、读取内存数据CPU内的寄存器中
// 2、进行判断
if (tickets > 0)
{
usleep(1000);
// 只有票数大于0才值得抢
std::cout << username << "正在抢票" << tickets << std::endl;
tickets--;
// --的本质
// 1、读取数据
// 2、更改数据
// 3、写回数据
// 用这段时间来模拟真实的抢票要花费的时间
// 解锁
pthread_mutex_unlock(&lock);
}
else
{
// 解锁
pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
代码示例2:
使用结构体封装了线程名和锁,并且使用了原生的线程创建方式
#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include "Thread.hpp"
// 共享资源, 火车票
int tickets = 10000;
// 就需要尽可能的让多个线程交叉执行
// 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测
// 如果可以,就发生切换。
// 1、多个执行流进行安全访问的共享资源 - 临界资源?
// 2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码很小的一部分
// 3、想让多个线程串行访问共享资源 -- 互斥
// 4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性
// 一个对资源进行操作,如果只用一条汇编语句就能完成 -- 原子性
// 反之:不是原子的 -- 当前理解,方便表述
// 解决方案:加锁
// 使用结构体 对锁和名字进行了封装 使用的时候直接使用结构体对象调用即可
class ThreadData
{
public:
ThreadData(const std::string &threadname, pthread_mutex_t *mutex_p)
: threadname_(threadname), mutex_p_(mutex_p)
{}
~ThreadData() {}
public:
std::string threadname_;
pthread_mutex_t *mutex_p_;
};
void *getTicket(void *args)
{
// 获取线程的名字
// std::string username = static_cast<const char *>(args);
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
// 加锁
pthread_mutex_lock(td->mutex_p_);
// 判断的本质:
// 1、读取内存数据CPU内的寄存器中
// 2、进行判断
if (tickets > 0)
{
usleep(1000);
// 只有票数大于0才值得抢
std::cout << td->threadname_ << "正在抢票" << tickets << std::endl;
tickets--;
// --的本质
// 1、读取数据
// 2、更改数据
// 3、写回数据
// 用这段时间来模拟真实的抢票要花费的时间
// 解锁 pthread_mutex_unlock(td->mutex_p_);
}
else
{
pthread_mutex_unlock(td->mutex_p_);
// 解锁
break;
}
// 抢完票就完了吗? 当然不是
usleep(1000);// 模拟形成一个订单给用户
}
return nullptr;
}
int main()
{
#define NUM 4
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr);
std::vector<pthread_t> tids(NUM);
for (int i = 0; i < NUM; i++)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "thread %d", i + 1);
ThreadData *td = new ThreadData(buffer, &lock);
pthread_create(&tids[i], nullptr, getTicket, td);
}
for (const auto &tid : tids)
{
pthread_join(tid, nullptr);
}
pthread_mutex_destroy(&lock);
}
总结
加锁和解锁的过程:
多个线程由并行执行变为了串行执行的,程序变慢了!
锁只规定互斥访问,没有规定必须谁优先执行!
锁就是真正的让多个执行流进行竞争的结果。
执行结果一直是一个线程在抢票原因是:
该线程竞争能力强,其他线程则比较弱,因此每次都是该线程抢到了。
如何看待锁
- 锁,本身就是一个共享资源!全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源,锁的安全谁来保护呢?
- pthread_mutex_lock、pthread_mutex_unlock:加锁的过程必须是安全的!加锁的过程其实是原子的!要不申请成功,要不申请失败。
- 如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会阻塞
申请失败将会阻塞
pthread_mutex_tyrlock
当然我们也可以使用 pthread_mutex_tyrlock(pthread_mutex_t *mutex)
函数目的是:试着去加锁
如果我加锁成功了,我会持有锁
如果加锁失败了,会出错返回
如何理解加锁和解锁的本质?
加锁的过程是原子的。
若当前线程1 线程2 线程3 同时访问临界资源的时候,如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,其他线程在做什么??
阻塞,等待。
如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,我可不可以被切换呢??
可以
当持有锁的线程被切走的的时候其他线程可以申请锁吗?
当持有锁的线程被切走的时候,是抱着锁被切走的,即便自己被切走了,其他线程依旧无法申请锁成功,也便无法向后执行!直到我最终释放这个锁!
所以,对于其他线程而言,有意义的锁的状态,无非两种
1.申请锁前
2.申请锁后
站在其他线程角度,看待当前线程持有锁的过程就是原子的!!
使用锁注意事项:
1、未来我们在使用锁的时候,一定要尽量保证临界区的粒度(代码长度)非常小。
2、加锁是程序员行为,必须要做到要加都要加!
互斥锁实现原理
i++或者++i都不是原子的。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
锁是如何实现互斥的?
%al:寄存器
交换的本质:共享的数据,交换到我的上下文中!!!
lock:
movb $0, %al //将0放到该线程的寄存器中
xchgb %al, mutex // 寄存器和内存单元的数据相交换
if(al寄存器的内容 > 0)
{
return 0;
}
else
挂起等待;
goto lock;
unlock:
movb $1, mutex // 将1移动到内存里面
唤醒等待Mutex的线程;
return 0;
1、CPU内寄存器只有一套被所有执行流共享
2、CPU内寄存器的内容,是每个执行流私有的,运行时上下文
封装加锁组件(RAII风格)
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
// 写了一个锁的类,里面有加锁和解锁的成员函数
// 并且有一个锁的成员变量
class Mutex
{
public:
Mutex(pthread_mutex_t *lock_p = nullptr) : lock_p_(lock_p)
{
}
void lock()
{
if (lock_p_)
pthread_mutex_lock(lock_p_);
}
void unlock()
{
if (lock_p_)
pthread_mutex_unlock(lock_p_);
}
~Mutex()
{
}
private:
pthread_mutex_t *lock_p_;
};
// 这个类中定义了一个上述类中的对象
// 并且将上述类中的成员函数放进该类中的构造函数和析构函数中
// guard:警卫
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex):mutex_(mutex)
{
mutex_.lock(); // 在构造函数中进行加锁
}
~LockGuard()
{
mutex_.unlock();// 在析构函数中进行解锁
}
private:
Mutex mutex_;
};
mythread.cc
#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include"Mutex.hpp"
// #include "Thread.hpp"
// // 定义全局变量的
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
// 共享资源, 火车票
int tickets = 10000;
class ThreadData
{
public:
ThreadData(const std::string &threadname, pthread_mutex_t *mutex_p)
: threadname_(threadname), mutex_p_(mutex_p)
{}
~ThreadData() {}
public:
std::string threadname_;
pthread_mutex_t *mutex_p_;
};
void *getTicket(void *args)
{
// 获取线程的名字
std::string username = static_cast<const char *>(args);
while (true)
{
LockGuard lockguard(&lock);
// 将lock传过去之后调用析构函数自动加锁
// 然后出作用域调用析构函数自动解锁
if (tickets > 0)
{
usleep(1000);
std::cout << username << "正在抢票" << tickets << std::endl;
tickets--;
}
else
{
// pthread_mutex_unlock(&lock);
// 解锁
// pthread_mutex_unlock(&lock);
break;
}
// 抢完票就完了吗? 当然不是
usleep(1000);// 模拟形成一个订单给用户
}
return nullptr;
}
int main()
{
pthread_mutex_init(&lock, nullptr);
pthread_t t1, t2, t3, t4;
pthread_create(&t1,nullptr,getTicket,(void*)"thread 1");
pthread_create(&t2,nullptr,getTicket,(void*)"thread 2");
pthread_create(&t3,nullptr,getTicket,(void*)"thread 3");
pthread_create(&t4,nullptr,getTicket,(void*)"thread 4");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
pthread_mutex_destroy(&lock);
}
RAII风格
重点:
LockGuard lockguard(&lock);
// 将lock传过去之后调用析构函数自动加锁
// 然后出作用域调用析构函数自动解锁
可重入与线程的关系(浅谈)
可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生
死锁,因此是不可重入的。
死锁
概念
一组执行流(无论是进程还是线程),它在持有自己的锁资源的同时,还想方设法的去申请对方的锁资源,因为大家互相持有自己的,还互相申请对方的,因为锁是不可抢占式的锁(我拿了锁,除非我自己主动归还,否则别人是不能直接要我的锁的)。
所以,就是大家互相持有自己的资源,还在等待对方的锁资源而导致代码无法推进的情况,这种情况就叫死锁。
提出问题
1、一把锁,有可能死锁吗?
可能的,比如自己连续申请两次锁
pthread_mutex_lock(&lock);
pthread_mutex_lock(&lock);
2、为什么会有死锁? 逻辑链条
首先一定是用了锁才会发生死锁
3、为什么要用锁呢?
保证临界资源的安全
多线程访问我们可能出现数据不一致的问题
多线程&&全局资源
多线程大部分资源(全局的)是共享的
多线程的特征
引出的大概念
任何技术都有自己的边界,是解决问题的,但有可能在解决问题的同时,一定会可能引入新的问题!
死锁的四个必要条件
1.互斥 -- 我们必须得保证访问一份资源是互斥的(这是我们锁基本特性,没有互斥那就是没有加锁)
2.请求与保持 -- 我要你的资源这是请求,我还保持我自己的不释放这就是保持,我又要你的资源又不释放我自己的资源,就是请求与保持。
3.不剥夺 -- 不能去抢占对方的锁,只能等待对方自动的给你这叫做不剥夺。
4.环路等待条件 -- 比如线程A、线程B、线程C。线程A拥有自己的锁它去要线程B的锁,线程B有自己的锁它去要线程C的锁,线程C有自己的锁它去要线程A的锁。形成了一个环路情况。
注意:
只有这四个必要条件同时都满足的情况下才会造成死锁。
如何破坏死锁
概念:
破坏死锁的本质就是破坏这四个条件的至少一个!
做法:
加锁顺序一致
避免锁未释放的场景
资源一次性分配
线程同步
本质
线程运行同步的本质:
当我们在进行临界资源访问安全的前提条件下,让多个线程按照一定的顺序进行资源访问。
先谈生产者消费者模型
生产的过程和消费的过程 - 解耦
生产者跟消费者互不影响。
临时保存产品的场所 -- 缓冲区
不符合生产者消费者模型的例子
函数调用:
调用方:生产了数据
形成变量:变量暂时保存数据
目标函数:消费了数据
main函数调用fun函数的时候
正在调用的过程中main正在等待fun函数返回
main函数与fun函数是强耦合关系
"321"原则
3种关系: 生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥|[保证共享资源的安全性]/同步)。
-- 产品(数据)2种角色: 生产者线程,消费者线程
1个交易场所: 一段特定结构的缓冲区。交易的是数据。
注意:
只要我们想写生产消费模型,我们本质工作其实就是维护321原则!
生产者消费者模型的特点
1、生产线程和消费线程进行解耦
2、支持生产和消费的一段时间的忙闲不均的问题 -- 缓冲区的优势
3、提高效率
条件变量
概念
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
定义
是一种数据类型,我们可以使用这个类型来定义变量或者对象。
具体操作
本质是对线程的PCB做处理。
所用函数
操作系统调用pthread_cond_wait()的时候就是把它放进等待队列里面
操作系统调用pthread_cond_signal()就是它唤醒。
定义的返回值
定义条件变量之前要先初始化。成功返回0,失败返回对应的错误码。
谁排队谁唤醒?
让指定线程在条件变量里面进行排队
主线程进行唤醒。
代码示例:
#include <iostream>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *start_routine(void *args)
{
std::string name = static_cast<const char *>(args);
while (true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex); // 为什么要有mutex 后面会说
// 判断暂时省略
std::cout << name << " -> " << tickets << std::endl;
tickets--;
pthread_mutex_unlock(&mutex);
}
return nullptr;
}
int main()
{
// 通过条件变量控制线程的执行
pthread_t t[5];
for (int i = 0; i < 5; i++)
{
char *name = new char[64];
snprintf(name, 64, "thread %d", i + 1);
pthread_create(&t[i], nullptr, start_routine, name);
}
while (true)
{
sleep(1);
pthread_cond_signal(&cond);
std::cout << "main thread wakeup one thread" << std::endl;
}
for (int i = 0; i < 5; i++)
{
pthread_join(t[i], nullptr);
}
return 0;
}
唤醒方式
唤醒单个线程:
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒多个线程:
int pthread_cond_signal(pthread_cond_t *cond);
运行结果图如下: