线程互斥量
线程互斥
进程线程间的互斥相关背景概念
• 共享资源
• 临界资源:多线程执⾏流被保护的共享的资源就叫做临界资源
• 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
• 互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤
• 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成,一般来说就是该代码对应的只有一条汇编语句,可以这么通俗的理解
互斥的作用
• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完
成线程之间的交互。
• 多个线程并发的操作共享变量,会带来⼀些问题。
以下是抢票系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else{break;}}
}
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);
}
当你经行运行时会发现票数减到负数

多线程抢票为什么会减到负数
首先我们得先理解上面tichket–并非是原子的,它可以转为一下三条汇编语句
1. 0xFF00 载入 ebx ticket
2. 0xFF02 减少 ebx 1
3. 0xFF04 写回 0x1111 ebx
假设线程1已经循环执行while里面的代码块,将ticket减少到了等于3,这时他执行ticket–;只执行到了汇编的第二条语句,被中断线程切换,他就会保存上下文,带走cpu里面自己的数据,线程二来了这时可以看到内存里面ticket的值为3,他将ticket–;语句循环执行完了变成了0,这时他的线程任务执行完毕切换线程1,这时恢复他的上下文可以看到他写回内存的值会变成2,也就是说ticket从0变成了2,这时我们便可以理解tichket–并非是原子的,但这并不是ticket变成负数的罪魁祸首
这时我们再看向ticket > 0;语句他会执行两个汇编语句1.将ticket的值从内存载入cpu寄存器 2.判断cpu寄存器值,也就是说他也是非原子的,比如当ticket等于1这时当有大量的线程在这ticket > 0一步被调度离开(就执行了1个或执行完了这个语句),自然在ticket–会被重复减到负数。
面对这种问题我们就需要引入互斥量这一概念来解决该问题
互斥量的使用
//静态分配
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);
//互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
销毁的注意事项:
• 使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁,程序结束会自动释放
• 不要销毁⼀个已经加锁的互斥量
• 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
调⽤ pthread_mutex_ lock 时,可能会遇到以下情况:
• 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
• 发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。
改进上⾯的售票系统:
一种临界资源,由多个线程访问,如果想要保证临界资源的安全,就必须让这个多个线程访问同一把锁!!!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&mutex);if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}}return nullptr;
}
int main(void)
{pthread_t t1, t2, t3, t4;pthread_mutex_init(&mutex, NULL);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);pthread_mutex_destroy(&mutex);
}
其中,被锁保护的资源叫做临界资源,某几段访问临界资源的代码区叫做临界区
加锁的本质:是用时间来换安全
加锁的表现:线程对临界区代码的串行执行
加锁原则:尽量保证临界区代码越少越好。
对临界资源保护:本质就是用锁对临界区代码进行保护
申请锁成功了,才能往后执行,不成功,就会阻塞等待(等待锁资源释放)
互斥量的原理

首先互斥量必须是原子的,由于他本身也是临界资源
那么在临界区中,线程可以被切换吗?
可以切换!!!在线程被切出去的时候,是持有锁被切走的。该线程即使被切换走了,照样没有任何线程能进入资源临界区访问临界资源!
互斥量的实现
硬件上是可以靠关闭中断,来不响应其他线程的要求,直到走出临界区,但同时也容易死机当走不出临界区时。还有其他方法在这里就不介绍了
软件上为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange指令,该指令的作⽤是把寄存器和内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,即使是多处理器平台,访问内存的总线周期也有先后,⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。现在我们把lock和unlock的伪代码改⼀下
lock:
movb $0, %al
xchgb %al, mutex
if(al寄存器的内容 > 0){return 0;
} else挂起等待;
goto lock;
unlock:
movb $1, mutex
唤醒等待Mutex的线程;
return 0;
lock
-
movb $0, %al:将寄存器al的值设为 0(准备用于交换)。 -
xchgb %al, mutex:原子交换指令(关键!),交换寄存器al和变量mutex的值。
这一步是原子的(不可被中断),保证多线程并发时不会出现 “同时修改锁状态” 的冲突。 -
if(al寄存器的内容 > 0):判断交换后al的值(即交换前mutex的原始值):- 若
al > 0:说明交换前mutex是 1(“未被占用” 状态),当前线程成功获取锁(此时mutex已被交换为 0,表示 “已占用”),直接返回(进入临界区)。 - 若
al == 0:说明交换前mutex是 0(“已被其他线程占用”),当前线程获取锁失败,进入 “挂起等待” 状态(加入等待队列)。
- 若
-
goto lock:挂起的线程被唤醒后,会重新执行lock流程,再次尝试获取锁。
unlock
movb $1, mutex:将mutex设为 1(“未被占用” 状态),表示锁已释放。唤醒等待Mutex的线程:从等待队列中唤醒一个(或多个)挂起的线程,让它们重新尝试获取锁。return 0:释放锁完成。
重点就是xchgb %al, mutex,他是交换的并非拷贝,哪怕当一个线程执行完该语句后被调度换了其他的线程这时,1只会跟着他走,由于其他线程进入时mutex的值已经变为了0,而且al值也为 0,这时交换的就是0和0交换。不影响后面的判断
可以借助此图理解

对互斥量进行封装
#pragma once
#include <iostream>
#include <pthread.h>namespace MutexModule
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}~Mutex(){pthread_mutex_destroy(&_mutex);}private:pthread_mutex_t _mutex;};class LockGuard{public:LockGuard(Mutex &mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;};
}
RAII设计思想,封装锁析构,脱离生命周期自动析构
不同线程对于锁的竞争能力可能会不同,在纯互斥环境中,如果锁分配不够合理,容易导致其他线程的饥饿问题----->不是说只要有互斥就必有饥饿,适合纯互斥场景就用互斥。
我们下个文章将会由此讲到线程同步问题!!!
