目录
[一 线程互斥](#一 线程互斥)
[1 进程线程间的互斥相关背景概念](#1 进程线程间的互斥相关背景概念)
[2 互斥量mutex](#2 互斥量mutex)
[3 两个问题:](#3 两个问题:)
[4 加锁](#4 加锁)
[5 mutex的封装](#5 mutex的封装)
一 线程互斥
1 进程线程间的互斥相关背景概念
共享资源
临界资源:多线程执行流中需要被保护的共享资源 ,就叫做临界资源
临界区:每个线程内部,访问临界资源的代码区域 ,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
多线程在访问共享资源时容易出现问题。线程之间会共享文件描述符表和已打开的文件结构体,多个线程实际上指向同一份文件表项。主线程默认会打开终端对应的标准输出文件描述符 1;多线程向终端输出内容,本质上就是多个线程同时对同一个文件进行写入操作。因此,终端显示器对应的文件属于多线程间的共享资源,并发访问时需要注意同步问题。
所以共享资源会导致数据不一致问题
未来要把共享资源保护起来的方法:同步,互斥
想保护临界资源必须先知道是怎么访问临界资源的。保护临界资源就是保护访问临界资源的代码----->叫做临界区
2 互斥量mutex
多个线程并发的操作共享共享变量。会带来一些问题
我们来写一个多线程抢票的代码--->模拟多线程访问并发资源,来看一下会带来哪些问题
cpp
// 操作共享变量会有问题的售票系统代码
#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);
}
我们运行代码之后发现,票数会被抢到负数,会导致数据不一致--->线程安全中的一种
cpp
一次执⾏结果:
thread 4 sells ticket:100
...
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2
3 两个问题:
(1)为什么会抢到负数?
if判断本身就是一种逻辑运算,能处理逻辑运算的典型硬件是CPU
if判断要做a.变量导入到CPU的eax寄存器中,(1000和0),CPU内存在算逻运算单元
b.CPU比较1000和0,后输出为真为假,但是这个过程是以线程为载体的。所以这是一个线程调度执行的过程
当只剩一张票的时候,假设有A B C D 四个线程,当A把1加载到eax中时,A被切走。此时,A会保存eax=1,下一步要和0作比较;所以A刚被切走,B就来了,B把1加载到eax中时,B被切走......(重复上述过程)。当A再次被调度过来,对tickets做--,同样的,B C D也会做--操作;这个时候tickets就会把多的票放进来
额外话题:多线程--的话题,对全局变量--的话题:
tickets--在C中是一条语句,但是在汇编中是三条语句,对应三个动作
tickets--是一个线程调度执行的过程
因为tickets--对应三个动作:操作前,操作中,操作后;在操作的时候是可能被中断的--->所以不是原子的
结论:整型变量,不具有原子性
如果汇编条数只有一条,就是原子的;否则不是原子的
所以信号量(原子的) != 全局计数器(非原子的)
(2)如何解决这个问题
让多线程进行互斥访问
任何时刻只允许一个线程,且是原子性的访问临界区
因为有互斥的存在,所以不会存在竞争的问题
通过加锁来保护临界区

4 加锁
(1)操作
mutex--->互斥量,也是我们所说的锁
**变量类型:pthread_mutex_t:**表示互斥锁或者互斥量
定义一把锁:pthreead_mutex_t glock=PTHREAD_MUTEX_INITALIZER(全局锁);
保护临界区,一定要把被保护的区域,范围缩到最小!!! 被保护的代码量尽量少,不要保护非临界区代码,否则会造成串行执行
互斥量加锁和解锁
cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁
返回值:成功返回0,失败返回错误号
如果加锁失败,线程就会阻塞;加锁成功就会立即返回-->允许访问后续代码,访问临界区
那我们来给这个抢票代码加一下锁:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
#include <string>
int ticket = 1000;
typedef struct threadData
{
std::string name;
pthread_mutex_t *plock;
}threaddata_t;
void *route(void *arg)
{
threaddata_t *td = static_cast<threaddata_t *>(arg);
while ( 1 ) {
// pthread_mutex_lock(td->plock);
if ( ticket > 0 ) {
usleep(1000);
printf("%s sells ticket:%d\n", td->name.c_str(), ticket);
ticket--;
// pthread_mutex_unlock(td->plock);
} else {
// pthread_mutex_unlock(td->plock);
break;
}
}
return nullptr;
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_mutex_t lock;
pthread_mutex_init(&lock, nullptr);
threaddata_t data1 = {"thread-1", &lock};
pthread_create(&t1, NULL, route, (void*)&data1);
threaddata_t data2 = {"thread-2", &lock};
pthread_create(&t2, NULL, route, (void*)&data2);
threaddata_t data3 = {"thread-3", &lock};
pthread_create(&t3, NULL, route, (void*)&data3);
threaddata_t data4 = {"thread-4", &lock};
pthread_create(&t4, NULL, route, (void*)&data4);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&lock);
return 0;
}
此时运行之后,票就不会出现抢到负数的情况
加锁会造成一定程度上的效率降低,所以尽可能减少加锁区,减少串行执行,增加并行执行
如何定义局部锁:必须用pthread_mutex_init进行初始化,用pthread_mutex_destory进行销毁
(2)锁的本质
操作上要注意的事情:
要申请锁,必须先看到锁---->锁本身也是共享资源,申请锁的过程,必须是原子的!!
申请锁的本质,是在申请许可
申请锁,可以让线程一部分申请,一部分不申请吗?不可以!把加锁保护当成一种约定,大家都要遵守
锁的原理:
(1)通过硬件实现
我们前面学到的情况,是线程发生切换后导致的(根本原因是外部的时钟中断),你怎么做到让线程不要做任何切换?
关闭时钟中断-->相当于加锁 ,运行一段时间后(这段时间线程执行是没人打扰的,因为是原子的),打开时钟中断---->相当于解锁
关闭中断是有风险的,所以不会给用户使用,而是给内核使用
(2)通过软件实现
为了实现互斥锁操作,大多数体系结构都提供了 swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换。由于只有一条指令,保证了原子性;即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。现在我们把 lock 和 unlock 的伪代码改一下。

swap和exchange是一条原子的汇编语句
内存数据往往被多线程共享,比如全局的,静态的
CPU寄存器只有一套,而CPU内存寄存器的内容本质,是当前线程的硬件上下文
每个线程都有自己独立的硬件上下文
这个指令的作用是吧寄存器和内存器的数据相交换---->本质是把共享的数据内容,变成一个线程私有的内容,意味着其他线程想得到这个内容,是得不到的,除非归还。此时这里的内容就是锁

上面的是函数的底层实现代码
bash
lock:
movb $0, %al ; 把寄存器%al清0
xchgb %al, mutex ; 原子交换:把mutex的值读入%al,同时把mutex设为0(整个过程不可被打断)
if (%al > 0) { ; 判断:如果从mutex读到的值>0(也就是之前mutex是1)
return 0; ; 加锁成功,直接返回
} else { ; 否则(读到的值是0,说明锁已经被别人拿走了)
挂起等待; ; 进入等待状态
}
goto lock; ; 被唤醒后,回到开头重新尝试加锁
unlock:
movb $1, mutex ; 原子性地把mutex的值从0写回1,释放锁
唤醒等待Mutex的线程; ; 通知等待的线程:锁现在可用了
return 0;
在物理内存里面定义一个变量: mutex=1,在CPU中有eax寄存器,值默认为0,eax就等同于%al
xchgb %al,mutex:交换mutex和%al的值,该条语句之内不能被切换,因为是原子的
当线程A执行到lock的第三条语句,时间片到了,被切走,此时A要保存它的硬件上下文-->eax=1,pc=3(表示执行到第三行代码);换到线程B,线程B首先把寄存器清0(此时eax和mutex都为0),交换%al ,mutex;eax=0,线程B被挂起,这个过程叫做竞争锁失败。线程B:eax=0,pc=5。线程A回来,写入自己eax的值,从第三条语句继续执行,eax>0,线程A解锁成功
上面的第二条语句叫做以原子性的方式申请锁
交换体系下,"1"只有一份,谁有"1",谁就能加锁成功--->"1"就是锁
上面是加锁的本质,那解锁呢?原子性的把mutex由0->1
5 mutex的封装
cpp
#ifndef __MUTEX_HPP
#define __MUTEX_HPP
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
// 锁的开关
class LockGuard
{
public:
LockGuard(Mutex *lockp): _lockp(lockp)
{
_lockp->Lock();
}
~LockGuard()
{
_lockp->Unlock();
}
private:
Mutex *_lockp;
};
#endif
LockGuard:RAII 风格自动锁,构造时自动加锁,析构时自动解锁,再也不会忘记解锁。
LockGuard必须和一个已经初始化的锁结合使用
在代码角度看临界区:创建一个临时变量,出了while循环临时变量自动销毁-----RAII风格的加锁逻辑
RAII 的核心就是 :
把资源(锁、文件、内存、连接)交给一个对象管理,利用 C++ 对象离开作用域 自动调用析构函数 的特性,自动释放资源。
你不用手动管,编译器帮你管
