linux-线程编程

一、核心知识点介绍

  1. 线程概念

    • 线程是进程内的一个执行流,是操作系统进行 CPU 调度的基本单位。
    • 同一个进程内的多个线程共享进程的地址空间(代码段、数据段、堆、打开的文件描述符等)。
    • 每个线程拥有自己独立的栈空间、程序计数器 (PC) 和线程本地存储 (TLS)。
    • 线程比进程更轻量级,创建和切换开销更小,更适用于并发任务。
  2. POSIX 线程 (pthreads)

    • Linux 主要使用 POSIX 线程标准(pthread)来实现线程编程。
    • 相关的函数和数据类型都定义在头文件 <pthread.h> 中。
    • 编译时需要链接 pthread 库,通常使用 -pthread-lpthread 编译选项。
  3. 线程创建与终止

    • 创建线程: int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
      • thread: 指向新线程 ID 的指针。
      • attr: 线程属性对象,通常设为 NULL 使用默认属性。
      • start_routine: 新线程要执行的函数(返回 void*,参数为 void*)。
      • arg: 传递给 start_routine 的参数。
      • 返回值:成功返回 0,失败返回错误码。
    • 等待线程结束: int pthread_join(pthread_t thread, void **retval);
      • thread: 要等待的线程 ID。
      • retval: 用于接收目标线程的退出状态(start_routine 的返回值)。
      • 返回值:成功返回 0,失败返回错误码。调用线程会阻塞直到目标线程终止。
    • 线程终止:
      • 线程函数 start_routine 执行 return 语句。
      • 在线程内部调用 void pthread_exit(void *retval); 显式退出。
      • 主线程调用 exitmain 函数返回会导致整个进程终止,所有线程也随之结束(不建议)。
    • 分离线程: int pthread_detach(pthread_t thread);
      • 将线程标记为分离状态。分离状态的线程终止时,其资源会自动回收,无需其他线程调用 pthread_join
      • 不能在已分离的线程上调用 pthread_join
  4. 线程同步

    多个线程并发访问共享资源时,需要同步机制来保证数据的一致性和避免竞态条件。

    • 互斥锁 (Mutex):
      • 用于保护临界区,确保同一时间只有一个线程能访问共享资源。
      • 初始化:
        • 静态初始化 (全局或静态变量):pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
        • 动态初始化:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); (通常 attr 设为 NULL)
      • 加锁: int pthread_mutex_lock(pthread_mutex_t *mutex); (阻塞)
      • 尝试加锁: int pthread_mutex_trylock(pthread_mutex_t *mutex); (非阻塞)
      • 解锁: int pthread_mutex_unlock(pthread_mutex_t *mutex);
      • 销毁: int pthread_mutex_destroy(pthread_mutex_t *mutex); (动态初始化的需要销毁)
    • 条件变量 (Condition Variable):
      • 用于线程间的条件等待。通常与互斥锁配合使用。
      • 允许线程在某个条件不满足时阻塞等待,直到其他线程改变条件并发出信号。
      • 初始化:
        • 静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
        • 动态初始化:int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
      • 等待条件: int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
        • 该函数会原子性地释放 mutex 并阻塞当前线程。在被唤醒后,会重新获取 mutex 锁再返回。
        • 注意: 条件检查应在循环中进行,因为可能存在虚假唤醒 (spurious wakeup)。
      • 唤醒等待线程:
        • int pthread_cond_signal(pthread_cond_t *cond); - 唤醒至少一个等待该条件的线程。
        • int pthread_cond_broadcast(pthread_cond_t *cond); - 唤醒所有等待该条件的线程。
      • 销毁: int pthread_cond_destroy(pthread_cond_t *cond);
  5. 线程安全与可重入

    • 线程安全: 一个函数或代码段可以被多个线程同时调用而不会产生错误结果。
    • 可重入: 一个函数可以在其自身尚未执行完毕时再次被调用(例如信号处理函数调用它)。可重入函数必然是线程安全的,但线程安全函数不一定是可重入的(例如使用全局静态数据的线程安全函数)。
    • 编写线程安全代码的关键:避免使用全局或静态变量,或使用同步机制保护对它们的访问;使用局部变量或线程本地存储 (TLS)。
  6. 线程取消

    • 一个线程可以请求取消另一个线程 (int pthread_cancel(pthread_t thread);)。
    • 目标线程可以设置取消状态 (int pthread_setcancelstate(int state, int *oldstate);) 和取消类型 (int pthread_setcanceltype(int type, int *oldtype);) 来控制如何响应取消请求。
    • 线程可以选择在取消点 (pthread_testcancel();) 或在接收到取消请求时立即退出。
    • 可以使用清理处理程序 (pthread_cleanup_push();, pthread_cleanup_pop();) 在取消时释放资源。

