Linux 线程(2)

1.并发问题

要了解并发问题我们可以通过一段代码来学习

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int num = 10;
#define THREAD_NUM 10

void* reduce_num(void* arg) {
    while (1) {
        if (num >= 0) {
            sleep(1);
            printf("当前num值:%d\n", num);
            num--;
        } else {
            break;
        }
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[THREAD_NUM];

    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_create(&threads[i], NULL, reduce_num, NULL);
    }

    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("最终num值:%d\n", num);
    return 0;
}

为什么这个代码最后num会到-10 ???(预期最多到-1)

我明明写的是num<0的时候循环就不会接着执行减减呀

为什么num会到-10???

我们来看这个代码

num--;

这个代码转换成汇编主要是三件事

1.先将内存中的 num 读入到 cpu 的寄存器中
2.CPU 内部进行 -- 操作
3.将计算结果写回内存

但是多线程的时候会有问题

首先我们要了解一件事

我们线程使用cpu计算的时候 有一个时间片

时间片到了cpu就会切换其他线程

并且线程会把此时的cpu寄存器的值(属于线程上下文的一部分)拷贝一份带走

以两个线程为例(多个线程同理)

if (num >= 0)进行判断

本质上也一种运算 叫做逻辑运算

本质也要执行和num--一样的三步

那么当第一个线程简称线程1 进来的时候

执行完三步切换到线程2

此时num为0

于是执行while循环的代码

但是线程1没执行到num--的时候 此时num为0

线程2恰好也要if (num >= 0)进行判断 此时刚好进入

当线程2执行到num--的时候 线程1的num--执行完了 此时num=-1

线程2再num-- num=-2

这也就是为什么num会产生负数的原因

此时这个时候的num就叫临界资源

很熟悉对吗

我们前面讲信号量的时候聊过 后面也会重提的

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完

上面的问题本质上是由于线程并发访问造成的

怎么解决

一次只允许一个线程进入就可以拉!!!

也就是加锁

2.进程锁

1.锁的静态初始化

cpp 复制代码
// 静态初始化(默认属性,等价于pthread_mutex_init(&mutex, NULL))
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

全局 / 静态锁实例 简洁,无需手动调用函数 仅支持默认属性

2.pthread_mutex_init

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
const pthread_mutexattr_t *restrict attr);

局部 / 动态分配的锁实例 支持自定义锁属性 需手动调用,代码稍多

mutex 指向要初始化的pthread_mutex_t锁实例的指针(必传)

attr 互斥锁的属性指针:

① NULL:使用默认属性(最常用,普通互斥锁);

② 自定义属性:需提前通过pthread_mutexattr_init创建

成功:返回0;

失败:返回非 0 的错误码(POSIX 线程函数不设置errno,直接返回错误码)。

3.pthread_mutex_destroy

cpp 复制代码
int pthread_mutex_destroy(pthread_mutex_t *mutex);

mutex 指向要销毁的pthread_mutex_t锁实例的指针(必传,且必须是已初始化的锁)

成功:返回0;

失败:返回非 0 的错误码

只能销毁已初始化的锁:对未初始化的锁或已销毁的锁调用该函数,会导致未定义行为(如程序崩溃);

不能销毁被持有(锁定)的锁:如果锁当前被某个线程持有,调用销毁函数会失败(返回错误码EBUSY),或导致未定义行为;

销毁后的锁可重新初始化:锁被销毁后,若需再次使用,需重新调用pthread_mutex_init初始化;

4.pthread_mutex_lock

作用

尝试获取指定的互斥锁:

如果锁当前是未被持有的状态,调用线程会成功获取锁,继续执行后续代码;

如果锁当前是已被其他线程持有的状态,调用线程会进入阻塞状态(默认属性),直到锁被释放后再重新竞争。

cpp 复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex);

参数

mutex:指向目标互斥锁(pthread_mutex_t类型)的指针(通常是全局 / 线程间共享的变量)。

返回值

成功获取锁:返回 0;

失败:返回非 0 的错误码(常见如):

EINVAL:传入的互斥锁无效(比如未初始化);

EDEADLK:调用线程已经持有该锁(重复加锁),导致死锁。

5.pthread_mutex_unlock

作用

释放当前线程持有的互斥锁:

把锁的状态置为 "未被持有",让其他阻塞在pthread_mutex_lock上的线程可以竞争获取锁;

只有当前持有该锁的线程才能调用(否则会报错)。

cpp 复制代码
int pthread_mutex_unlock(pthread_mutex_t *mutex);

mutex:同pthread_mutex_lock,指向目标互斥锁的指针。

返回值

成功释放锁:返回 0;

失败:返回非 0 的错误码(常见如):

EPERM:当前线程并没有持有该锁,却尝试释放;

EINVAL:传入的互斥锁无效。

我们把原先代码加锁后再试一下

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int num = 10;
#define THREAD_NUM 10

pthread_mutex_t mutex;

