目录
建议全文阅读
一、线程id
线程id是库函数赋予的,因此也是库内部管理的
类似于,身份证是国家发的,所以国家管你;学号是学校分配的,所以学校管你
如果将线程id转为16进制,其实就是地址
为什么是地址?是什么地址?
动态库如何对线程进行管理?
先描述,在组织
动态库,在加载的时候,会被加载到进程地址空间的堆栈之间的共享区
库在创建线程时,创建线程结构体字段,这个结构体内部放着该线程的所有属性
在这个结构体内部,还会有一个叫做线程栈的字段
这个字段就是所谓线程独立的栈空间
所以,每当我们要找到一个线程,就是找到该线程控制块的地址即可
而pthread_t,即所谓线程id就是线程控制块的开始地址
二、线程互斥
多线程并发访问公共资源时,会发生冲突
什么冲突呢?就是可能会导致多个线程同时访问同一个资源,导致结果错误
这是因为线程可以在任意阶段被切换
当线程a在执行时,可能会在任意阶段被线程b切换
同理,线程b也有可能会在任意阶段被线程c切换
当一个线程被切换时,会保留运行的数据,以备后面再被唤醒时继续运行,这些数据叫做上下文
同时,在CPU运行时,会重新读取内存中的数据,以保持更新状态
这上述的机制,就有可能出现错误
例如抢票。
三、处理互斥---加锁
1、什么是锁?
锁是一个叫做互斥锁类型的数据类型,作用是:任何时刻,只允许一个线程进行资源访问
这样就使得并行的访问,变成串行的访问。
进而解决线程互斥,即同时访问资源的冲突。
2、怎么用锁?
锁是全局 / 静态的,下面介绍初始化锁、申请锁、释放锁的函数接口。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
功能: 初始化一个互斥锁对象。
参数:
mutex: 指向 pthread_mutex_t 类型的互斥锁对象。
attr: 指向 pthread_mutexattr_t 类型的属性对象,通常可以设置为 NULL 使用默认属性。
返回值: 0 成功,非零值表示错误。
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能: 对互斥锁上锁。如果锁已经被其他线程锁定,调用线程会被阻塞,直到锁被释放。
参数:mutex: 指向 pthread_mutex_t 类型的互斥锁对象。
返回值: 0 成功,非零值表示错误。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能: 解锁互斥锁。如果锁的状态允许,其他线程可以获得该锁。
参数:mutex: 指向 pthread_mutex_t 类型的互斥锁对象。
返回值: 0 成功,非零值表示错误。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能: 销毁互斥锁对象。锁的对象必须在不再使用时被销毁。
参数:mutex: 指向 pthread_mutex_t 类型的互斥锁对象。
返回值: 0 成功,非零值表示错误。
锁的使用:当有多个线程执行,并且要访问同一个资源时,就需要用到锁。锁的使用过程:初始化锁、加锁、解锁。
简单示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 定义共享资源和互斥锁
int shared_counter = 0; // 共享计数器
pthread_mutex_t lock; // 互斥锁
// 线程函数
void* thread_function(void* arg) {
for (int i = 0; i < 100000; i++) {
// 上锁,保护共享资源
pthread_mutex_lock(&lock);
// 访问和修改共享资源
shared_counter++;
// 解锁
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
// 初始化互斥锁
if (pthread_mutex_init(&lock, NULL) != 0) {
perror("互斥锁初始化失败");
return EXIT_FAILURE;
}
// 创建线程
pthread_t threads[4];
for (int i = 0; i < 4; i++) {
if (pthread_create(&threads[i], NULL, thread_function, NULL) != 0) {
perror("线程创建失败");
return EXIT_FAILURE;
}
}
// 等待所有线程完成
for (int i = 0; i < 4; i++) {
if (pthread_join(threads[i], NULL) != 0) {
perror("线程等待失败");
return EXIT_FAILURE;
}
}
// 销毁互斥锁
pthread_mutex_destroy(&lock);
// 打印最终结果
printf("最终的共享计数器值: %d\n", shared_counter);
return EXIT_SUCCESS;
}
3、加锁原则(原子性)
(加锁的本质,是将并行改串行)
1、加锁范围尽量小(只在临界区使用)
2、所有线程都应该加锁
3、所有线程都应该可以看到锁,因此锁也是一种资源,也要保证锁资源的安全;因此加锁的过程是原子性的
4、原子性:要么不做,要做就做完,没有中间状态;对锁就是要么没有申请,要么就申请成功
5、如果线程申请锁失败,线程会被阻塞
6、如果线程申请锁成功,代码继续执行
7、如果线程申请锁成功,线程执行临界区代码,线程执行临界区代码期间,可以切换线程
但是其他线程也无法执行临界区代码,因为锁没有被解锁,其他线程无法申请锁,即被阻塞
所以,即使切换了,线程依旧可以安全执行完毕
因此:只有当前线程释放了锁,其他线程才可以申请锁,即实现了线程申请锁的原子性质
要么申请成功,要么申请失败。
4、临界区和非临界区
对于访问资源,需要引出一个概念:临界区
临界区就是进行资源访问的代码
非临界区就是不进行资源访问的代码
保护临界区就是保护进行访问资源的代码
加锁,就是加在进临界区和出临界区这个阶段
使得线程对资源的访问由并行改为串行处理,从而避免了冲突
四、理解锁
1、理解线程阻塞
如何立即额申请锁成功,让临界区代码继续执行?lock函数会返回
如何立即额申请锁失败,不让临界区代码继续执行?lock函数一直不返回,即线程被阻塞
当申请锁失败,线程会被阻塞,状态设置为s,进入等待队列,等待被唤醒
当被唤醒,再次重新申请锁,成功,执行;失败,再次阻塞,进入等待队列.....
2、锁处理互斥冲突的底层实现
在正式理解锁的实现时,需要理解下述现象:
CPU的寄存器只有一套,当一个线程在执行时,寄存器中会存放当前线程的数据
这部分数据是线程私有的数据,当线程被切换时,线程会记录执行流的上下文数据,以备再被唤醒时继续执行
同时,新的线程执行时,因为数据是在内存中的,首先要从内存调取数据
把数据调取到CPU的寄存器后才能执行
我们写的代码,CPU是不认识的,CPU是电子设备,只认识有电还是没有电
因此代码首先会被汇编编译器翻译为汇编语言,再被二进制编译器将之汇编语言翻译成二进制序列,再被CPU执行
汇编指令其实对应的是底层的电子硬件电路
在汇编层面,当一个线程申请锁时,代码经过编译后链接后,会执行相关的汇编指令
我们说过,当一个新的线程被执行时,首先要到内存中调取数据
汇编执行的指令是:直接将内存的数据和寄存器的数据交换
相当于将内存中的数据变为线程私有
于是,当线程申请一个锁的时候,假设锁的状态设为1
首先会将在内存中的锁的数据和空数据交换
于是,锁的数据就被调取到了当前的CPU线程执行流中
于是当前线程的锁的状态是为1
而原来内存中锁的状态,被交换为了0,相当于锁已经被占用,不能再被申请
也就是相当于当前线程成功申请了锁
而我们又说过,当切换新的线程时
首先当前清空当前执行流的内容,即CPU寄存器中的内容
而这部分内容会被上一个被切换的线程带走,叫做上下文数据
于是,锁的数据也被带走了
而,当新线程申请锁时,再次去交换内存中的锁的数据时
会发现,锁的状态为0,已经没有锁了
锁去了哪里?被上一个线程的上下文数据带走了
因此,此时,当前切换调度的线程无法申请锁,无法继续执行
于是,只能阻塞,被操作系统调入等待队列,等待被唤醒
同理,此后的所有其他线程都将处于这个状态
直到拥有锁的线程再次被唤醒,并且执行完毕后,再释放锁的资源
其他线程才能申请锁成功
而释放锁资源的本质,其实也就是交换数据
将内存中锁的状态设置为1
以下是加锁、解锁的汇编语言: