文章目录
Linux线程互斥
一、进程线程间的互斥相关概念
1.临界资源和临界区
2.互斥和原子性
二、互斥量mutex
通过前边的学习,我们知道了线程之间的通信十分简单,除了线程的栈空间和上下文数据是私有的,其他的数据和内容都是共享的,所以我们定义全局变量就可以直接进行通信,但是在可以进行通信之后,也会带来很多问题,例如可能多个线程在同一时刻发送数据或接收数据,多个线程并发操作,可能会导致数据异常,所以为了解决这个问题,我们就引入互斥量的概念。
1.抢票程序是否引入互斥量现象观察
我们通过演唱会抢票的例子来学习互斥量:
首先来看看不引入互斥量的情况:
#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <mutex>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class tickets
{
private:
int ticket;
pthread_mutex_t mx;
public:
tickets()
: ticket(1000)
{
//初始化互斥锁
pthread_mutex_init(&mx, nullptr);
}
~tickets()
{
//销毁互斥锁
pthread_mutex_destroy(&mx);
}
bool buy_tickets()
{
bool ret = true;
if (ticket > 0)
{
usleep(1000);
cout << "我是[" << pthread_self() << "]线程,我正在抢第" << ticket << "张票" << endl;
ticket--;
printf("");
}
else
{
cout << "票已经卖完了" << endl;
ret = false;
}
return ret;
}
};
void *pthread_run(void *args)
{
tickets *t = (tickets *)args;
while (true)
{
if (!t->buy_tickets())
break;
}
}
int main()
{
tickets *t = new tickets();
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
pthread_create(tid + i, nullptr, pthread_run, (void *)t);
}
for (int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
在不引入互斥量时,我们会发现惊奇的现象,就是可能会有多个线程抢同一张票,导致最后会出现负数票的情况,这是万万不能接受的。
接下来,我们再来看一下引入互斥量的现象:
#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <mutex>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class tickets
{
private:
int ticket;
pthread_mutex_t mx;
public:
tickets()
: ticket(1000)
{
//初始化互斥锁
pthread_mutex_init(&mx, nullptr);
}
~tickets()
{
//销毁互斥锁
pthread_mutex_destroy(&mx);
}
bool buy_tickets()
{
bool ret = true;
//加锁
pthread_mutex_lock(&mx);
if (ticket > 0)
{
usleep(1000);
cout << "我是[" << pthread_self() << "]线程,我正在抢第" << ticket << "张票" << endl;
ticket--;
printf("");
}
else
{
cout << "票已经卖完了" << endl;
ret = false;
}
//解锁
pthread_mutex_unlock(&mx);
return ret;
}
};
void *pthread_run(void *args)
{
tickets *t = (tickets *)args;
while (true)
{
if (!t->buy_tickets())
break;
}
}
int main()
{
tickets *t = new tickets();
//tickets* t;
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
pthread_create(tid + i, nullptr, pthread_run, (void *)t);
}
for (int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
在引入互斥量之后,就不会出现有多个线程同时抢一张票而导致最后出现负数的情况。
2.抢票程序原理分析
为什么会出现负票数呢?
有很多人可能会说,–ticket不就是一条语句吗?为什么不是原子性的呢?
先来看–ticket转换为汇编的代码:
此时,我们必须知道,虽然寄存器是被每一个线程共享的,但是寄存器中的数据确实私有的,当某一个线程时间片到了之后被切走,会将寄存器中的数据也带走保存在自己的上下文当中,所以此时在进行–ticket操作时就有可能出现下边的情况:
步骤一:
步骤二:
步骤三:
经过上边的三步,我们就会发现,–ticket并不是原子性的,可能在执行某一行代码时,可能该线程被切走,所以导致数据异常,出现负数票的情况。
要解决以上问题,需要做到三点:
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
3.互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
销毁互斥量
销毁互斥量需要注意:
互斥量加锁和解锁
4. 加锁后的程序
#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <mutex>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class tickets
{
private:
int ticket;
pthread_mutex_t mx;
public:
tickets()
: ticket(1000)
{
//初始化互斥锁
pthread_mutex_init(&mx, nullptr);
}
~tickets()
{
//销毁互斥锁
pthread_mutex_destroy(&mx);
}
bool buy_tickets()
{
bool ret = true;
//加锁
pthread_mutex_lock(&mx);
if (ticket > 0)
{
usleep(1000);
cout << "我是[" << pthread_self() << "]线程,我正在抢第" << ticket << "张票" << endl;
ticket--;
printf("");
}
else
{
cout << "票已经卖完了" << endl;
ret = false;
}
//解锁
pthread_mutex_unlock(&mx);
return ret;
}
};
void *pthread_run(void *args)
{
tickets *t = (tickets *)args;
while (true)
{
if (!t->buy_tickets())
break;
}
}
int main()
{
tickets *t = new tickets();
pthread_t tid[5];
for (int i = 0; i < 5; i++)
{
pthread_create(tid + i, nullptr, pthread_run, (void *)t);
}
for (int i = 0; i < 5; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
5.互斥量原理探究
经过上边的证明,我们知道了ticket–或者ticket++都不是原子性的,那么为什么加入一个锁可以保证原子性呢?
这是因为在一批线程来竞争某一资源,当其中一个线程先申请到锁时,此时其他线程再来申请锁,锁告诉线程,不好意思,锁已经属于别人了,此时无论哪个线程再来申请,都不可能申请成功,只有当拥有锁的线程将锁释放,也就是归还之后,其他的线程才可能申请成功,这样就保证了互斥性和原子性。
但是我们又忽视了一个情况,需要访问临界资源,必须先来申请锁,所以锁是肯定要被所有线程看到了,那么问题来了,锁不就是一个临界资源吗?而且处理成汇编也不只是一个语句,那么锁是原子性的吗?
来分析一下汇编代码:
所以mutex的初始值为1,当一个线程来申请锁时,这个线程的al寄存器会将0换给mutex,而al的值变为1,当第二个线程来申请锁,先将al寄存器的值清零,然后与mutex交换,得到的值还是0
,所以就会挂起等待。哪怕拿到锁的线程暂时被挂起了,也会将al寄存器的值保存在自己的上下文当中,也就是将锁带走了,其他线程也不能申请到锁。
可重入VS线程安全
一、概念
1.线程安全
2.重入
二、常见的线程不安全的情况
三、常见的线程安全的情况
四、常见不可重入的情况
五、常见可重入的情况
六、可重入与线程安全联系
七、可重入与线程安全区别
常见锁概念
一、死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
并且,单执行流也可以造成死锁,例如下边的程序:
#include<iostream>
#include<pthread.h>
using namespace std;
pthread_mutex_t mtx;
void* thread_run(void* args)
{
pthread_mutex_lock(&mtx);
Spthread_nutex_lock(&mtx);
pthread_exit((void*)0);
// char* name = (char*)args;
// cour<<i am a <<name<<endl;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,thread_run,(void*)"new thread");
pthread_mutex_init(&mtx,nullptr);
pthread_join(tid,nullptr);
pthread_mutex_destroy(&mtx);
return 0;
}
二、死锁四个必要条件
也就是说如果出现了死锁,就一直达到了以上的条件。
三、避免死锁
Linux线程同步
一、同步概念与竞态条件
我们前边提到了,线程可以访问同一份资源,可能会出现数据异常的问题,所以我们引入了互斥的概念,通过加锁来使同一时间只有一个线程访问临界资源,当加锁之后,可能又会因为某一线程的竞争力过强,一直处于加锁和销毁锁的状态,导致其他线程不能去访问临界资源而导致饥饿问题,所以我们就要引入线程同步的概念,当一个线程在销毁锁之后,不能立马去申请锁,而是要到队列的末尾排序,进入条件变量的等待队列,不会造成其他的线程长时间不能访问临界资源的情况。
二、条件变量
当临界资源加锁之后,我们不容易知道临界资源的状态,这时我们可以引入临界变量来获得临界资源的状态,下边我来举一个生活中的例子:
1. 条件变量函数初始化
与互斥锁的初始化类似:
2.条件变量函数销毁
3.条件变量函数等待条件满足
4.唤醒等待
第一个是唤醒等待一批线程
第二个是唤醒等待一个线程
5.程序示例
我们现在实现一个老板控制工人工作的例子,来观察一个线程可以通过条件变量来控制其他的线程。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
pthread_mutex_t mx;
pthread_cond_t cond;
void* worker_routine(void* args)
{
while(true)
{
int num = *(int*)args;
pthread_cond_wait(&cond,&mx);
cout<<"worker"<<num<<"is working..."<<endl;
}
}
void* boss_routine(void* args)
{
while(true)
{
cout<<"boss say:";
pthread_cond_signal(&cond);
sleep(1);
}
}
int main()
{
pthread_t boss;
pthread_t tid[3];
pthread_mutex_init(&mx,nullptr);
pthread_cond_init(&cond,nullptr);
for(int i=0;i<3;i++)
{
int* num = new int(i);
pthread_create(tid+i,nullptr,worker_routine,(void*)num);
}
pthread_create(&boss,nullptr,boss_routine,(void*)"boss");
for(int i=0;i<3;i++)
{
pthread_join(tid[i],nullptr);
}
pthread_join(boss,nullptr);
return 0;
}