二、技术代码示例

示例 1:基本线程创建与等待
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程函数
void *print_message(void *ptr) {
    char *message = (char *) ptr;
    printf("%s\n", message);
    return NULL; // 线程正常结束
}

int main() {
    pthread_t thread1, thread2;
    const char *message1 = "Thread 1";
    const char *message2 = "Thread 2";

    // 创建线程1
    if (pthread_create(&thread1, NULL, print_message, (void *)message1)) {
        fprintf(stderr, "Error creating thread1\n");
        return 1;
    }

    // 创建线程2
    if (pthread_create(&thread2, NULL, print_message, (void *)message2)) {
        fprintf(stderr, "Error creating thread2\n");
        return 1;
    }

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("Main thread exiting.\n");
    return 0;
}

编译: gcc -o basic_thread basic_thread.c -pthread

示例 2:使用互斥锁保护共享计数器
c 复制代码
#include <stdio.h>
#include <pthread.h>

#define NUM_THREADS 5
#define MAX_COUNT 100000

int counter = 0; // 共享计数器
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER; // 保护计数器的互斥锁

void *increment_counter(void *arg) {
    for (int i = 0; i < MAX_COUNT / NUM_THREADS; i++) {
        pthread_mutex_lock(&counter_mutex); // 进入临界区前加锁
        counter++; // 修改共享变量
        pthread_mutex_unlock(&counter_mutex); // 离开临界区后解锁
    }
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];

    // 创建多个线程
    for (int i = 0; i < NUM_THREADS; i++) {
        if (pthread_create(&threads[i], NULL, increment_counter, NULL)) {
            perror("pthread_create");
            return 1;
        }
    }

    // 等待所有线程结束
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // 销毁互斥锁 (静态初始化通常不需要显式销毁,但习惯上可以加)
    pthread_mutex_destroy(&counter_mutex);

    printf("Final counter value: %d (Expected: %d)\n", counter, MAX_COUNT);
    return 0;
}
示例 3:使用条件变量实现生产者-消费者模型 (简化版)
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define BUFFER_SIZE 10

int buffer[BUFFER_SIZE]; // 共享缓冲区
int count = 0; // 当前缓冲区中元素数量
int in = 0; // 生产者放入位置
int out = 0; // 消费者取出位置

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 保护缓冲区的互斥锁
pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER; // 生产者条件变量 (缓冲区有空位)
pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER; // 消费者条件变量 (缓冲区有数据)

void *producer(void *arg) {
    for (int i = 0; i < 20; i++) { // 生产20个数据
        pthread_mutex_lock(&mutex);

        // 等待缓冲区有空位 (使用while循环防止虚假唤醒)
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_producer, &mutex);
        }

        // 生产数据,放入缓冲区
        buffer[in] = i;
        printf("Produced: %d\n", i);
        in = (in + 1) % BUFFER_SIZE;
        count++;

        // 通知消费者可能有数据了
        pthread_cond_signal(&cond_consumer);

        pthread_mutex_unlock(&mutex);
        // 模拟生产耗时
        // usleep(rand() % 100000);
    }
    return NULL;
}

