Linux线程安全

Linux线程安全


Linux线程安全详解

Linux线程互斥

进程线程间的互斥相关背景概念

  • 临界资源:多线程共享的资源,如全局变量。
  • 临界区:访问临界资源的代码段。
  • 互斥:确保同一时刻只有一个线程进入临界区,保护临界资源。
  • 原子性:操作不可中断,要么完成要么未完成。

进程与线程对比:

  • 进程间通信需创建第三方资源(如管道、共享内存),这些资源即临界资源,访问代码为临界区。
  • 线程共享进程的大部分资源(如全局变量),无需额外创建资源即可通信。

示例:线程间通信

c 复制代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int count = 0;
void* Routine(void* arg) {
    while (1) {
        count++;
        sleep(1);
    }
    pthread_exit((void*)0);
}
int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, Routine, NULL);
    while (1) {
        printf("count: %d\n", count);
        sleep(1);
    }
    pthread_join(tid, NULL);
    return 0;
}
  • count 是临界资源,被主线程和新线程共享。
  • count++ 和 printf 是临界区。

互斥与原子性问题 :

多线程并发操作临界资源可能导致数据不一致。例如抢票系统:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int tickets = 1000;
void* TicketGrabbing(void* arg) {
    const char* name = (char*)arg;
    while (1) {
        if (tickets > 0) {
            usleep(10000); // 模拟业务耗时
            printf("[%s] get a ticket, left: %d\n", name, --tickets);
        } else {
            break;
        }
    }
    printf("%s quit!\n", name);
    pthread_exit((void*)0);
}
int main() {
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, TicketGrabbing, "thread 1");
    pthread_create(&t2, NULL, TicketGrabbing, "thread 2");
    pthread_create(&t3, NULL, TicketGrabbing, "thread 3");
    pthread_create(&t4, NULL, TicketGrabbing, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    return 0;
}

运行结果可能出现负票数,原因:

  1. if (tickets > 0) 判断后线程可能切换。
  2. usleep 期间其他线程可进入临界区。
  3. --tickets 非原子操作,分为加载(load)、更新(update)、存储(store)三步,可能被打断。

互斥量mutex

互斥量(mutex)是Linux提供的锁机制,解决线程间的互斥问题,要求:

  1. 同一时刻只有一个线程进入临界区。
  2. 无线程在临界区时,允许多线程竞争进入。
  3. 线程不在临界区时不得阻止其他线程进入。

互斥量的接口

  • 初始化 :

    c 复制代码
    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    • 动态分配:pthread_mutex_init(&mutex, NULL);
    • 静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 销毁 :

    c 复制代码
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    • 注意:已加锁的互斥量不可销毁。
  • 加锁 :

    c 复制代码
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    • 未锁:加锁成功。
    • 已锁:阻塞等待。
  • 解锁 :

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

