多线程的大部分资源都是共享的,线程之间进行通信不需要费那么大的劲去创建第三方资源。但是如果不对资源进行保护,那么就可能出现意料之外的逻辑错误。
比如下面的抢票程序
#define NUM 5
int tickets=200; //总票数void* routine(void* arg){while(1){if(tickets>0){usleep(30000); //模拟抢票的过程printf("线程:%d抢票成功,票的序号是%d\n",pthread_self(),tickets);tickets--;}else{break;}}return nullptr;
}
int main(){pthread_t tid[NUM];for(int i=0;ipthread_create(&tid[i],nullptr,routine,nullptr);}for(int i=0;ipthread_join(tid[i],nullptr);}
}
逻辑上似乎没用错误,但是代码的结果却出现了票数为负数的情况。
为什么会出现票数为负数的情况?
tickets本身是一个全局变量,是被所有线程所共享的,也就是一个临界资源。在代码的运行过程中,出现了以下的情况:
进行 - - 操作的时候,不是原子的(安全的)。它对应了三条汇编指令:
既然–操作需要三个步骤才能完成,那么thread1可能在任何一个步骤被切走。假设此时thread1读取到的值为1000,而当thread1被切走时,寄存器中的1000被保存到了thread1的上下文数据中。
假设此时thread2被调度了,由于thread1只进行了--
操作的第一步,因此thread2此时看到tickets的值还是1000,而系统给thread2的时间片可能较多,导致thread2一次性执行了100次--
才被切走,最终tickets由1000减到了900。
此时系统再把thread1恢复上来,恢复的本质就是继续执行thread1的代码,并且要将thread1曾经的硬件上下文信息恢复出来,此时寄存器当中的值是恢复出来的1000,然后thread1继续执行--
操作的第二步和第三步,最终将999写回内存。
最后的结果是”凭空多出了100张票"
所以我们可以总结:从内核态返回用户态的时候,OS中线程间进行切换,极有可能出现数据交叉操作,而导致数据不一致的问题。
要解决上面的问题,就需要保证访问临界资源的过程是原子操作。多个线程访问临界区时需要做到下面三点:
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
初始化互斥量
函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数说明:
返回值说明:成功返回0,失败返回错误码
调用pthread_mutex_init函数初始化互斥量叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥量,该方式叫做静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
销毁互斥量
函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数说明:
返回值:销毁成功返回0,失败返回错误码
销毁互斥量需要注意的是:
PTHREAD_MUTEX_INITIALIZER
初始化的互斥量不需要销毁。互斥量加锁与解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
**改进上面的抢票系统:**为临界区加上锁
#include
#include
#include
#include #define NUM 5
int tickets=200; //总票数
pthread_mutex_t mutex;
void* routine(void* arg){while(1){pthread_mutex_lock(&mutex);if(tickets>0){usleep(30000); //模拟抢票的过程printf("线程:%d抢票成功,票的序号是%d\n",pthread_self(),tickets);tickets--;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}return nullptr;
}
int main(){pthread_t tid[NUM];for(int i=0;ipthread_create(&tid[i],nullptr,routine,nullptr);}for(int i=0;ipthread_join(tid[i],nullptr);}
}
首先的一个问题:在加锁的临界区,线程可以被切换吗?
需要理解的是:加锁!=不能被切换;
在加锁的临界区,线程可以被切换。加锁解锁等操作对应的也是代码。线程在任意代码段都可能被切换。
但是线程加锁是原子的,即使线程被切走后,锁也保证了绝对不会有其他线程进入临界区。想要访问临界区,线程就必须要抱锁。一旦一个线程抱锁,该线程就不会担心该锁对应的资源因线程切换而出问题。
**总结:**加锁的线程可以被切换,但是其他线程无法进入临界区。
锁是否需要被保护?
被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。锁需要被多个线程访问,因此锁也是临界资源。
锁是临界资源,那么锁也需要被保护,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?
锁实际是自己保护自己,有关锁的操作都是原子的,那么锁就是安全的。
加锁和解锁具有原子性,如何实现?
下面是lock和unlock的伪代码
lock的执行过程
mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:
如果当前thread1申请锁。xchb交换内存mutex和al寄存器的数据后,寄存器的数据为1,mutex的数据为0。此时thread1申请锁成功。
如果thread2申请锁。由于当前内存mutex的值是0,和al寄存器交换后,两者的数据都是0。因此thread2申请锁失败,线程挂起。
当在申请锁的过程中,线程被切换,如何保证锁的原子性?
首先需要有以下的认识:
比如:如果thread1刚刚交换完内存mutex和寄存器al的数据,但是还没有来得及判断是否申请锁成功就被切走。
此时thread2再去申请锁失败。
当thread1被切回来时,上下文数据恢复到寄存器中,此时al寄存器的数据为1。thread1申请锁成功。
解锁的过程和加锁的过程相同,不同的是初始向al寄存器填充的数据是1
基本概念
常见线程不安全的情况
常见的线程安全的情况
常见的不可重入的情况
常见的可重入的情况
可重入与线程安全联系
可重入与线程安全区别
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态 。
常见死锁情况
定义:死锁是值两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象。 若无外力作用,它们都将无法推进下去,此时称系统处于死锁状态或系统产生了死锁,而这些永远在互相等待的进程称为死锁进程。
例如,如果线程A锁住了记录1并等待记录2,而线程B锁住了记录2并等待记录1,这样两个线程就发生了死锁现象。
函数调用先于释放锁,锁还没来得及释放进程就退出。
std::mutex m;
void func()
{//进程报锁m.lock();if(1){return ;}m.unlock();
}
mutex _mutex;
void func()
{_mutex.lock();//do somrthing....//重复申请锁,第二次申请处于阻塞等待状态_mutex.lock();_mutex.unlock();
}
下面的例子中,process1和process2先对_mutex1和_mutex2上锁。
然后由于 _ mutex2已经上锁,process1会一直阻塞等待 _ mutex2;同样,由于 _ mutex1上锁,process2会一直阻塞等待 _mutex1
mutex _mutex1;
mutex _mutex2;void process1() {_mutex1.lock();_mutex2.lock();//do something1..._mutex2.unlock();_mutex1.unlock();
}void process2() {_mutex2.lock();_mutex1.lock();//do something2..._mutex1.unlock();_mutex2.unlock();
}
死锁的四个必要条件
注意:四个条件都要满足才会出现死锁的情况
避免死锁
避免死锁的算法
同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。
线程互斥,在一定的场景下是不合理的。可能出现饥饿现象:一个执行流长时间得不到某种资源。
例如:在互斥量的时候,我们解决了共享变量的问题,但是我们在创建锁的过程中,可能会存在一个优先级高的线程,每次都是它优先申请到锁。该线程一直在申请锁、检测(进行抢票)、释放锁,导致其他线程没有机会得到这把锁(俗称饥饿问题)。这样错了嘛?显然没有,但是这样安排是不合理的。
排队的本质:让线程在获取锁安全的前提下,按照某种顺序进行申请和释放锁,让每个线程都有机会申请到锁,这就叫做同步。
条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。
条件变量主要有两个动作
初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数说明:
返回值:条件变量初始化成功返回0,失败返回错误码。
调用pthread_cond_init函数初始化条件变量叫做动态分配,除此之外,我们还可以用下面这种方式初始化条件变量,该方式叫做静态分配:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond)
注意:使用PTHREAD_COND_INITIALIZER
初始化的条件变量不需要销毁。
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
两个接口的区别:
实例:使用pthread_cond_signal依次唤醒线程
#include
#include
#include pthread_cond_t cond;
pthread_mutex_t mutex;
void* waitcommand(void* arg){char* s=(char*)arg;while(1){pthread_cond_wait(&cond,&mutex);int cnt=3;while (cnt--){printf("%s is running...\n",s);sleep(1);}}return nullptr;
}
int main(){pthread_cond_init(&cond,nullptr);pthread_mutex_init(&mutex,nullptr);pthread_t tid[3];for(int i=0;i<3;i++){char* s=(char*)malloc(32);sprintf(s,"thread %d",i);pthread_create(&tid[i],nullptr,waitcommand,(void*)s);}//循环的唤醒执行线程while(1){pthread_cond_signal(&cond);sleep(3);}for(int i=0;i<3;i++){pthread_join(tid[i],nullptr);}return 0;
}
使用pthread_cond_broadcast唤醒所有线程
#include
#include
#include pthread_cond_t cond;
pthread_mutex_t mutex;
void* waitcommand(void* arg){char* s=(char*)arg;while(1){pthread_cond_wait(&cond,&mutex);int cnt=3;while (cnt--){printf("%s is running...\n",s);sleep(1);}}return nullptr;
}
int main(){pthread_cond_init(&cond,nullptr);pthread_mutex_init(&mutex,nullptr);pthread_t tid[3];for(int i=0;i<3;i++){char* s=(char*)malloc(32);sprintf(s,"thread %d",i);pthread_create(&tid[i],nullptr,waitcommand,(void*)s);}//循环的唤醒执行线程while(1){pthread_cond_broadcast(&cond);sleep(3);}for(int i=0;i<3;i++){pthread_join(tid[i],nullptr);}return 0;
}
而实际进入pthread_cond_wait
函数后,会先判断条件变量是否等于0,若等于0则说明不满足,此时会先将对应的互斥锁解锁,直到pthread_cond_wait
函数返回时再将条件变量改为1,并将对应的互斥锁加锁。
条件变量的使用规范
等待条件
pthread_mutex_lock(&mutex);
while (条件为假)pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);