void *consumer(void *arg) {
    for (int i = 0; i < 20; i++) { // 消费20个数据
        pthread_mutex_lock(&mutex);

        // 等待缓冲区有数据
        while (count == 0) {
            pthread_cond_wait(&cond_consumer, &mutex);
        }

        // 消费数据,从缓冲区取出
        int item = buffer[out];
        printf("Consumed: %d\n", item);
        out = (out + 1) % BUFFER_SIZE;
        count--;

        // 通知生产者可能有空位了
        pthread_cond_signal(&cond_producer);

        pthread_mutex_unlock(&mutex);
        // 模拟消费耗时
        // usleep(rand() % 200000);
    }
    return NULL;
}

int main() {
    pthread_t prod_thread, cons_thread;

    srand(time(NULL)); // 初始化随机数种子 (用于模拟耗时)

    // 创建生产者和消费者线程
    if (pthread_create(&prod_thread, NULL, producer, NULL)) {
        perror("pthread_create producer");
        return 1;
    }
    if (pthread_create(&cons_thread, NULL, consumer, NULL)) {
        perror("pthread_create consumer");
        return 1;
    }

    // 等待线程结束
    pthread_join(prod_thread, NULL);
    pthread_join(cons_thread, NULL);

    // 清理 (静态初始化通常不需要显式销毁)
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_producer);
    pthread_cond_destroy(&cond_consumer);

    return 0;
}

三、注意事项

  1. 错误处理: 始终检查 pthread_* 函数的返回值,它们失败时会返回非零错误码(可通过 strerror 查看)。
  2. 资源泄露: 确保创建的线程被正确等待 (pthread_join) 或分离 (pthread_detach),避免僵尸线程。动态初始化的互斥锁和条件变量需要销毁 (pthread_mutex_destroy, pthread_cond_destroy)。
  3. 死锁: 仔细设计锁的获取顺序,避免循环等待。
  4. 竞态条件: 使用同步机制(互斥锁、条件变量)保护所有对共享资源的访问。
  5. 虚假唤醒: 在使用 pthread_cond_wait 时,必须在循环中检查条件,因为即使没有收到信号,等待也可能返回。
  6. 性能: 锁操作有开销,尽量减少临界区的范围。考虑使用读写锁 (pthread_rwlock_t) 或原子操作 (C11 _Atomic 或 GCC __sync_*, __atomic_* 内置函数) 优化特定场景。

1. 线程属性

线程属性(Thread Attributes)用于在创建线程时配置其行为。通过设置属性,可以控制线程的栈大小、调度策略、是否可分离等特性。

关键属性
  • 栈大小pthread_attr_setstacksize() 可设置线程栈大小。
  • 调度策略pthread_attr_setschedpolicy() 设置调度策略(如 SCHED_FIFOSCHED_RR)。
  • 可分离状态pthread_attr_setdetachstate() 设置线程为可分离(PTHREAD_CREATE_DETACHED)或可连接(PTHREAD_CREATE_JOINABLE)。
示例(POSIX)
c 复制代码
#include <pthread.h>

int main() {
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置为可分离
    pthread_t tid;
    pthread_create(&tid, &attr, thread_func, NULL);
    pthread_attr_destroy(&attr);
    return 0;
}

2. 线程本地存储(TLS)

线程本地存储(Thread-Local Storage, TLS)允许每个线程拥有独立的变量副本,避免共享数据的竞争条件。

实现方式
  • C/C++ :使用 thread_local 关键字(C11/C++11)。
  • POSIXpthread_key_create() 创建键,pthread_setspecific()pthread_getspecific() 设置/获取值。
示例(C++)
cpp 复制代码
#include <iostream>
#include <thread>

thread_local int counter = 0; // 每个线程独立副本

void increment() {
    counter++;
    std::cout << "Thread " << std::this_thread::get_id() 
              << ": counter = " << counter << std::endl;
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join(); t2.join();
    return 0;
}

