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)unlink 将锁的值置为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,线程 B return,线程B申请锁成功。
从上面的例子中可以看出,锁是原子性的,无论有没有线程切换、什么时候切换,都不影响锁的申请。
5.线程安与重入问题
(1)定义
线程安全 (Thread Safety)
多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。
可重入性 (Reentrancy)
同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
重入可以分为两种情况:1.多线程重入函数 2.信号导致一个执行流重复进入函数
(2)线程安全与可重入性常见情况
线程不安全的情况
1. 不保护共享变量的函数
2. 函数状态随着被调用而发生变化的函数
3. 返回指向静态变量指针的函数
4. 调用线程不安全函数的函数
不可重入的情况
1. 调用了malloc/free函数(使用全局链表管理堆)
2. 调用了标准I/O库函数(使用全局数据结构)
3. 函数体内使用了静态的数据结构
线程安全的情况
1. 线程对全局变量/静态变量只有读取权限
2. 类或接口对线程是原子操作
3. 线程切换不会导致接口执行结果出现二义性
可重入的情况
1. 不使用或只读全局变量或静态变量
2. 不使用malloc/new开辟空间
3. 不调用不可重入函数
4. 不返回静态/全局数据(所有数据由调用者提供)
5. 使用本地数据或全局数据的本地副本
(3)可重入与线程安全联系
函数是可重入的,那就是线程安全的!!!
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数会修改全局变量,那么这个函数既不是线程安全也不是可重入的。
(4)可重入与线程安全区别
可重入函数是线程安全函数的一种。
线程安全不⼀定是可重入的,而可重入函数则⼀定是线程安全的。 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果重入这个函数时若锁还未释放则会产生死锁,因此是不可重入的。
(5)注意
如果不考虑信号导致一个执行流重复进入函数这种重入情况,线程安全和重入在安全角度不做区分
但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点。
可重入描述的是⼀个函数是否能被重复进入,表示的是函数的特点。
6. 死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其它进程所占用不释放的资源而处于的一种永久等待状态。
假如线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问。
而现在线程A、B分别持有一把锁,就造成了两个线程互相申请对方的锁但不释放,这两个锁就成了死锁。

死锁四个必要条件
(1)互斥条件:⼀个资源每次只能被一个执行流使用(一把锁只能被一个线程持有)。
(2)请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:⼀个执行流已获得的资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

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

被折叠的 条评论
为什么被折叠?