void* reduce_num(void* arg) {
    while (1) {
        pthread_mutex_lock(&mutex);
        if (num >= 0) {
            sleep(1);
            printf("当前num值:%d\n", num);
            num--;
            pthread_mutex_unlock(&mutex);
        } else {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[THREAD_NUM];

    pthread_mutex_init(&mutex, NULL);

    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_create(&threads[i], NULL, reduce_num, NULL);
    }

    for (int i = 0; i < THREAD_NUM; i++) {
        pthread_join(threads[i], NULL);
    }

    pthread_mutex_destroy(&mutex);

    printf("最终num值:%d\n", num);
    return 0;
}

我们枷锁后达到了我们的预期结果

num为0的时候执行最后一次--

当num为-1的时候停止

但是我们要思考一个问题 每个线程竞争cpu的强度不一样

我们只保证一次只有一个线程可以执行一个代码

万一有一个线程竞争能力强 其他线程一直在等 这样会造成严重的效率问题

因此我们线程还有一批条件变量的接口

6.条件变量静态初始化

cpp 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

7.pthread_cond_init

作用:动态初始化条件变量(也可使用静态初始化宏),为条件变量分配系统资源并设置属性。

cpp 复制代码
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

cond:指向要初始化的条件变量(pthread_cond_t 类型)的指针(通常为全局 / 线程间共享变量)。

attr:条件变量的属性指针,传入NULL表示使用默认属性(如进程内共享、默认调度策略);若需自定义属性(如进程间共享),需先初始化pthread_condattr_t。

成功:返回0;

失败:返回非 0 的错误码(如EINVAL:属性无效,ENOMEM:内存不足)。

8.pthread_cond_destroy

作用:释放条件变量占用的系统资源,销毁后条件变量不可再使用(除非重新初始化)。

cpp 复制代码
int pthread_cond_destroy(pthread_cond_t *cond);

cond:指向已初始化的条件变量的指针。

成功:返回0;

失败:返回非 0 的错误码(如EINVAL:条件变量无效,EBUSY:仍有线程在等待该条件变量)。

9.pthread_cond_wait

cpp 复制代码
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

作用:让当前线程阻塞等待条件变量被唤醒,同时自动释放持有的互斥锁;当被唤醒后,线程会重新获取互斥锁并继续执行。

cond:指向目标条件变量的指针。

mutex:指向与条件变量绑定的互斥锁的指针(必须是当前线程已持有的锁)。

成功:返回0;

失败:返回非 0 的错误码(如EINVAL:参数无效,EPERM:当前线程未持有互斥锁)。

注意事项:必须用循环判断条件(防止虚假唤醒)
操作系统可能因信号、线程调度等原因触发虚假唤醒(线程被唤醒,但条件并未满足),因此绝对不能用 if 判断条件,必须用 while 循环:

pthread_cond_wait 是原子操作,包含三步:
1.释放当前线程持有的mutex(让其他线程可以操作共享资源,修改条件);
2.将当前线程加入条件变量的等待队列,进入阻塞状态(不占用 CPU 资源);
3.当被pthread_cond_signal/pthread_cond_broadcast唤醒后,线程会重新竞争获取mutex,获取成功后才会退出pthread_cond_wait。

9.pthread_cond_signa

作用:从条件变量的等待队列中,唤醒任意一个等待的线程(具体唤醒哪个由系统调度决定)。

cpp 复制代码
int pthread_cond_signal(pthread_cond_t *cond);

cond:指向目标条件变量的指针。

成功:返回0;

失败:返回非 0 的错误码(如EINVAL:条件变量无效)。

注意事项

若没有线程等待该条件变量,该函数无任何效果(不会报错);

唤醒的线程不会立即执行,需重新竞争获取互斥锁后才能继续。

10.pthread_cond_broadcast

作用:唤醒条件变量等待队列中的所有线程,让它们竞争互斥锁后继续执行。

cpp 复制代码
int pthread_cond_broadcast(pthread_cond_t *cond);

cond:指向目标条件变量的指针。

成功:返回0;

失败:返回非 0 的错误码(如EINVAL:条件变量无效)。

具体条件变量的接口如何使用详见 生产者 - 消费者模型(简单版)

3.可重入和线程安全

1.概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数

2.常见的线程不安全的情况

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数

3.常见的线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性

4.常见不可重入的情况

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构

5.常见可重入的情况

不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

6.可重入与线程安全联系

函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

7.可重入与线程安全区别

可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的。

4.死锁

1.概念

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状态。

2.死锁四个必要条件

互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

3.避免死锁

破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配

4.避免死锁算法

死锁检测算法(了解)
银行家算法(了解)

相关推荐
Promise4852 小时前
关于使用wsl实现linux移植(imux6ull)的网络问题
linux·服务器·网络
郝学胜-神的一滴2 小时前
Linux线程的共享资源与非共享资源详解
linux·服务器·开发语言·c++·程序人生·设计模式
郝学胜-神的一滴2 小时前
Linux进程与线程的区别:从内存三级映射角度深入解析
linux·服务器·c++·程序人生
雪花凌落的盛夏2 小时前
x86电脑安装steamOS
linux
不爱吃糖的程序媛2 小时前
OpenHarmony Linux 环境 SDK 使用说明(进阶--依赖库的解决方法)
linux·运维·harmonyos
KaDa_Duck2 小时前
DASCTF 2025下半年赛 PWN-mvmp复盘笔记
linux·笔记·安全
ChristXlx2 小时前
Linux安装Minio(虚拟机适用)
linux·运维·网络
顾安r2 小时前
12.18 脚本网页 C标准库
linux·c语言·stm32·嵌入式硬件·html5
A13247053122 小时前
Linux文件查找:find和locate命令入门
linux·运维·服务器·网络·chrome