抢票系统改进:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int tickets = 1000;
pthread_mutex_t mutex;
void* TicketGrabbing(void* arg) {
    const char* name = (char*)arg;
    while (1) {
        pthread_mutex_lock(&mutex);
        if (tickets > 0) {
            usleep(100);
            printf("[%s] get a ticket, left: %d\n", name, --tickets);
            pthread_mutex_unlock(&mutex);
        } else {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    printf("%s quit!\n", name);
    pthread_exit((void*)0);
}
int main() {
    pthread_mutex_init(&mutex, NULL);
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, TicketGrabbing, "thread 1");
    pthread_create(&t2, NULL, TicketGrabbing, "thread 2");
    pthread_create(&t3, NULL, TicketGrabbing, "thread 3");
    pthread_create(&t4, NULL, TicketGrabbing, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);
    return 0;
}
  • 加锁后票数不再出现负值。
  • 注意:加锁降低并行性,需合理选择加锁范围。

互斥量实现原理探究

  • 原子性:加锁后,临界区操作对其他线程表现为原子性(要么未开始,要么已完成)。
  • 线程切换:临界区内可能切换,但锁未释放,其他线程无法进入。
  • 锁的保护:锁本身是临界资源,需保证申请过程原子性。
  • 实现机制 :使用 swap/exchange 指令(单指令原子性)。
    • 伪代码 :
      • 加锁:xchgb 交换寄存器(清0)与 mutex(初始1),成功得1,失败得0。
      • 解锁:mutex 置1,唤醒等待线程。
    • 原理:总线周期独占,确保多核环境下原子性。

可重入VS线程安全

概念

  • 线程安全:多线程并发执行同一代码,结果一致。
  • 可重入:函数被多执行流调用,前一流未结束,后一流进入,结果无误。

常见的线程不安全的情况

  • 无锁保护的共享变量操作。
  • 函数状态随调用改变(如静态变量)。
  • 返回静态变量指针。
  • 调用线程不安全函数。

常见的线程安全的情况

  • 只读全局/静态变量。
  • 接口操作原子性。
  • 线程切换不影响结果。

常见的不可重入的情况

  • 调用 malloc/free(全局堆链表)。
  • 使用标准I/O(全局数据结构)。
  • 函数体内依赖静态数据。

常见的可重入的情况

  • 不使用全局/静态变量。
  • 不调用 malloc/new。
  • 不调用不可重入函数。
  • 数据由调用者提供或局部拷贝。

可重入与线程安全联系

  • 可重入函数一定是线程安全的。
  • 不可重入函数多线程使用可能不安全。
  • 有全局变量的函数通常既不可重入也不安全。

可重入与线程安全区别

  • 可重入是线程安全的一种。
  • 加锁可使函数线程安全,但若锁未释放则不可重入。

常见锁概念

死锁

死锁是多线程互相等待对方释放资源,导致永久阻塞。

  • 单线程死锁:同一线程多次申请同一锁。

    c 复制代码
    #include <stdio.h>
    #include <pthread.h>
    pthread_mutex_t mutex;
    void* Routine(void* arg) {
        pthread_mutex_lock(&mutex);
        pthread_mutex_lock(&mutex); // 死锁
        pthread_exit((void*)0);
    }
    int main() {
        pthread_mutex_init(&mutex, NULL);
        pthread_t tid;
        pthread_create(&tid, NULL, Routine, NULL);
        pthread_join(tid, NULL);
        pthread_mutex_destroy(&mutex);
        return 0;
    }
    • 状态:Sl+(锁阻塞)。
  • 阻塞原理:

    • 进程等待资源时,从运行队列移至资源等待队列。
    • 资源就绪后,唤醒并移回运行队列。

死锁的四个必要条件

  1. 互斥:资源独占。
  2. 请求与保持:持有资源并请求新资源。
  3. 不剥夺:资源不可强夺。
  4. 循环等待:线程间形成等待环。

避免死锁

  • 破坏任一必要条件。
  • 统一加锁顺序。
  • 避免未释放锁。
  • 一次性分配资源。
  • 使用死锁检测或银行家算法。

Linux线程同步

同步概念与竞态条件

  • 同步:在数据安全前提下,按特定顺序访问临界资源,避免饥饿。
  • 竞态条件:时序问题导致程序异常。
  • 问题:单纯加锁可能导致强竞争力线程垄断资源,其他线程饥饿。

条件变量

条件变量描述资源就绪状态,配合互斥锁实现同步:

  • 等待:线程挂起等待条件满足。
  • 唤醒:另一线程使条件满足并通知。

条件变量函数

  • 初始化 :

    c 复制代码
    int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
    • 静态:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 销毁 :

    c 复制代码
    int pthread_cond_destroy(pthread_cond_t *cond);
  • 等待 :

    c 复制代码
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
  • 唤醒 :

    c 复制代码
    int pthread_cond_signal(pthread_cond_t *cond); // 唤醒首个
    int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒全部

示例:主线程控制子线程活动

c 复制代码
#include <iostream>
#include <pthread.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void* Routine(void* arg) {
    pthread_detach(pthread_self());
    std::cout << (char*)arg << " run..." << std::endl;
    while (true) {
        pthread_cond_wait(&cond, &mutex);
        std::cout << (char*)arg << "活动..." << std::endl;
    }
}
int main() {
    pthread_mutex_init(&mutex, nullptr);
    pthread_cond_init(&cond, nullptr);
    pthread_t t1, t2, t3;
    pthread_create(&t1, nullptr, Routine, (void*)"thread 1");
    pthread_create(&t2, nullptr, Routine, (void*)"thread 2");
    pthread_create(&t3, nullptr, Routine, (void*)"thread 3");
    while (true) {
        getchar();
        pthread_cond_signal(&cond); // 逐个唤醒
        // pthread_cond_broadcast(&cond); // 全部唤醒
    }
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

为什么pthread_cond_wait需要互斥量

  • 同步需求:条件需由另一线程改变共享数据触发。

  • 死锁问题:若不释放锁,等待时持有锁导致死锁。

  • 功能 :

    • 等待时释放锁。
    • 被唤醒时自动加锁。
  • 错误设计 :

    c 复制代码
    pthread_mutex_lock(&mutex);
    while (condition_is_false) {
        pthread_mutex_unlock(&mutex); // 非原子
        pthread_cond_wait(&cond); // 可能错过信号
        pthread_mutex_lock(&mutex);
    }

条件变量使用规范

  • 等待 :

    c 复制代码
    pthread_mutex_lock(&mutex);
    while (condition_is_false)
        pthread_cond_wait(&cond, &mutex);
    // 修改条件
    pthread_mutex_unlock(&mutex);
  • 唤醒 :

    c 复制代码
    pthread_mutex_lock(&mutex);
    // 设置条件为真
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);

总结

  • 互斥:通过互斥量保护临界资源,确保数据一致性。
  • 同步:条件变量配合锁实现有序访问,避免竞态和饥饿。
  • 安全:理解可重入与线程安全,防范死锁。
相关推荐
路溪非溪23 分钟前
C++标准库之智能指针
linux·开发语言·c++
程序员大雄学编程30 分钟前
Linux 磁盘满了怎么排查?df / du / find 三步定位大文件
linux·运维·php
RuoyiOffice39 分钟前
Hyper-V+Kubernetes 搭建实战:保留 Windows,构建三节点 Linux 容器集群
linux·spring boot·kubernetes·containerd·hyper-v·微服务部署·ruoyi office
承渊政道1 小时前
Linux网络学习【五种IO模型与⾮阻塞IO】
linux·网络·学习·ubuntu·io模型·非阻塞io
wuminyu2 小时前
JVM执行引擎学习总结:热点字节码编译成本地机器码
java·linux·c语言·jvm·c++
kiros_wang2 小时前
通用工具模块封装:防抖节流、参数校验公共工具类
linux·运维·ubuntu
honsor2 小时前
以太网温湿度传感器|动环监控核心采集设备,协议、组网、Zabbix接入实战
linux·服务器·ubuntu
Android系统攻城狮2 小时前
Linux FFmpeg深度解析之avcodec_default_get_buffer2调用流程与实战(二十)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
深圳恒讯2 小时前
香港云主机硬盘满了怎么办?Linux和Windows在线扩容操作教程
linux·运维·windows
Android系统攻城狮3 小时前
Linux FFmpeg深度解析之av_parser_init调用流程与实战(二十一)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频