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 多阶段并行任务同步 等待所有线程到达指定点
读写锁 读多写少的资源访问 读共享,写独占
信号量 控制并发资源访问数量 支持计数,灵活控制并发度

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

相关推荐
XS0301062 小时前
Java 基础(七)多态
java·开发语言
杨云龙UP2 小时前
Oracle / ODA环境TRACE、alert日志定位与ADRCI清理 SOP_20260423
linux·运维·服务器·数据库·oracle
不知名的老吴2 小时前
一文读懂:单例模式的经典案例分析
java·开发语言·单例模式
REDcker2 小时前
跨平台编译详解 工具链配置与工程化实践
linux·c++·windows·macos·c·跨平台·编译
Sapphire~2 小时前
Linux-15 ubuntu 和 windows 双系统,更新系统导致丢失ubuntu 入口
linux·运维·ubuntu
yaoxin5211232 小时前
388. Java IO API - 处理事件
java·服务器·数据库
JAVA学习通2 小时前
AI 工作流编排系统的任务拆分、重试与观测:2026年工程实践深度解析
java·人工智能·spring
zzzsde2 小时前
【Linux】线程概念与控制(1)线程基础与分页式存储管理
linux·运维·服务器·开发语言·算法
小樱花的樱花2 小时前
Linux进程管理相关命令
linux·运维·服务器
计算机安禾2 小时前
【Linux从入门到精通】第13篇:磁盘管理与文件系统——数据存在哪了?
linux·运维·服务器