做资讯的网站,微信公众号推广平台,开发门户网站,网站栏目列表目录
#x1f6a9;看现象#xff0c;说原因
#x1f6a9;解决方案
#x1f6a9;互斥锁 #x1f680;关于互斥锁的理解
#x1f680;关于原子性的理解
#x1f680;如何理解加锁和解锁是原子的
#x1f6a9;对互斥锁的简单封装 引言
大家有任何疑问#xff0c;可…目录
看现象说原因
解决方案
互斥锁 关于互斥锁的理解
关于原子性的理解
如何理解加锁和解锁是原子的
对互斥锁的简单封装 引言
大家有任何疑问可以在评论区留言或者私信我我一定尽力解答。
今天我们学习Linux线程互斥的话题。Linux同步和互斥是Linux线程学习的延伸。但这部分挺有难度的请大家做好准备。那我们就正式开始了。
看现象说原因
我们先上一段代码
#includeiostream
#includepthread.h
#includeunistd.h
#includevector
#includecassert
using namespace std;
int NUM5;
int ticket1000;
class pthread
{
public:char buffer[1024];pthread_t id;
};
void *get_ticket(void *args)
{pthread *pthstatic_castpthread*(args);while(1){usleep(1234);if(ticket0){return nullptr;}coutpth-buffer is ruuing ticket: ticketendl;ticket--;}
}int main()
{vectorpthread* pthpool;for(int i0;iNUM;i){pthread* new_pthnew pthread();snprintf(new_pth-buffer,sizeof (new_pth-buffer),thread-%d,i1);int npthread_create((new_pth-id),nullptr,get_ticket,new_pth);assert(n0);(void)n;pthpool.push_back(new_pth);}for(int i0;ipthpool.size();i){int m pthread_join(pthpool[i]-id,nullptr);assert(m0);(void)m;}return 0;}
这段代码模拟的是抢票模型一共有一千张票我们让几个线程同时去抢票。看看有什么不符合实际的情况发生。 还真有不符合实际的情况发生竟然抢到了负票。卧槽这是什么情况我们赶紧分析一下。
首先在代码中我们定义了一个全局变量ticket 。这个变量被所有线程所共享。
对于这种情形我们直接拉向极端情况假设此时的票数只有一张了。一个线程进入if内部但是对票数还没有进行操作这时时间片到了这个线程被切了下去。紧接着一个线程就通过if判断顺利抢到了最后一张票对票数进行了操作。此时已经无票可抢了。这时那个被切下来的线程又带着它的数据开始了抢票。但是在这个线程看来票数依旧还有最后一张所以它又对票数进行了减减操作得到了负票。
这种情况显然是不合理的假如一个电影院有100个座位结果卖出去102张票这怎么可以呢 我们定义的全局变量在没有保护的情况下往往晒不安全的。像上面多个线程在交替执行时造成的数据安全问题我们称之为出现了数据不一致问题。 这就是个坑啊必须解决。
解决方案
在提出解决方案之前我们先回顾几个概念。 多个执行流进行安全访问的共享资源叫做临界资源我们把多个执行流中访问临界资源的代码叫做临界区临界区往往是线程代码很小的一部分。想让多个线程串行访问共享资源的方式叫做互斥。对一个资源进行访问的时候要么不做要么做完这种特性叫做原子性。一个对资源进行的操作如果只有一挑汇编语句完成那么就是原子的反之就不是原则的。这是当前我们对原子性的理解后面还会发生改变。 我们提出的解决方案就是加锁。相信大家第一次听到锁。对于什么是锁如何加锁锁的原理是什么我们都不清楚别着急我们在接下来的内容里会进行详细的详解。
我们先使用一下锁见见猪跑
#includeiostream
#includepthread.h
#includeunistd.h
#includevector
#includecassert
using namespace std;
int NUM5;
int ticket1000;
pthread_mutex_t mutexPTHREAD_MUTEX_INITIALIZER;
class pthread
{
public:char buffer[1024];pthread_t id;
};
void *get_ticket(void *args)
{pthread *pthstatic_castpthread*(args);while(1){ pthread_mutex_lock(mutex);usleep(1234);if(ticket0){ pthread_mutex_unlock(mutex);return nullptr;}coutpth-buffer is ruuing ticket: ticketendl;ticket--;pthread_mutex_unlock(mutex);}
}int main()
{vectorpthread* pthpool;for(int i0;iNUM;i){pthread* new_pthnew pthread();snprintf(new_pth-buffer,sizeof (new_pth-buffer),user-%d,i1);int npthread_create((new_pth-id),nullptr,get_ticket,new_pth);assert(n0);(void)n;pthpool.push_back(new_pth);}for(int i0;ipthpool.size();i){int m pthread_join(pthpool[i]-id,nullptr);assert(m0);(void)m;}return 0;} 结果显示抢票的过程非常顺利接下来我们把重心指向锁。
互斥锁 首先我们先认识一些锁的常见接口
// 所有锁的相关操作函数都在这个头文件下
//这些函数如果又返回值操作成功的话返回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);
刚刚我们已经使用一种方式实现了加锁接下来我们用另一种方式
#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_castThread_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);vectorpthread_t tids(NUM); for (int i 0; i NUM; i){char buffer[1024];Thread_Data *tdnew 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指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性 。
如图 我们假设有线程AB两个线程A想要获得锁
锁内存储的数据就是int类型的1。 A线程中有数字0。 ①movb $0%al将线程A中的1move到寄存器中。此时是有可能发生时间片到达的但是寄存器内的数据属于线程A线程A是要带走的。 ②xchgb %almutex将锁中的数据和寄存器内的数据进行交换。此时寄存器内的数据变成1,锁中的数据变为0。这是关键的一步也有可能会发生切换。假设不巧的很A线程被切下去了B线程被切上来了。B线程从第一步开始走到现在寄存器内的数据应该是0。然后进入判断体eles进行挂起等待。 ③如果在第二步中线程A被切下来等待一段时间时间片再次轮到线程A时A将自己的数据加载到寄存器内进入判断然后获得锁。 交换的过程由一条汇编构成
交换的本质共享的数据交换到线程的上下文中。
那么。如何完成解锁的操作呢。解锁的操作特别简单只需一步。 将寄存器内的1归还给锁。然后return返回就可以了。 对互斥锁的简单封装
相信大家对互斥锁都有了充分的了解。接下来我们就实现一下对互斥锁的简单封装。
#include iostream
#include pthread.h
#include unistd.h
#include vector
#include cassertclass 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特性。
到这里我们本篇的内容也就结束了我们期待下一期博客相遇。