Linux软件编程学习笔记(七):线程分离与线程间通信详解

一、线程分离属性(Detached Thread)

1.1 什么是线程分离?

每个线程在创建时有两种状态:

  • 可结合(joinable)线程 (默认):线程终止后,其资源(如栈、寄存器状态)不会被自动释放,必须由其他线程调用 pthread_join() 来回收。如果忘记调用,就会产生僵尸线程,造成资源泄漏。
  • 分离(detached)线程 :线程终止时,系统会自动回收其所有资源,无需 pthread_join()

关键点:一个线程在生命周期内要么是可结合的,要么是分离的,不能同时是两者。

1.2 为什么需要分离线程?

在实际开发中,很多线程扮演"即用即弃"的角色------执行完特定任务后,我们不需要获取其返回值,也不关心其退出状态。对于这类线程:

  • 如果使用可结合方式,必须显式调用 pthread_join(),增加代码复杂度;
  • 如果忘记 pthread_join(),会造成资源泄漏。

分离线程正是为解决这个问题而设计:让线程退出时自动清理资源,简化代码,避免僵尸线程。

1.3 如何设置线程分离?

有两种方式:

方式一:通过线程属性对象(创建时设置)

这是标准且推荐的方法。使用 pthread_attr_t 设置分离状态,然后将属性传入 pthread_create()

相关函数

c 复制代码
#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
  • detachstate 参数:
    • PTHREAD_CREATE_DETACHED:创建分离线程
    • PTHREAD_CREATE_JOINABLE:创建可结合线程(默认)

示例代码

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

void *thread_func(void *arg) {
    printf("分离线程 TID=%lu 开始执行\n", pthread_self());
    sleep(1);
    printf("分离线程退出,资源由系统自动回收\n");
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_attr_t attr;
    int ret;

    // 1. 初始化属性对象
    pthread_attr_init(&attr);
    // 2. 设置为分离状态
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    // 3. 创建线程
    ret = pthread_create(&tid, &attr, thread_func, NULL);
    if (ret != 0) {
        perror("pthread_create");
        pthread_attr_destroy(&attr);
        return -1;
    }
    // 4. 属性对象不再需要,销毁
    pthread_attr_destroy(&attr);

    // 主线程不需要 pthread_join,等待子线程执行
    sleep(2);
    printf("主线程结束\n");
    return 0;
}
方式二:线程创建后调用 pthread_detach()

如果线程已经以默认(可结合)状态创建,可以在运行时调用 pthread_detach() 将其转为分离状态。

c 复制代码
int pthread_detach(pthread_t thread);

两种用法:

  • 其他线程分离目标线程pthread_detach(tid);
  • 线程自己分离自己pthread_detach(pthread_self());

示例

c 复制代码
void *thread_func(void *arg) {
    pthread_detach(pthread_self());   // 自己分离自己
    // ... 执行任务
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    // 也可以由主线程分离:pthread_detach(tid);
    sleep(1);
    return 0;
}

1.4 分离线程 vs 可结合线程 对比

特性 可结合 (joinable) 分离 (detached)
资源回收 必须调用 pthread_join() 线程退出时系统自动回收
能否获取返回值 可以(通过 pthread_join 不能
默认状态 ✅ 是 ❌ 否,需显式设置
僵尸线程风险 有(忘记回收时)
适用场景 需要知道线程执行结果或退出状态 后台任务、无需关注结果的服务线程

二、线程间通信(Inter-thread Communication)

2.1 线程间通信的必要性

线程位于同一进程空间内,共享进程的数据段 (全局变量、静态变量)、堆区文件描述符 等。因此,线程间天然可以通过共享变量进行通信。例如:

c 复制代码
int shared_data = 0;  // 全局变量,所有线程共享

void *thread1(void *arg) { shared_data = 100; }
void *thread2(void *arg) { printf("%d\n", shared_data); }

但这种方式会引入严重的 资源竞争(Race Condition)问题。当多个线程同时读写同一共享变量时,由于CPU时间片调度的不确定性,可能导致数据不一致。

2.2 临界区与原子操作

  • 临界区(Critical Section):访问共享资源的代码片段。
  • 原子操作(Atomic Operation):不可被中断的操作,即执行过程中不会被线程调度打断。

我们通常需要将临界区"保护"起来,确保同一时刻只有一个线程进入,这就是互斥锁(Mutex) 的作用。


三、互斥锁(Mutex)

互斥锁是最基本的同步机制,通过 pthread_mutex_t 类型实现。

3.1 基本函数接口

c 复制代码
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, 
                       const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);   // 加锁(阻塞)
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 尝试加锁(非阻塞)
  • pthread_mutex_init:初始化锁,attr 通常为 NULL(使用默认属性)。
  • pthread_mutex_lock:如果锁已被占用,则阻塞等待;否则获取锁。
  • pthread_mutex_unlock:释放锁,唤醒等待线程。
  • pthread_mutex_trylock:尝试加锁,若锁已被占用则立即返回错误,不阻塞。

3.2 使用示例(保护共享变量)

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

