目录
[一 进程间互斥的概念及相关背景](#一 进程间互斥的概念及相关背景)
[1.2 互斥量](#1.2 互斥量)
[1.2.1 抢票案例](#1.2.1 抢票案例)
[1.2.3 从两个方面解析出现互斥(数据不一致问题)](#1.2.3 从两个方面解析出现互斥(数据不一致问题))
[1.2.3.1 ticket--(非主要矛盾,但也有关)](#1.2.3.1 ticket--(非主要矛盾,但也有关))
[1.2.3.2 ticket>0](#1.2.3.2 ticket>0)
[1.2.3.3 局部互斥量及其封装锁 利用RALL思想](#1.2.3.3 局部互斥量及其封装锁 利用RALL思想)
[1.2.4 线程切换时间点](#1.2.4 线程切换时间点)
[2.1 硬件级实现](#2.1 硬件级实现)
[2.2 软件级实现](#2.2 软件级实现)
在前面章节的学习中,我们对于一个对于多线程共享的资源,加上了bug的注释,本章我们将介绍为什么是bug及如何解决
一 进程间互斥的概念及相关背景
共享资源
临界资源:多线程执⾏流被保护的共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有⼀个执⾏流进⼊临界区,访问临界资源,通常对临界资源起
保护作⽤
原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,
要么未完成
1.2 互斥量
⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量
归属单个线程,其他线程⽆法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完
成线程之间的交互。
多个线程并发的操作共享变量,会带来⼀些问题。
1.2.1 抢票案例
想看看没有引入互斥,在线程并发出现的问题
bash#include <iostream> #include <unistd.h> #include <pthread.h> using namespace std; int ticket=100; void * routine(void *args) { string name=static_cast<char*>(args); while(true) { if(ticket>0) { usleep(1000); cout<<name<<"抢->"<<ticket<<endl; --ticket; } else break; } return nullptr; } int main() { pthread_t t1, t2, t3, t4, t5; pthread_create(&t1, nullptr, routine, (void*)"thread-1"); pthread_create(&t2, nullptr, routine, (void*)"thread-2"); pthread_create(&t3, nullptr, routine, (void*)"thread-3"); pthread_create(&t4, nullptr, routine, (void*)"thread-4"); pthread_create(&t5, nullptr, routine, (void*)"thread-5"); pthread_join(t1,NULL); pthread_join(t2,NULL); pthread_join(t3,NULL); pthread_join(t4,NULL); pthread_join(t5,NULL); return 0; }发现抢票结果出现了负数
1.2.2引入互斥量(运行速度变慢了)
bash
man 3 pthread_mutex_lock # 查看加锁函数的手册
man 3 pthread_mutex_unlock # 查看解锁函数的手册
man 3 pthread_mutex_init # 查看初始化函数的手册
结果正常
加锁后代码及结果
bash
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *routine(void *args)
{
string name = static_cast<char *>(args);
while (true)
{
pthread_mutex_lock(&lock);
if (ticket > 0)
{
usleep(1000);
cout << name << "抢->" << ticket << endl;
--ticket;
pthread_mutex_unlock(&lock);
}
else
{
pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3, t4, t5;
pthread_create(&t1, nullptr, routine, (void *)"thread-1");
pthread_create(&t2, nullptr, routine, (void *)"thread-2");
pthread_create(&t3, nullptr, routine, (void *)"thread-3");
pthread_create(&t4, nullptr, routine, (void *)"thread-4");
pthread_create(&t5, nullptr, routine, (void *)"thread-5");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_join(t5, NULL);
return 0;
}

发现加锁后结果正常
1.2.3 从两个方面解析出现互斥(数据不一致问题)
1.2.3.1 ticket--(非主要矛盾,但也有关)
c/c++的ticket--是一条语句,但这并非是原子操作,可以被打断
汇编语句是原子操作,要么执行,要么不执行
将ticket--变成汇编语言,有三句
0XFF00(载入 从内存中将ticket载入到ebx(随便给的一个寄存器))
0XFF02(减少 ebx-1)
0XFF04(写入 将ticket写回到内存) (注意:指令也是有长度的,所以上面的地址非连续)
假设只有线程a 线程b 其中ticket为1000 还有一个pc指针指向要执行的命令
先切换到线程a,该过程中,将ticket读取到ebx,再进行-1,后,当pc指向OXFF04时,OS进行了调度,切换进程,那么此时的寄存器就要对a进行上下文保存,将ebx内的值和pc指针进行保存
然后再切换到线程b中,我们假设b被分配的时间片更多些,它将ticket读取到ebx,进行-1,再写回内存,重复直到恰好写回内存时,ticket为1了
此时再进行线程切换,切换为线程a,那么就会读取寄存器存放的线程a的上下文,发现pc的意思是将ticket=999写回内存中,此时就会出现,线程a这一操作,直接将线程b前面所有的努力都浪费了
1.2.3.2 ticket>0
CPU会进行计算,为逻辑计算和算数计算,上面的为算数计算,此处的ticket>0为逻辑计算
ticket>0也并非是原子操作,但此处我们直接假设它是,方便下面讲解
假设有n个线程,当第一个线程判断完ticket>0后,时间片到了,就会发生线程切换,寄存器保存第一个线程的上下文,第2个我们也是如此,直到第n个,
当n个线程都完成了线程切换,又回到了第一个线程,那么它从寄存器取回上下文数据,执行判断完之后的代码,进行--,然后时间到了,线程切换到第二个,也是如此,直到第n个,就会出现ticket减到为负数的情况
我们上面在判断大于0后,进行休眠,就是为了然时间片为0,触发线程切换,就是为了出现该现象
1.2.3.3 局部互斥量及其封装锁 利用RALL思想
bash
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
bash
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
bash
#include "mutex.hpp"
#include <unistd.h>
using namespace milestone;
int ticket = 100;
class ThreadData
{
public:
ThreadData(const string &s, Mutex &locka)
: name(s), locks(&locka)
{
}
~ThreadData()
{
}
Mutex *locks;
string name;
};
void *routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
Lockguard(*td->locks);//利用RALL思想
if (ticket > 0)
{
usleep(1000);
cout << "抢->" << ticket << endl;
--ticket;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3, t4, t5;
Mutex lock;
ThreadData *td1 = new ThreadData("thread 1", lock);
pthread_create(&t1, NULL, routine, td1);
ThreadData *td2 = new ThreadData("thread 2", lock);
pthread_create(&t3, NULL, routine, td2);
ThreadData *td3 = new ThreadData("thread 3", lock);
pthread_create(&t3, NULL, routine, td3);
ThreadData *td4 = new ThreadData("thread 4", lock);
pthread_create(&t4, NULL, routine, td4);
ThreadData *td5 = new ThreadData("thread 5", lock);
pthread_create(&t5, NULL, routine, td5);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_join(t5, NULL);
return 0;
}
mutex.hpp
bash
#include <iostream>
#include <pthread.h>
using namespace std;
namespace milestone
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
void lock()
{
int n = pthread_mutex_lock(&_mutex);
}
void unlock()
{
int n = pthread_mutex_unlock(&_mutex);
}
~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;
};
}
1.2.4 线程切换时间点
- 时间片为0
2.阻塞是IO
3.sleep
上面操作都会让OS陷入内核
所有选择新的进程,是从内核态返回用户态时,进行检查,是否需要切换
二.理解锁:理解锁为什么是原子的
2.1 硬件级实现
我们前面说过计算机内部有个时钟,硬件实现,即关闭时间中断即可,那么就不会出现切换
2.2 软件级实现
为了实现互斥操作,大多体系结构都提供了swap/exchange指令(只有一条指令,原子性),作用是把寄存器的数据和内存单元的数据进行交换


理解: mutex初始化会被设置为1
1.假设线程a运行,先lock movb $0 %al 此操作是将自己寄存器的值设为0(无论原来为何值),
2.接着进程xchgb交换,于mutex的内存单元的值进行交换(注意,是交换,不是拷贝)(可以确保只有1这个1把锁,所有1,谁就进行原子操作)
- 接着进行判断,如果线程寄存器的内容大于0,就进行返回(加锁成功),结束当前函数,执行临界区
4.执行完后,进行解锁,将当前线程寄存器的值于mutex进行交换,还锁,然后再唤醒之前在等锁的进程
5.在线程a加锁后,如果线程进行切换了,线程b再进行初始化为0,于mutex进行交换后,仍为0,就进行挂起等待了,线程c d e也是如此
6.直到线程a将锁还回去后,再进行唤醒线程b,进行加锁
🔥个人主页: Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