3. 屏障(Barrier)

屏障(Barrier)用于同步多个线程的执行,确保所有线程到达某一点后才继续执行。

操作逻辑
  • 初始化屏障,指定等待的线程数 NNN。
  • 每个线程调用 pthread_barrier_wait() 等待。
  • 当第 NNN 个线程到达时,所有线程继续执行。
示例(POSIX)
c 复制代码
#include <pthread.h>

pthread_barrier_t barrier;

void* thread_func(void* arg) {
    // ... 执行任务
    pthread_barrier_wait(&barrier); // 等待其他线程
    // ... 继续执行
    return NULL;
}

int main() {
    pthread_barrier_init(&barrier, NULL, 3); // 等待3个线程
    pthread_t t1, t2, t3;
    pthread_create(&t1, NULL, thread_func, NULL);
    pthread_create(&t2, NULL, thread_func, NULL);
    pthread_create(&t3, NULL, thread_func, NULL);
    pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL);
    pthread_barrier_destroy(&barrier);
    return 0;
}

4. 读写锁

读写锁(Read-Write Lock)允许多个线程同时读取数据,但写入时独占访问。适用于读多写少的场景。

操作逻辑
  • 读锁:多个线程可同时获取读锁。
  • 写锁:仅一个线程可获取写锁,且与读/写锁互斥。
示例(POSIX)
c 复制代码
#include <pthread.h>

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

void* reader(void* arg) {
    pthread_rwlock_rdlock(&rwlock); // 获取读锁
    // ... 读操作
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

void* writer(void* arg) {
    pthread_rwlock_wrlock(&rwlock); // 获取写锁
    // ... 写操作
    pthread_rwlock_unlock(&rwlock);
    return NULL;
}

5. 信号量

信号量(Semaphore)用于控制对共享资源的访问数量,支持计数信号量(非二进制)。

核心操作
  • P操作sem_wait):申请资源,信号量减1。
  • V操作sem_post):释放资源,信号量加1。
示例(POSIX)
c 复制代码
#include <semaphore.h>

sem_t sem;

void* worker(void* arg) {
    sem_wait(&sem); // P操作
    // ... 访问资源
    sem_post(&sem); // V操作
    return NULL;
}

int main() {
    sem_init(&sem, 0, 3); // 初始化信号量,允许3个线程同时访问
    pthread_t t1, t2, t3;
    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);
    pthread_create(&t3, NULL, worker, NULL);
    pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_join(t3, NULL);
    sem_destroy(&sem);
    return 0;
}

对比总结

机制 适用场景 特点
线程属性 线程配置管理 控制栈大小、调度策略等
TLS 线程私有数据 避免竞争,每个线程独立副本
Barrier 多阶段并行任务同步 等待所有线程到达指定点
读写锁 读多写少的资源访问 读共享,写独占
信号量 控制并发资源访问数量 支持计数,灵活控制并发度

这些同步机制在多线程编程中各有侧重,需根据实际场景选择最合适的工具。

相关推荐
吕司2 小时前
Linux动静态库
linux·运维·服务器
123过去2 小时前
mfcuk使用教程
linux·测试工具·安全
我真会写代码2 小时前
Redis核心特性详解:事务、发布订阅与数据删除淘汰策略
java·数据库·redis
风曦Kisaki3 小时前
#Linux进阶Day05:防火墙+VMware网络+sshd远程管理
linux·运维
blueSatchel3 小时前
I2C驱动学习
linux·c语言
IT 行者3 小时前
LangChain4j 集成 Redis 向量存储:我踩过的坑和选型建议
java·人工智能·redis·后端
一定要AK3 小时前
Java流程控制
java·开发语言·笔记
321.。3 小时前
Linux 进程控制深度解析:从创建到替换的完整指南
linux·开发语言·c++·学习
tryCbest3 小时前
Java和Python开发项目部署简介
java·开发语言·python