int counter = 0;              // 共享变量
pthread_mutex_t mutex;       // 互斥锁

void *increment(void *arg) {
    for (int i = 0; i < 100000; ++i) {
        pthread_mutex_lock(&mutex);   // 加锁
        counter++;                    // 临界区
        pthread_mutex_unlock(&mutex); // 解锁
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    pthread_mutex_destroy(&mutex);
    printf("counter = %d\n", counter); // 正确结果 200000
    return 0;
}

注意:锁的粒度要适中,既不能太大(降低并发),也不能太小(无法有效保护)。

3.3 死锁(Deadlock)

死锁是指多个线程因竞争资源而相互等待,导致所有线程无法继续执行的状态。

死锁产生的四个必要条件

  1. 互斥条件:资源只能被一个线程占用。
  2. 不可剥夺条件:资源只能由占用者主动释放。
  3. 请求与保持条件:线程持有部分资源,同时等待其他资源。
  4. 循环等待条件:多个线程形成循环等待链。

避免死锁的常用方法

  • 使用 pthread_mutex_trylock 替代 pthread_mutex_lock,加锁失败时释放已持有的锁并重试。
  • 保持一致的加锁顺序:所有线程按相同顺序获取多把锁。
  • 减少锁的数量,尽量将资源合并。

四、信号量(Semaphore)

信号量是一种更通用的同步机制,可以用于线程间或进程间同步。在Linux中,信号量通过 sem_t 类型实现(POSIX 无名信号量)。

4.1 核心函数

c 复制代码
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_wait(sem_t *sem);   // 申请资源(P操作)
int sem_post(sem_t *sem);   // 释放资源(V操作)
int sem_trywait(sem_t *sem); // 非阻塞申请
  • sem_init:初始化信号量,value 为初始资源数;pshared 为 0 表示线程间共享,非 0 表示进程间共享。
  • sem_wait:如果信号量值 > 0,则减 1 并立即返回;否则阻塞等待。
  • sem_post:将信号量值加 1,并唤醒等待的线程(若有)。

4.2 信号量 vs 互斥锁

  • 互斥锁:只有 0 和 1 两种状态(二元信号量),用于保护临界区。
  • 信号量:可以有多个资源计数,可控制同时访问的线程数量(例如限流)。也可作为二进制信号量使用。

4.3 示例:使用信号量控制线程并发数

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

#define MAX_CONCURRENT 3

sem_t sem;

void *worker(void *arg) {
    int id = *(int *)arg;
    sem_wait(&sem);  // 申请资源,若并发数已满则阻塞
    printf("线程 %d 开始工作\n", id);
    sleep(2);
    printf("线程 %d 结束工作\n", id);
    sem_post(&sem);  // 释放资源
    return NULL;
}

int main() {
    pthread_t threads[10];
    int ids[10];
    sem_init(&sem, 0, MAX_CONCURRENT); // 最多3个线程同时工作

    for (int i = 0; i < 10; ++i) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, worker, &ids[i]);
    }

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

    sem_destroy(&sem);
    return 0;
}

五、总结

内容 核心要点
线程分离 设置 PTHREAD_CREATE_DETACHED 或调用 pthread_detach(),避免僵尸线程
互斥锁 保护临界区,防止资源竞争,注意死锁问题,保持加锁顺序一致
信号量 更灵活的同步工具,可控制资源数量,适用于生产者-消费者等场景
线程间通信本质 共享内存(全局变量、堆区)配合同步机制,确保数据一致性

多线程编程既带来了性能提升,也引入了同步复杂性。合理使用分离属性 简化资源管理,借助互斥锁信号量保障数据安全,是编写可靠多线程程序的关键。

📌 所有函数接口的详细手册可通过 man 3 函数名 查询,建议读者在实际开发中多实践、多调试。

相关推荐
Awh-13 分钟前
Linux软件应用编程 第一章中:Linux文件操作
linux·运维·服务器
sel_91 小时前
【多轮对话论文导读(三)】多轮对话与Agent论文阅读笔记:用户模拟、轨迹生成与长期记忆
论文阅读·人工智能·笔记·深度学习·算法·机器学习
zzm6281 小时前
《SIGIR 2018论文精读|深度学习点击序列模型:对搜索页面点击行为进行序列建模》
笔记·深度学习·推荐系统·论文精读
我命由我123452 小时前
UI 设计 7 种排列方式
xml·学习·ui·html·产品运营·产品经理·学习方法
Tim风声(网络工程师)2 小时前
Kail下载
linux·运维·服务器
常驻客栈2 小时前
B-深入理解计算机系统第3版之第8章:异常控制流
笔记·学习·常驻客栈·深入理解计算机系统·学习笔记‘’
lzhdim2 小时前
13、JavaScript事件循环机制 - JavaScript学习系列文章
开发语言·前端·javascript·学习·ecmascript
小杨不想秃头2 小时前
信息安全工程师考试
学习·网络安全
喜欢吃豆2 小时前
otes on LLMs 更新:一份更系统的大模型工程学习手册
学习·大模型·agent