Linux:44线程互斥lesson32
目录
线程互斥
抢票的代码(已加锁)
问题:未加锁的代码为什么票数会减到了负数???
(1)数据不一致问题:
(2)判断引起的!!!
引入线程互斥
线程互斥概念
解决以上问题的解决方法。
创建锁的方式/互斥量的接⼝
1.定义全局的锁/静态分配
编辑
2.动态分配
(1)锁的初始化
(2)销毁互斥量
互斥量加锁和解锁
代码1:对静态锁和动态锁的代码实现
延伸:
进程间互斥???
理解锁
编辑 锁的原理:
(1)硬件级实现锁:关闭时钟中断,这样的话,就不会进行线程切换的情况
(2)软件级实现锁:
解释:
对于%al有很多歌,而mutex只有一个1的解释
总结:
代码2:对锁的封装+RAII
test_Mutex.cpp
Mutex.hpp
1. RAII 的作用
线程互斥
抢票的代码(已加锁)
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 1000;void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0) // 1. 判断{usleep(1000); // 模拟抢票花的时间printf("%s sells ticket:%d\n", id, ticket); // 2. 抢到了票ticket--; // 3. 票数--}else{break;}}return nullptr;
}int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
问题:未加锁的代码为什么票数会减到了负数???
(1)数据不一致问题:
在C语言中‘--’会被切换成3条汇编语言,不是原子性的,在进行汇编过程中会发生线程切换
-- 操作并不是原⼦操作,⽽是对应三条汇编指令:• load :将共享变量ticket从内存加载到寄存器中• update :更新寄存器⾥⾯的值,执⾏-1操作• store :将新值,从寄存器写回共享变量ticket的内存地址
切换,pc记录上下文,进入等待队列切换:什么时候???
1.时间片用完
2.阻塞是IO
3.sleep.....(切走)什么时候选择新:从内核态返回用户态的时候进行检查!!
陷入等待,选择新的继续执行。
拿这个图举个例子,假如ticket == 10000,第一个线程A,通过判断进入临界区,执行完ticket ----操作,拿到ticket == 9999,准备继续执行的时候被切换了,后序线程把ticket抢到只剩1的时候,轮到线程A开始执行了,准备把ticket写会去的时候,结果,本来ticket == 1,又变成 9999 了,导致了数据不一致的问题。
(2)判断引起的!!!
举个例子,假如ticket == 10000,第一个线程A,通过判断进入临界区,准备执行的时候被切换了,然后线程B同样如此,后序线程把ticket抢到只剩1的时候,轮到线程A和线程B开始执行了,他们开始的时候都通过判断进入临界区执行流,对ticket进行 -- 操作,结果ticket就出现负数了。
引入线程互斥
线程互斥概念
• 临界资源:多线程执⾏流共享的资源就叫做临界资源
• 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
• 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起 保护作⽤
• 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成
解决以上问题的解决方法。
• 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
• 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程 进⼊该临界区。
• 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区
要做到这三点,本质上就是需要⼀把锁。Linux上提供的这把锁叫互斥量。
创建锁的方式/互斥量的接⼝
1.定义全局的锁/静态分配
原则:尽量加锁的范围粒度要细,尽量不要加太多非临界区代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
2.动态分配
(1)锁的初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);参数:mutex:要初始化的互斥量attr:NULL
(2)销毁互斥量
销毁互斥量需要注意:
• 使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
• 不要销毁⼀个已经加锁的互斥量
• 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);返回值:成功返回0,失败返回错误号
代码1:对静态锁和动态锁的代码实现
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 1000;
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;全局锁,静态所
pthread_mutex_t mutex;//动态锁void *route(void *arg)
{char *id = (char *)arg;while (1){// pthread_mutex_lock(&lock);pthread_mutex_lock(&mutex);if (ticket > 0) // 1. 判断{usleep(1000); // 模拟抢票花的时间printf("%s sells ticket:%d\n", id, ticket); // 2. 抢到了票ticket--; // 3. 票数--// pthread_mutex_unlock(&lock);pthread_mutex_unlock(&mutex);}else{// pthread_mutex_unlock(&lock);pthread_mutex_unlock(&mutex);break;}}return nullptr;
}int main(void)
{pthread_mutex_init(&mutex,nullptr);pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
注意事项:
(1)全局锁不需要释放,程序运行结束,会自动释放掉
(2)动态锁: 需要初始化,和进行销毁。
(3)c++11里面的mutex的锁
#include <mutex>
std::mutex cpp_lock;
cpp_lock.lock();
cpp_lock.unlock();
延伸:
进程间互斥???
同理:共享内存,shm,(pthread_mutex t*)shm
对共享内存进行加锁,实际上跟对线程临界区本质是一样的。
理解锁
对临界资源进行保护:本质就是用锁,来对临界区代码进行保护
问题·1:如果有一个线程,不遵守你的规则?
-----------写bug,所有线程都必遵守
问题2:加锁之后,在临界区内部,允许线程切换吗,切换了后会怎么样(重点)!!!
答:允许切换的?!
为什么?
因为允许切换,但是我当前进程没有释放锁,我是持有锁被切换的,即便我不在,其他线程也得等我执行完代码,释放锁,其他线程才能展开锁,进入临界区。
结论:所有线程必须等我跑完,其他的线程才可以运行,锁的能力本质:把并行转化成串行
锁的原理:
(1)硬件级实现锁:关闭时钟中断,这样的话,就不会进行线程切换的情况
(2)软件级实现锁:
为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange指令,该指令的作⽤是把寄存器和 内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,即使是多处理器平台,访问内存的总线周 期也有先后,⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。 |
解释:
(1)锁就是一种标记位,可以当做一个整数mutex
(2)%al:一个寄存器,要么记录0,要么记录1
(3)lock执行完,al里面的内容为1,则return 0表示申请所以成功,继续执行后面代码
(4)否则,就挂起等待,等待go to lock
对于%al有很多歌,而mutex只有一个1的解释
进程/线程切换:CPU内的寄存器硬件只有一套(metex = 1,只有一个),但CPU寄存器内的数据可以有多份(各个线程的%al)
如下:
CPU硬件寄存器只有一套,
但里面的数据,线程A的数据,线程B的数据...
里面的数据可以有很多份。换句话说,如果把一个变量的内容,交换到cpu寄存区的内部,本质是:把该变量的内容,获取到当前执行流的硬件上下文中,当前CPU寄存器的硬件上下文(起始就是各个寄存器的内容)
属于:进程/线程私有的
总结:
我们用swap,exchange将内存中的变量,交换到CPU的寄存器中“本质是当前线程/进程,在获取锁,因为是交换,不是拷贝,所以1只有1份,所以谁申请到,谁的al > 1,mutex就jiaohuan谁持有锁” |
锁就是mutex的内容1,谁的%al持有1,谁就持有锁。 |
释放锁,就是向mutex里面写1就可以了。 |
代码3:自动释放锁
代码2:对锁的封装+RAII
test_Mutex.cpp
// 操作共享变量会有问题的售票系统代码
#include <iostream>
#include <mutex>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"using namespace MutexModule;class ThreadData
{
public:ThreadData(const std::string &n, Mutex &lock): name(n),lockp(&lock){}~ThreadData() {}std::string name;Mutex *lockp;
};int ticket = 1000;
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;全局锁,静态所 ---1
// pthread_mutex_t mutex;//动态锁 ---2
// Mutex* lock = new Mutex();//模版对象 ---3,指针要动态分配内存,不然行不通
void *route(void *arg)
{// char *id = (char *)arg;// while (1)// {// // pthread_mutex_lock(&lock);// // pthread_mutex_lock(&mutex);// // lock->Lock();// if (ticket > 0) // 1. 判断// {// usleep(1000); // 模拟抢票花的时间// printf("%s sells ticket:%d\n", id, ticket); // 2. 抢到了票// ticket--; // 3. 票数--// // pthread_mutex_unlock(&lock);// // pthread_mutex_unlock(&mutex);// // lock->Unlock();// }// else// {// // pthread_mutex_unlock(&lock);// // pthread_mutex_unlock(&mutex);// // lock->Unlock();// break;// }// }// return nullptr;ThreadData* td = static_cast<ThreadData *>(arg);while (1){LockGuard guard(*(td->lockp)); // 加锁完成, RAII风格的互斥锁的实现if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}else{break;}usleep(123);}return nullptr;
}int main(void)
{// pthread_mutex_init(&mutex,nullptr);// pthread_t t1, t2, t3, t4;// pthread_create(&t1, NULL, route, (void *)"thread 1");// pthread_create(&t2, NULL, route, (void *)"thread 2");// pthread_create(&t3, NULL, route, (void *)"thread 3");// pthread_create(&t4, NULL, route, (void *)"thread 4");// pthread_join(t1, NULL);// pthread_join(t2, NULL);// pthread_join(t3, NULL);// pthread_join(t4, NULL);Mutex lock;pthread_t t1, t2, t3, t4;ThreadData *td1 = new ThreadData("thread 1", lock);pthread_create(&t1, NULL, route, td1);ThreadData *td2 = new ThreadData("thread 2", lock);pthread_create(&t2, NULL, route, td2);ThreadData *td3 = new ThreadData("thread 3", lock);pthread_create(&t3, NULL, route, td3);ThreadData *td4 = new ThreadData("thread 4", lock);pthread_create(&t4, NULL, route, td4);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
Mutex.hpp
#pragma
#include<iostream>
#include<pthread.h>
#include<cstring>
namespace MutexModule{class Mutex{public:Mutex(){//初始化pthread_mutex_init(&mutex,nullptr);}//上锁void Lock(){int n = pthread_mutex_lock(&mutex);if(n != 0)//上锁失败{std::cerr<<"lock error:"<<strerror(n)<<std::endl;}}void Unlock(){int n = pthread_mutex_unlock(&mutex);if(n != 0)//开锁失败{std::cerr<<"unlock error:"<<strerror(n)<<std::endl;}}~Mutex(){}private:pthread_mutex_t mutex;};class LockGuard{public:LockGuard(Mutex &mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;};
}
1. RAII 的作用
RAII 是一种资源管理机制,通过将资源的生命周期绑定到对象的生命周期来管理资源。在 C++ 中,对象的生命周期通常由其构造函数和析构函数控制:
构造函数:负责获取资源(如加锁)。
析构函数:负责释放资源(如解锁)。
在你的代码中:
cpp
复制
LockGuard guard(*td->lockp);
LockGuard
的构造函数会调用td->lockp
的加锁操作,确保当前线程获取互斥锁。当
LockGuard
对象超出作用域时(如退出while
循环的当前迭代),其析构函数会被自动调用,从而释放互斥锁。