linux线程同步与互斥
1. 线程互斥相关概念
2. 线程互斥
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0) // 1. 判断{usleep(1000); // 1. 模拟抢票花的时间printf("%s sells ticket:%d\n", id, ticket); // 2. 抢到了票ticket--; // 3. 票数--}else{break;}}return nullptr;
}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);
}
为什么会减到负数?
首先,ticket--操作不是原子的。
对于ticket--的原子性,要么--,要么不做--,不会存在中间状态,而CPU执行ticket--的汇编语言有三条语句,这三条汇编之间,任意一个位置线程都可能被中断切换,因此ticket--不是原子的。
0xFF00 mov ebx ticket //将ticket的值传给寄存器
0xFF02 减少 ebx 1 //执行减一操作
0xFF04 写回 0x1111 ebx //将ebx计算后的值写回内存
当一个线程执行ticket--时,需要执行上面三步,可能当该线程执行完第二步,在执行第三步之前,发生了线程切换,这个线程就会保存自己的上下文然后被放到系统的等待队列中了,如果没被其他线程打扰则正常完成ticket--操作。
假如一个线程A
//执行完第二步
ebx:99
PC指针:oxff04
还有一个线程B运气比较好一直执行而没有被打断,直到执行到ticket = 1;
ebx:1
pc指针: 0xff02
然后发生线程切换,CPU开始执行线程A的内容,而线程A中保存的ticket=99,执行完第三步汇编后,将此时’99‘保存到内存中ticket对应的位置,这就导致线程B的努力全都白费了!!这就导致了数据不一致问题。
票数减到负数虽然与此有关,但主要矛盾其实是判断语句ticket>0。
假如现在ticket=1,此时 线程1进入判断语句并通过,在usleep的时间里,有其他线程经过判断进入,然后被切走,之后线程2、线程3、线程4也是如此,然后执行线程1,ticket=0,线程2、3、4依次使ticket--,因此票数减到了负数!
从上面的例子中可以看出,全局资源没有加保护时,可能会有并发问题。
route函数因为有临界资源,所以是不可重入函数。
线程切换与切回
什么时候会发生线程切换呢?1. 时间片到了 2.阻塞式IO 3.挂起或休眠的函数什么时候选择新的线程?从内核态返回用户态时,进行检查,条件不满足则选择新的线程。
简单的说,只有一条汇编语句是原子性的。
那么怎么解决票数减为负数的问题呢?这就是线程锁的用处
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&lock);if (ticket > 0) // 1. 判断{usleep(1000); // 1. 模拟抢票花的时间printf("%s sells ticket:%d\n", id, ticket); // 2. 抢到了票ticket--; // 3. 票数--pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}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);
}
3. 互斥锁/互斥量mutex
3.1 pthread_mutex_init/destroy
两种申请锁的方式,局部锁需要初始化和手动销毁,而全局锁不需要被手动释放,程序运行结束会自动释放。
#include <pthread.h>int pthread_mutex_init(pthread_mutex_t *restrict mutex, // 指向要初始化的互斥锁const pthread_mutexattr_t *restrict attr // 属性参数(通常为NULL)
);// 静态初始化方式,全局
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
成功:返回 0。失败:返回错误码(非零值),例如:EAGAIN:系统资源不足。ENOMEM:内存不足。EPERM:权限不足(如进程无法设置优先级继承属性)
int pthread_mutex_destroy(pthread_mutex_t *mutex);返回值:
成功:返回 0。
失败:返回错误码(如 EBUSY 表示锁正在被占用)。
3.2 pthread_mutex_lock
函数 行为
pthread_mutex_lock 阻塞直到获得锁。
pthread_mutex_trylock 非阻塞,立即返回:成功获得锁返回 0,锁被占用返回 EBUSY。
pthread_mutex_unlock 释放锁,允许其他线程竞争。返回值:成功:返回 0。失败:返回错误码(如 EINVAL 表示锁未初始化,EDEADLK 表示死锁)。
无论是局部锁还是全局锁,申请锁前都需要先加锁,就是将申请的锁传递给pthread_mutex_lock加锁。
(1)关于加锁:
1.所有线程都需要竞争申请锁,多线程都得先看到锁,锁本身就是临界资源,因此申请锁的过程必须是原子的。
2.申请锁成功则继续向后运行、访问临界区代码、访问临界资源,失败则阻塞挂起申请执行流。
(2)两个本质:
锁提供的功能的本质:将执行临界区代码由并行转为串行。(这种在执行期间不会被打扰,也是原子性的一种表现)
对临界区资源的保护本质上就是用锁对临界区的代码进行保护。
(3)加锁之后的线程切换
加锁之后,在临界区内部也是允许线程切换的。但是当前线程是持有锁被动切换的,即使该线程不在,其他线程也得等到该线程回来执行完代码,释放锁后,才能展开所得竞争,进入临界区。
4. 锁的原理
硬件级实现:时钟中断
软件级实现:
为了实现互斥锁的操作,大多数体系结构都提供了swap和exchange指令,该指令的作用是把寄存器和内存单元的数据交换。(这两个指令只有一条汇编指令,保证了原子性)
锁其实就是一种标记位,可以把锁mutex当成一个整数,假如是1,表示该锁没有被线程申请。
上图中的%al是一个寄存器,在此代码中作为 临时变量,用于存储 mutex
的当前值,并判断锁的状态:0
:锁被占用(其他线程持有)。1
:锁空闲(可获取)。
lock与unlock
(1)lock:将0存入%al中,然后和锁的值进行交换,加入交换前锁空闲,则交换后%al的值为1,执行return 0, 否则挂起等待。
(2)将锁的值置为1,唤醒正在挂起等待的线程。
我们用swap、exchange将内存中的变量交换到CPU的寄存器中,本质上是当前进程/线程在获取锁,注意,是交换而不是拷贝!!!所以1只有1份,谁申请到,谁就持有锁。
包含线程切换的lock流程
线程切换时线程会将上下文内容保存起来,包括执行到哪段代码、该线程的%al里的内容等。
假如有线程A、B、C
线程A执行lock,执行完第一条语句将%al的值置为0(这是线程A的私有上下文内容),然后切换。此时线程A的%al为0, 内存中的mutex为1。
然后线程B执行lock,执行第一句将%al的值清0(清理的都是自己的上下文数据),执行完第二条语句后切换。此时线程B的%al为1, 内存中的mutex为0。
线程C执行lock,执行第一句将%al置为0,由于mutex为0,所以执行完第二条语句后%al仍然为0,通过条件判断进程C被阻塞挂起。此时线程C的%al为0, 内存中的mutex为0。
然后切换到线程A,线程A阻塞挂起。切换到线程B,线程Breturn,线程B申请锁成功。
从上面的例子中可以看出,锁是原子性的,无论有没有线程切换、什么时候切换,都不影响锁的申请。
5.线程安与重入问题
(1)定义
线程安全 (Thread Safety)
定义:多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。
可重入性 (Reentrancy)
定义:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
(2)线程安全与可重入性常见情况
线程不安全的情况
1. 不保护共享变量的函数
2. 函数状态随着被调用而发生变化的函数
3. 返回指向静态变量指针的函数
4. 调用线程不安全函数的函数
不可重入的情况
1. 调用了malloc/free函数(使用全局链表管理堆)
2. 调用了标准I/O库函数(使用全局数据结构)
3. 函数体内使用了静态的数据结构
线程安全的情况
1. 线程对全局变量/静态变量只有读取权限
2. 类或接口对线程是原子操作
3. 线程切换不会导致接口执行结果出现二义性
可重入的情况
1. 不使用全局变量或静态变量
2. 不使用malloc/new开辟空间
3. 不调用不可重入函数
4. 不返回静态/全局数据(所有数据由调用者提供)
5. 使用本地数据或全局数据的本地副本
(4)可重入与线程安全区别
可重入函数是线程安全函数的⼀种
6. 死锁

如果要避免死锁,就要破坏死锁的四个必要条件、破坏循环等待条件问题。例如资源⼀次性分配, 使用超时机制、加锁顺序⼀致。