文章目录
- 1. 进程线程间的互斥相关背景概念
- 2. 互斥量mutex
- 3. 互斥量的接口
- 初始化互斥量
- 销毁互斥量
- 互斥量加锁和解锁
- 4. 互斥量---锁
- 静态分配(初始化)
- 动态分配(初始化)
- 5. 互斥量实现原理探究
- 6. 总结:
1. 进程线程间的互斥相关背景概念
- 临界资源:多线程执行流共享的资源就叫做临界资源。
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
2. 互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来一些问题。
例子:操作共享变量会有问题的售票系统代码
- 代码块
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
using namespace std;
//买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!
void *getTickets(void *args)
{
(void*)args;
// pthread_self() 获取该线程的id
while(true)
{
if(tickets>0)
{
usleep(1000);
printf("%p: %d\n", pthread_self(), tickets);
tickets--;
}
else
{
break;
}
}
}
int main()
{
// 创建一个线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, nullptr, getTickets, nullptr);
pthread_create(&tid2, nullptr, getTickets, nullptr);
pthread_create(&tid3, nullptr, getTickets, nullptr);
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
return 0;
}
我们理想下:最后票数为0。
-
运行结果
结果竟然是-1。 -
为什么可能无法获得争取结果?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程。
- usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
- - -ticket 操作本身就不是一个原子操作。
-
- 读取数据到cpu内的寄存器中
- 2.CPU内部进行计算**- -**
- 3.将结果写回内存中
- 在这其中很容易进行到一半就挂起;让其它线程来再执行。
-
—(图片摘自相关教材)
- – 操作并不是原子操作,而是对应三条汇编指令:
- load :将共享变量ticket从内存加载到寄存器中。
- update : 更新寄存器里面的值,执行-1操作。
- store :将新值,从寄存器写回共享变量ticket的内存地址。
- 要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
- 要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
3. 互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
- 方法1,静态分配:
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER
- 方法2,动态分配:
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 pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
4. 互斥量—锁
改进上面的售票系统:
静态分配(初始化)
- 代码块:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cassert>
using namespace std;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配
// 买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!
void *getTickets(void *args)
{
(void *)args;
// pthread_self() 获取该线程的id
while (true)
{
int n = pthread_mutex_lock(&mutex); // 上锁
assert(n == 0);
if (tickets > 0)
{
usleep(1000);
printf("%p: %d\n", pthread_self(), tickets);
tickets--;
pthread_mutex_unlock(&mutex); // 解锁
assert(n == 0);
}
else
{
pthread_mutex_unlock(&mutex); // 解锁
assert(n == 0);
break;
}
// 抢完票,其实还需要后续的动作
usleep(rand() % 200);
}
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid() ^ 2023);
// 创建一个线程
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, nullptr, getTickets, nullptr);
pthread_create(&tid2, nullptr, getTickets, nullptr);
pthread_create(&tid3, nullptr, getTickets, nullptr);
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_mutex_destroy(&mutex); // 销毁锁
return 0;
}
- 结果达到理想状态:
动态分配(初始化)
- 代码块:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cassert>
using namespace std;
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态分配
// 买票逻辑
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!
#define THREAD_NUM 5
class ThreadData
{
public:
ThreadData(const string &n, pthread_mutex_t *pm) : tname(n), pmtx(pm)
{
}
string tname;
pthread_mutex_t *pmtx;
};
void *getTickets(void *args)
{
// pthread_self() 获取该线程的id
ThreadData *mutex = (ThreadData *)args;
while (true)
{
int n = pthread_mutex_lock(mutex->pmtx); // 上锁
assert(n == 0);
if (tickets > 0)
{
usleep(1000);
printf("%s: %d\n", mutex->tname.c_str(), tickets);
tickets--;
pthread_mutex_unlock(mutex->pmtx); // 解锁
assert(n == 0);
}
else
{
pthread_mutex_unlock(mutex->pmtx); // 解锁
assert(n == 0);
break;
}
// 抢完票,其实还需要后续的动作
usleep(rand() % 200);
}
delete mutex;
return nullptr;
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid() ^ 2023);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
pthread_t t[THREAD_NUM];
// 创建线程
for (int i = 0; i < THREAD_NUM; ++i)
{
string name = "thread ";
name += to_string(i + 1);
ThreadData *td = new ThreadData(name, &mutex);
pthread_create(t + i, nullptr, getTickets, td);
}
// 等待线程
for (int i = 0; i < THREAD_NUM; ++i)
{
pthread_join(t[i], nullptr);
}
pthread_mutex_destroy(&mutex); // 销毁锁
return 0;
}
- 结果展示:
5. 互斥量实现原理探究
- 经过上面的例子我们已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。
- 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。
交换的现象: 内存<->%al 做交换
交换的本质: 共享<->私有(线程自己的上下文)
即保证了锁里面‘1’ 的唯一性。
6. 总结:
-
如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响;避免方法:
- 加锁保护:加锁的时候,一定要保证加锁的粒度,越小越好!!
-
加锁就是串行执行。
- 执行临界区代码一定是串行的!
-
加锁了之后,线程在临界区中,会切换,没有时序问题。
- 原子性的体现
- 虽然被切换了,但是你是持有锁被切换的, 所以其他抢票线程要执行临界区代码,也必须先申请锁,锁它是无法申请成功的,所以,也不会让其他线程进入临界区,就保证了临界区中数据一致性!
-
我是一个线程,我不申请锁,就是单纯的访问临界资源! 这是一种错误的编码方式。
-
在没有持有锁的线程看来,对它最有意义的情况只有两种:
-
- 线程1没有持有锁(什么都没做)
-
- 线程1释放锁(做完),此时我可以申请锁!
-
-
要访问临界资源,每一个线程都必须现申请锁,每一个线程都必须先看到同一把锁并且可以访问它,即锁本身就是一种共享资源。那么谁来保证锁的安全呢??为了保证锁的安全,申请和释放锁,必须是原子滴
-
锁自身保证的(上面的伪代码)—设计者这样设计滴
-
在执行流视角,是如何看待CPU上面的寄存器的?
- CPU内部的寄存器本质,叫做当前执行流的上下文! !寄存器们,空间是被所有的执行流共享的,但是寄存器的内容,是被每一个执行流私有的! 上下文!