目录
一.Linux线程互斥
1.进程线程间的互斥相关背景概念
- 临界资源: 多线程执行流共享的资源叫做临界资源(全局变量count)。
- 临界区: 每个线程内部,访问临界资源的代码,就叫做临界区。(对count的++,--操作的代码)
- 互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
- 原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
(1)临界资源和临界区
①代码,主线程和新线程都对count++操作
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int count = 0; //全局
void* Routine(void* arg)
{
while (1){
count++;
sleep(1);
}
pthread_exit((void*)0);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, Routine, NULL);
while (1){
printf("count: %d\n", count);
sleep(1);
}
pthread_join(tid, NULL);
return 0;
}
②结果: 全局变量count就叫做临界资源,因为它被多个执行流共享,线程执行函数中printf和count++就叫做临界区,因为这些代码对临界资源进行了访问。
(2)互斥和原子性
多线程情况下,如果多个执行流都对同一份临界资源进行访问可能导致数据不一致的问题。解决该问题的方案就叫做互斥,互斥的作用就是,保证在任何时候有且只有一个执行流进入临界区对临界资源进行访问。
①代码: 模拟抢票系统
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int tickets = 1000;
void* routine(void* arg) //抢票
{
const char* name = (char*)arg;
while (1){
if (tickets > 0){
usleep(1000);
printf("[%s] get a ticket, left: %d\n", name, --tickets);
}
else{
break;
}
}
printf("%s quit!\n", name);
pthread_exit((void*)0);
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, routine, (void*)"thread 1");
pthread_create(&t2, NULL, routine, (void*)"thread 2");
pthread_create(&t3, NULL, routine, (void*)"thread 3");
pthread_create(&t4, NULL, routine, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
②结果:没有对临界资源进行保护,出现了负数
③出现负数原因分析
- if语句判断条件为真以后(判断也需要CPU参与),代码可以并发的切换到其他线程," 同一时刻" 有多个线程判断tickets时tickets的值是相同的。
- usleep用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
- --tickets作本身就不是一个原子操作。
--tickets实际的操作过程
1.对一个变量进行++,--操作需要三个步骤
- load: tickets变量本身是在内存中的,需要加载到寄存器中
- update: CPU对寄存器的值进行-1
- store: 将寄存器的值写回内存
2.汇编代码
3.--tickets的过程中有三条汇编指令,其中在执行任何一条指令时都有可能被切走
2.互斥量mutex引出
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量成为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,就会带来一些问题。
解决上述抢票系统的问题,需要做到三点:
- 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

3.互斥量接口
(1) 初始化
函数:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数说明:
- mutex:需要初始化的互斥量。
- attr:初始化互斥量的属性,一般设置为NULL即可。
返回值:
- 互斥量初始化成功返回0,失败返回错误码。
调用pthread_mutex_init函数初始化互斥量叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥量,该方式叫做静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
(2)销毁
函数 : int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:mutex:需要销毁的互斥量。返回值: 互斥量销毁成功返回0,失败返回错误码。
销毁互斥量需要注意:
- 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁。
- 不要销毁一个已经加锁的互斥量。
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
(3)加锁
函数:int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:mutex:需要加锁的互斥量。
返回值:互斥量加锁成功返回0,失败返回错误码。调用pthread_mutex_lock时,可能会遇到以下情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
(4)解锁
函数: int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数: