【Linux笔记】——线程互斥与互斥锁的封装
🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:Linux
🌹往期回顾🌹:【Linux笔记】——Linux线程封装
🔖流水不争,争的是滔滔不息
- 一、线程互斥的概念
- 二、互斥量
- 三、互斥量接口
- 初始化互斥量
- 销毁互斥量
- 互斥量加锁与解锁
- 四、对互斥量底层进行理解
- 五、互斥锁的封装
一、线程互斥的概念
线程互斥是一种用于多线程编程的技术,旨在确保同一时间只有一个线程可以访问共享资源。在多线程环境中,多个线程可能会同时访问和修改共享数据,导致数据不一致或竞态条件。通过使用互斥机制,可以避免这些问题,确保线程安全。
线程互斥说白了,就是让线程串行访问资源,避免线程并行访问资源使数据出现错乱。比如一个资源被一个线程访问并执行得到的结果已经是正确的结果了,这时候又来了一个线程,这个线程又访问并执行一遍这个资源,数据就会出现错乱。
这里注意不要混淆,线程互斥是目的,互斥量是工具,锁是通用属于。
二、互斥量
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来⼀些问题。
#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);
}
这个代码在不加锁的情况下非常容易出问题。原因就在于 ticket > 0 的判断和 ticket-- 之间不是原子操作,也就是说多个线程可能会同时判断通过,然后都执行了 ticket–,导致数据错乱。
if语句判断条件为真以后,代码可以并发的切换到其他线程。usleep(1000); 模拟抢票业务的过程,在这个过程中可能有很多线程线程进入该代码。这就不是原子的。
解决上述问题,一、代码要有互斥行为,当代码进入临界区执行的时候,其他线程不允许进入该临界区。二、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只允许一个线程进入该临界区。三、如果线程不在临界区执行,那么该线程不能阻止其他线程进入临界区。为了做到上面这三点,本质上需要一把锁,Linux把这把锁叫做互斥量。
什么是原子性
原子操作是指在多线程或多进程环境中,某个操作一旦开始就不会被中断,直到操作完成。这种操作要么完全执行,要么完全不执行,不会出现部分执行的情况。原子操作常用于确保数据的一致性,特别是在并发编程中。
原子操作具有不可分割性,即在执行过程中不会被其他线程或进程打断。这种特性使得原子操作非常适合用于实现锁、信号量等同步机制。
原子操作广泛应用于并发编程中,特别是在需要确保数据一致性的场景。例如,在多线程环境下,多个线程可能会同时访问和修改共享变量,此时使用原子操作可以避免竞态条件(Race
Condition)的发生。
三、互斥量接口
初始化互斥量
静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
这个是全局的,不需要释放,程序结束自动释放
动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:mutex是要初始化的互斥量,attr是NULL。
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁⼀个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量加锁与解锁
加锁
int n=pthread_mutex_lock(&_mutex);
解锁
int n=pthread_mutex_unlock(&_mutex);
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
发起函数调时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
对临界区进行保护,本质就是用锁对临界区的代码进行保护。
四、对互斥量底层进行理解
为了实现互斥操作,大多数体系结构都提供了swap和exchange指令,该指令的作用是把寄存器和内存单元的数据相互交换。
CPU的寄存器硬件只有一套,但是CPU寄存器内的数据可以有多份,这些多份的数据就是线程或者进程的上下文数据。把一个变量的内容交到CPU寄存器的内部,就是把该变量的内容,获取到当前执行流的硬件上下文中。我们用swap和exchange将内存与寄存器数据进行交换,就是获取锁的过程就是把当前线程的上下文数据进行交换,所以谁申请到谁持有锁。简单理解的话是,上下文数据是每个线程/进程所特有的不共享的,把硬件理解为一个容器放着锁的地方,且这个容器是不回变的,变的是里面线程/进程的上下文数据,所以进来的线程/进程的上下文数据申请到锁交换走了就持有了锁。CPU 是一个“硬件容器”,每次执行线程就像“把自己的上下文装进容器”。而锁的本质是某段共享数据的访问控制权。谁抢到了这份共享数据的使用权(通常是一个“锁变量”成功从 0 → 1 的原子修改),谁就持有锁。
实际上,锁的竞争与上下文本身没有直接交换关系。锁操作是通过某些汇编指令实现的原子操作(如 lock xchg),是线程在“当前占有 CPU 执行权”时尝试修改锁变量。
当有一个线程a,在寄存器清零(注意我们上面的聊的独立的上下文数据,这里清零只是线程a上下文在寄存器这个“容器”清零了),然后与内存单元的数据1就是锁,进行交换。这时候线程a持有了锁。那么这时候把线程a切走挂起,线程b进来清零然后与内存单元交换也不会持有锁。这里也从侧面说明了原子性。当释放锁的时候,只需要内存单元mutex数据置1就可以了。上述是形象化理解。
原子锁操作的关键在于:线程通过原子指令在“自己的上下文”中进行“对内存中锁变量”的原子修改,谁成功交换就谁持有锁。锁的状态独立于线程调度、寄存器内容,完全由共享内存中的那一份锁变量决定。
正统理解:
五、互斥锁的封装
#include <iostream>
#include <pthread.h>using namespace std;namespace MutexModule
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr); // 初始化互斥锁}void Lock(){int n = pthread_mutex_lock(&_mutex); // 上锁if (n == 0){// cout<<"上锁成功"<<endl;}}void Unlock(){int n = pthread_mutex_unlock(&_mutex); // 解锁if (n == 0){// cout<<"解锁成功"<<endl;}}~Mutex(){pthread_mutex_destroy(&_mutex); // 销毁互斥锁}pthread_mutex_t *Get()//外部调用拿到{return &_mutex;}private:pthread_mutex_t _mutex; // 定义互斥锁};class LockGuard // RAII自动上锁解锁{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;};}
初始化锁,销毁锁,上锁,解锁,函数往上招呼就行了。加个自动上锁,直接调这个LockGuard在临界区前调用,不用手动写释放了。