线程id与互斥---线程阻塞和线程加锁详解

目录

建议全文阅读

一、线程id

二、线程互斥

三、处理互斥---加锁

1、什么是锁?

2、怎么用锁?

3、加锁原则(原子性)

4、临界区和非临界区

四、理解锁

1、理解线程阻塞

2、锁处理互斥冲突的底层实现

五、线程封装


一、线程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

以下是加锁、解锁的汇编语言:

五、线程封装

  • 24
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二十5画生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值