执行流是指操作系统对进程或线程的调度和执行顺序。它决定了程序中的指令按照何种顺序被执行。
现阶段可以粗浅的理解为,执行流决定执行哪个线程或进程的代码(或者说执行流决定了CPU资源的分配),执行流执行代码的顺序逻辑即为程序员编写代码时的顺序逻辑。 一台主机上会有多个进程或线程时,实际上不是一个进程或线程分配到一个执行流(执行流的数量取决于CPU的核心数量), 而是执行流不挺的根据预先设置好的执行流调度算法(即CPU资源分配算法)在各个进程或线程之间切换,由于切换的速度很快、CPU的计算速度很快(对人来说)以及CPU调度算法的作用,用户往往干受不到这个过程。
函数被不同的执行流调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入。重入调用函数不会对预先设计好的代码执行逻辑产生影响的称为“可重入函数”。有可能因为重入而造成错乱,像这样的函数称为“不可重入函数”,
不可重入函数的特征:
①调用了malloc或free,因为malloc也是用全局链表来管理堆的。
②调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
③修改了全局变量,因为全局变量在函数调用之间可能被修改,从而导致函数的行为不可预测。
执行流切换时,操作系统会保存当前任务的上下文信息,包括程序计数器、寄存器值、栈指针等。这些上下文信息被保存在任务的控制块中。 当调度器切换到另一个任务时,它会从该任务的控制块中恢复之前保存的上下文信息,使得该任务可以从上次中断的地方继续执行。
原子性指的是不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成,代码的原子性则指的是一行代码要么被执行完要么没开始执行。实际上如果不采取额外的措施,进程或线程切换无法保证代码的原子性的。因为一行代码往往有多条汇编代码组成,汇编代码的执行是原子的,C语言代码不是原子的。例如:
C代码 i++
对应三条汇编指令:
load:将共享变量ticket从内存加载到寄存器中
update: 更新寄存器里面的值,执行-1操作
store:将新值,从寄存器写回共享变量ticket的内存地址
执行流在执行完load后可能被切换.I++语句没有被完全执行(故i++
语句不是原子的),如果这个时候又有执行流对全局变量 i 进行操作,就可能会产生预期之外的结果。
多线程执行流共享的资源(如全局变量)就叫做临界资源,每个线程内部,访问临界资源的代码,就叫做临界区。
根据前面对原子性,可重入概念的介绍,可以知道重入访问临界资源会产生错误,但多线程又有访问临界资源的需求,故有了互斥的概念和实现需求。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
//1.初始化互斥量锁——静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//1.初始化互斥量——动态分配
//mutex:要初始化的互斥量
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t*restrict attr);
//2.加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//3.解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//4.销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用逻辑图:
例子:
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex;
void* thread_func(void* arg)
{
pthread_mutex_lock(&mutex); // 加锁
printf("Thread %ld is inside the critical section\n", (long)arg);
pthread_mutex_unlock(&mutex); // 解锁
return NULL;
}
int main()
{
pthread_t thread1, thread2;
pthread_mutex_init(&mutex, NULL); // 初始化锁
pthread_create(&thread1, NULL, thread_func, (void*)1);
pthread_create(&thread2, NULL, thread_func, (void*)2);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex); // 销毁锁
return 0;
}
注意:
①使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
②不要销毁一个已经加锁的互斥量,已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
③互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。如果互斥量处于锁住状态,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。(线程对互斥量(锁)的争取是原子的,即只有抢到互斥量和没抢到两种状态,同一个互斥量同一时间最多只能被一个线程锁定)。
注意:
上下文 CPU寄存器 内存 是三个不同的存储空间 。
①先看lock伪代码部分,认识到lock函数本身是分多步完成的
②再看红字部分的问题
③按照数字序号,理解互斥锁的是如何实现的
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。为了避免死锁问题,编写代码时需要保证代码逻辑①加锁顺序一致②避免锁未释放③资源一次性分配。
死锁的产生图解: