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);

总结

  • 互斥:通过互斥量保护临界资源,确保数据一致性。
  • 同步:条件变量配合锁实现有序访问,避免竞态和饥饿。
  • 安全:理解可重入与线程安全,防范死锁。
相关推荐
Waitccy5 小时前
CentOS 7 离线升级 OpenSSH
linux·运维·centos
黎明晓月5 小时前
Linux 上使用 Docker 部署 Kafka 集群
linux·docker·kafka
清风~徐~来5 小时前
【Linux】应用层协议 HTTP
linux·运维·http
柳如烟@5 小时前
LVS-NAT 负载均衡与共享存储配置
linux·负载均衡·lvs
Forget_85506 小时前
Linux的例行性工作
linux·运维·服务器
用手码出世界6 小时前
自定义minshell
linux·服务器
心随雪冻7 小时前
18.PCIe总线入门理解与Linux上PCIe设备配置与使用
linux·arm开发·嵌入式硬件
m0_745364247 小时前
LVS-DR模式配置脚本
linux·运维·服务器·github·lvs
不知名。。。。。。。。7 小时前
Linux--命令行操作
linux·运维·服务器
老年DBA8 小时前
Linux Namespace(网络命名空间)系列三 --- 使用 Open vSwitch 和 VLAN 标签实现网络隔离
linux·运维·服务器·网络