目录
引言
大家有任何疑问,可以在评论区留言或者私信我,我一定尽力解答。
今天我们学习Linux线程互斥的话题。Linux同步和互斥是Linux线程学习的延伸。但这部分挺有难度的,请大家做好准备。那我们就正式开始了。
🚩看现象,说原因
我们先上一段代码:
cpp
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<cassert>
using namespace std;
int NUM=5;
int ticket=1000;
class pthread
{
public:
char buffer[1024];
pthread_t id;
};
void *get_ticket(void *args)
{
pthread *pth=static_cast<pthread*>(args);
while(1)
{
usleep(1234);
if(ticket<0)
{
return nullptr;
}
cout<<pth->buffer<<" is ruuing ticket: "<<ticket<<endl;
ticket--;
}
}
int main()
{
vector<pthread*> pthpool;
for(int i=0;i<NUM;i++)
{
pthread* new_pth=new pthread();
snprintf(new_pth->buffer,sizeof (new_pth->buffer),"thread-%d",i+1);
int n=pthread_create(&(new_pth->id),nullptr,get_ticket,new_pth);
assert(n==0);
(void)n;
pthpool.push_back(new_pth);
}
for(int i=0;i<pthpool.size();i++)
{
int m= pthread_join(pthpool[i]->id,nullptr);
assert(m==0);
(void)m;
}
return 0;
}
这段代码模拟的是抢票模型,一共有一千张票,我们让几个线程同时去抢票。看看有什么不符合实际的情况发生。
还真有不符合实际的情况发生:竟然抢到了负票。卧槽,这是什么情况,我们赶紧分析一下。
首先,在代码中我们定义了一个全局变量:ticket 。这个变量被所有线程所共享。
对于这种情形,我们直接拉向极端情况:假设此时的票数只有一张了。一个线程进入if内部,但是对票数还没有进行操作,这时,时间片到了,这个线程被切了下去。紧接着,一个线程就通过if判断,顺利抢到了最后一张票,对票数进行了操作。此时已经无票可抢了。这时,那个被切下来的线程又带着它的数据开始了抢票。但是在这个线程看来,票数依旧还有最后一张,所以,它又对票数进行了减减操作,得到了负票。
这种情况显然是不合理的,假如一个电影院有100个座位,结果卖出去102张票,这怎么可以呢?
我们定义的全局变量,在没有保护的情况下,往往晒不安全的。像上面多个线程在交替执行时造成的数据安全问题,我们称之为出现了数据不一致问题。
这就是个坑啊,必须解决。
🚩解决方案
在提出解决方案之前,我们先回顾几个概念。
- 多个执行流进行安全访问的共享资源,叫做临界资源
- 我们把多个执行流中,访问临界资源的代码叫做临界区,临界区往往是线程代码很小的一部分。
- 想让多个线程串行访问共享资源的方式叫做互斥。
- 对一个资源进行访问的时候,要么不做,要么做完,这种特性叫做**原子性。**一个对资源进行的操作,如果只有一挑汇编语句完成,那么就是原子的,反之就不是原则的。这是当前我们对原子性的理解,后面还会发生改变。
我们提出的解决方案就是加锁。相信大家第一次听到锁。对于什么是锁,如何加锁,锁的原理是什么我们都不清楚,别着急,我们在接下来的内容里会进行详细的详解。
我们先使用一下锁,见见猪跑!!
cpp
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<cassert>
using namespace std;
int NUM=5;
int ticket=1000;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
class pthread
{
public:
char buffer[1024];
pthread_t id;
};
void *get_ticket(void *args)
{
pthread *pth=static_cast<pthread*>(args);
while(1)
{
pthread_mutex_lock(&mutex);
usleep(1234);
if(ticket<0)
{
pthread_mutex_unlock(&mutex);
return nullptr;
}
cout<<pth->buffer<<" is ruuing ticket: "<<ticket<<endl;
ticket--;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
vector<pthread*> pthpool;
for(int i=0;i<NUM;i++)
{
pthread* new_pth=new pthread();
snprintf(new_pth->buffer,sizeof (new_pth->buffer),"user-%d",i+1);
int n=pthread_create(&(new_pth->id),nullptr,get_ticket,new_pth);
assert(n==0);
(void)n;
pthpool.push_back(new_pth);
}
for(int i=0;i<pthpool.size();i++)
{
int m= pthread_join(pthpool[i]->id,nullptr);
assert(m==0);
(void)m;
}
return 0;
}
结果显示抢票的过程非常顺利,接下来,我们把重心指向锁。
🚩互斥锁
首先,我们先认识一些锁的常见接口
cpp
// 所有锁的相关操作函数都在这个头文件下
//这些函数如果又返回值,操作成功的话,返回0,失败的话。返回错误码。错误原因被设置
#include <pthread.h>
// 锁的类型,用来创建锁
pthread_mutex_t
// 对锁进行初始化,第二个参数一般设位null
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 如果这个锁没有用了,可以调用该函数对锁进行销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 如果创建的锁是全局变量,可以这样初始化。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 对特定代码部分进行上锁,这部分代码只能有一次只能有一个执行流进入,被保护的资源叫做临界资源。
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 尝试上锁,不一定成功。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 取消锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
刚刚,我们已经使用一种方式实现了加锁,接下来,我们用另一种方式:
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cassert>
using namespace std;
int NUM = 5;
int ticket = 1000;
class Thread_Data
{
public:
Thread_Data(string name,pthread_mutex_t* mutex):_name(name),_mutex(mutex)
{}
~Thread_Data()
{}
public:
string _name;
pthread_mutex_t* _mutex;
};
void *get_ticket(void *args)
{
Thread_Data *pth = static_cast<Thread_Data*>(args);
while (1)
{
pthread_mutex_lock(pth->_mutex);
if (ticket > 0)
{
usleep(1234);
cout << pth->_name << " is ruuing ticket: " << ticket << endl;
ticket--;
pthread_mutex_unlock(pth->_mutex);
}
else
{
pthread_mutex_unlock(pth->_mutex);
break;
}
}
}
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,nullptr);
vector<pthread_t> tids(NUM);
for (int i = 0; i < NUM; i++)
{
char buffer[1024];
Thread_Data *td=new Thread_Data(buffer,&mutex);
snprintf(buffer, sizeof(buffer), "user-%d", i + 1);
int n =pthread_create(&tids[i], nullptr, get_ticket, td);
assert(n == 0);
(void)n;
}
for (int i = 0; i < tids.size(); i++)
{
int m = pthread_join(tids[i], nullptr);
assert(m == 0);
(void)m;
}
return 0;
}
运行一下,发现一直是4号线程在跑,其他线程呢?我也没让其他线程退出呀!而且抢票的时间变长了。
- 加锁和解锁是多个线程串行进行的,所以程序允许起来会变得很慢。
- 锁只规定互斥访问,没有规定谁优先访问。
- 锁就是让多个线程公平竞争的结果,强者胜出嘛。
🚀关于互斥锁的理解
- 所有的执行流都可以访问这一把锁,所以锁是一个共享资源。
- 加锁和解锁的过程必须是原子的,不会存在中间状态。要么成功,要么失败。加锁的过程必须是安全的。
- 谁持有锁,谁进入临界区。
如果一个执行流申请锁成功,继续向后运行;如果申请失败的话,这个执行流怎么办?
这种情况试一试不就知道了。我们依旧使用上面的一份代码,稍稍做一下修改:
所以,当一个执行流申请锁失败时,这个执行流会阻塞在这里。
🚀关于原子性的理解
如图,三个执行流
问:如果线程1申请锁成功,进入临界资源,正在访问临界资源区的时候,其他线程在做什么?
答:都在阻塞等待,直到持有锁的线程释放锁。
问; 如果线程1申请锁成功,进入临界资源,正在访问临界资源区的时候,可不可以被切换?
答:绝对是可以的,CPU管你有没有锁呢,时间片到了你必须下来。当持有锁的线程被切下来的时候,
是抱着锁走的,即使自己被切走了,其他线程依旧无法申请锁成功,也就无法继续向后执行。
这就叫作:江湖上没有我,但依旧有我的传说。
所以对于其他线程而言,有意义的锁的状态,无非两种:①申请锁前,②释放锁后
所以,站在其他线程的角度来看待当前持有锁的过程,就是原子的。
所以,未来我们在使用锁的时候,要遵守什么样的原则呢?
- 一定要保证代码的粒度(锁要保护的代码的多少i)要非常小。
- 加锁是程序员的行为,必须做到要加的话所有的线程必须要加锁。
🚀如何理解加锁和解锁是原子的
在分析如何实现加锁和解锁之前,我们先形成几个共识:
- CPU内执行流只有一套,且被所有执行流所共享。
- CPU内寄存器的内容属线程所有,是每个执行流的上下文。时间片到达,数据带走。
- 在进行加锁和解锁的时候,这个线程随时会因时间片已到而被换下来。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性 。
如图:
我们假设有线程A,B两个线程,A想要获得锁
锁内存储的数据就是int类型的1。 A线程中有数字0。
①:movb $0,%al:将线程A中的1move到寄存器中。此时,是有可能发生时间片到达的,但是寄存器内的数据属于线程A,线程A是要带走的。
②:xchgb %al,mutex:将锁中的数据和寄存器内的数据进行交换。此时寄存器内的数据变成1,锁中的数据变为0。这是关键的一步,也有可能会发生切换。假设不巧的很,A线程被切下去了,B线程被切上来了。B线程从第一步开始,走到现在,寄存器内的数据应该是0。然后进入判断体eles进行挂起等待。
③如果在第二步中线程A被切下来,等待一段时间,时间片再次轮到线程A时,A将自己的数据加载到寄存器内进入判断,然后获得锁。
交换的过程由一条汇编构成
交换的本质:共享的数据,交换到线程的上下文中。
那么。如何完成解锁的操作呢。解锁的操作特别简单,只需一步。
将寄存器内的1归还给锁。然后return返回就可以了。
🚩对互斥锁的简单封装
相信大家对互斥锁都有了充分的了解。接下来,我们就实现一下对互斥锁的简单封装。
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cassert>
class Mutex
{
public:
Mutex(pthread_mutex_t *mutex) : _mutex(mutex)
{
}
void unlock()
{
if (_mutex)
{
pthread_mutex_unlock(_mutex);
}
}
void lock()
{
if(_mutex)
{
pthread_mutex_lock(_mutex);
}
}
~Mutex()
{
}
public:
pthread_mutex_t *_mutex;
};
class Lockguard
{
public:
Lockguard(Mutex mutex) : _mutex(mutex)
{
_mutex.lock();
}
~Lockguard()
{
_mutex.unlock();
}
public:
Mutex _mutex;
};
这种利用变量出了函数作用域自动销毁的性质,我们称之为RAII特性。
到这里,我们本篇的内容也就结束了,我们期待下一期博客相遇。