C++-linux系统编程 9.线程——C语言&POSIX线程库版本

Linux系统编程之线程详解-C语言版

在进程之后,线程是Linux系统编程中另一个核心的并发执行单元。相比进程,线程更轻量、资源开销更低,是实现多任务并发的重要手段。本文将从线程的基本概念、核心操作、同步机制到实际应用,全面解析Linux线程编程。

一、线程的基本概念

什么是线程?

线程(Thread)是进程内的一个执行单元,是操作系统调度的基本单位。一个进程可以包含多个线程,所有线程共享进程的资源(如内存空间、文件描述符等),但拥有独立的执行路径(程序计数器、栈、寄存器)。

简单来说:进程是资源分配的基本单位,线程是调度执行的基本单位

线程与进程的核心区别

对比项进程(Process)线程(Thread)
资源分配拥有独立的地址空间、文件描述符等资源共享所属进程的资源,不独立分配资源
开销创建/销毁开销大(需分配资源)创建/销毁开销小(仅需栈和寄存器)
通信方式依赖IPC机制(管道、共享内存等)可直接访问共享内存(全局变量等)
调度粒度较粗(进程切换需保存完整资源状态)较细(仅需保存线程私有状态)
独立性进程崩溃不影响其他进程线程崩溃可能导致整个进程崩溃

线程的优势

  1. 轻量级并发:创建和销毁线程的开销远小于进程,适合频繁创建销毁的场景。
  2. 高效通信:线程共享进程内存空间,通过全局变量即可通信,无需复杂IPC机制。
  3. 资源利用率高:多线程可充分利用多核CPU,提高程序吞吐量(如服务器同时处理多个请求)。
  4. 响应性提升:在IO密集型任务中,一个线程等待IO时,其他线程可继续执行(如GUI程序避免界面卡顿)。

二、线程的核心操作(基于POSIX线程库)

Linux下线程编程主要依赖POSIX线程库(pthread),提供了创建、终止、同步等核心接口。使用时需包含头文件 <pthread.h>,编译时需链接线程库(-lpthread 选项)。

1. 线程创建(pthread_create)

函数原型
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine)(void *), void *arg);
参数说明
  • thread:传出参数,用于存储新创建线程的ID。
  • attr:线程属性(如分离态、栈大小),NULL 表示使用默认属性。
  • start_routine:线程入口函数(函数指针),线程创建后会执行该函数。
  • arg:传递给线程入口函数的参数。
返回值
  • 成功:返回 0;失败:返回非0错误码(注意:不是设置 errno)。
示例代码
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

// 线程入口函数
void *thread_func(void *arg) {
    char *msg = (char *)arg;
    printf("子线程:%s(线程ID:%lu)\n", msg, pthread_self()); // pthread_self()获取当前线程ID
    sleep(2); // 模拟线程工作
    return (void *)123; // 线程退出返回值
}

int main() {
    pthread_t tid; // 线程ID
    char *msg = "Hello from main";

    // 创建线程
    int ret = pthread_create(&tid, NULL, thread_func, msg);
    if (ret != 0) {
        fprintf(stderr, "线程创建失败:%d\n", ret);
        return 1;
    }

    printf("主线程:已创建子线程(ID:%lu)\n", tid);
    sleep(3); // 等待子线程执行完毕
    return 0;
}
编译运行
gcc thread_demo.c -o thread_demo -lpthread  # 必须链接pthread库
./thread_demo
输出
主线程:已创建子线程(ID:140561126704896)
子线程:Hello from main(线程ID:140561126704896)

2. 线程终止与资源回收

线程终止有三种常见方式:

  1. 线程入口函数执行完毕并返回(正常终止)。
  2. 线程调用 pthread_exit() 主动退出。
  3. 其他线程调用 pthread_cancel() 强制终止该线程。
(1)线程主动退出(pthread_exit)
#include <pthread.h>
void pthread_exit(void *retval);
  • retval:线程退出返回值,可被 pthread_join() 捕获。

示例:

void *thread_func(void *arg) {
    printf("子线程:执行中...\n");
    pthread_exit((void *)456); // 主动退出,返回456
}
(2)等待线程终止并回收资源(pthread_join)

类似于进程的 wait()pthread_join() 用于主线程等待子线程终止并获取返回值,避免线程资源泄漏。

函数原型:

int pthread_join(pthread_t thread, void **retval);
  • thread:要等待的线程ID。
  • retval:传出参数,用于存储线程的退出返回值(pthread_exit 或函数返回值)。

示例:

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);

    void *ret;
    pthread_join(tid, &ret); // 阻塞等待子线程结束
    printf("子线程退出返回值:%d\n", (int)ret); // 输出456
    return 0;
}

注意:若不调用 pthread_join() 且未设置线程为分离态,线程终止后会成为“僵尸线程”,占用系统资源。

(3)线程分离(pthread_detach)

对于无需等待退出状态的线程,可设置为分离态(Detached),线程终止后资源会自动回收,无需 pthread_join()

函数原型:

int pthread_detach(pthread_t thread);

示例(两种设置方式):

// 方式1:创建时通过属性设置分离态
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置分离态
pthread_create(&tid, &attr, thread_func, NULL);
pthread_attr_destroy(&attr);

// 方式2:创建后调用pthread_detach
pthread_create(&tid, NULL, thread_func, NULL);
pthread_detach(tid); // 标记为分离态

3. 强制终止线程(pthread_cancel)

强制终止指定线程,需线程本身可被取消(默认允许取消)。

函数原型:

int pthread_cancel(pthread_t thread);

示例:

void *thread_func(void *arg) {
    while (1) {
        printf("子线程:运行中...\n");
        sleep(1);
    }
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    sleep(3); // 让子线程运行3秒
    pthread_cancel(tid); // 强制终止子线程
    pthread_join(tid, NULL); // 回收资源
    return 0;
}

注意pthread_cancel() 并非立即终止线程,而是向线程发送取消请求,线程会在“取消点”(如系统调用、pthread_testcancel())处响应并终止。

三、线程的共享与私有资源

线程与进程的核心区别在于资源共享,明确哪些资源共享、哪些私有是线程编程的基础。

1. 共享资源(线程间可见)

  • 进程地址空间:代码段、数据段、堆(全局变量、静态变量、动态分配的内存 malloc)。
  • 文件描述符表:打开的文件、管道、套接字等。
  • 信号处理函数:进程级的信号处理配置。
  • 当前工作目录用户ID/组ID 等进程属性。
  • 系统资源限制信号掩码

2. 私有资源(线程独有)

  • 线程ID(TID):用于标识线程(pthread_self() 获取)。
  • 栈空间:每个线程有独立的栈,存储局部变量和函数调用上下文。
  • 寄存器状态:程序计数器(PC)、栈指针(SP)等,确保线程独立执行。
  • 线程特有数据(TSD):通过 pthread_key_create 等函数创建的线程私有数据。
  • errno变量:现代Linux中每个线程有独立的 errno 副本(避免多线程干扰)。

示例:共享全局变量

#include <pthread.h>
#include <stdio.h>

int global_count = 0; // 全局变量,线程共享

void *thread_inc(void *arg) {
    for (int i = 0; i < 10000; i++) {
        global_count++; // 线程1修改全局变量
    }
    return NULL;
}

void *thread_dec(void *arg) {
    for (int i = 0; i < 10000; i++) {
        global_count--; // 线程2修改全局变量
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, thread_inc, NULL);
    pthread_create(&tid2, NULL, thread_dec, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("最终global_count:%d\n", global_count); // 预期0,但实际可能非0?
    return 0;
}

问题:上述代码运行后 global_count 可能不为0,因为 global_count++ 不是原子操作(本质是“读取-修改-写入”三步,可能被其他线程打断),导致竞态条件(Race Condition)。这就是线程同步问题的根源。

四、线程同步:解决竞态条件

当多个线程并发访问共享资源时,若操作非原子,会导致数据不一致(如上述计数器问题)。线程同步机制通过控制资源访问顺序,确保操作的原子性和正确性。

1. 互斥锁(Mutex):最常用的同步工具

互斥锁(Mutual Exclusion)通过“加锁-操作-解锁”流程,确保同一时间只有一个线程访问共享资源。

核心操作函数
函数功能
pthread_mutex_init()初始化互斥锁
pthread_mutex_lock()加锁(若已锁则阻塞等待)
pthread_mutex_unlock()解锁(唤醒等待的线程)
pthread_mutex_destroy()销毁互斥锁
示例:用互斥锁解决计数器问题
#include <pthread.h>
#include <stdio.h>

int global_count = 0;
pthread_mutex_t mutex; // 声明互斥锁

void *thread_inc(void *arg) {
    for (int i = 0; i < 10000; i++) {
        pthread_mutex_lock(&mutex); // 加锁
        global_count++; // 临界区操作(原子化)
        pthread_mutex_unlock(&mutex); // 解锁
    }
    return NULL;
}

void *thread_dec(void *arg) {
    for (int i = 0; i < 10000; i++) {
        pthread_mutex_lock(&mutex); // 加锁
        global_count--; // 临界区操作
        pthread_mutex_unlock(&mutex); // 解锁
    }
    return NULL;
}

int main() {
    pthread_mutex_init(&mutex, NULL); // 初始化互斥锁

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, thread_inc, NULL);
    pthread_create(&tid2, NULL, thread_dec, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("最终global_count:%d\n", global_count); // 正确输出0

    pthread_mutex_destroy(&mutex); // 销毁互斥锁
    return 0;
}

关键global_count++ 被包裹在 pthread_mutex_lockpthread_mutex_unlock 之间,成为“临界区”,确保同一时间只有一个线程执行,避免竞态条件。

2. 条件变量(Condition Variable):线程间协作

条件变量是线程间基于特定条件的同步机制,常用于线程间的“等待-通知”场景(如生产者-消费者模型)。它必须与互斥锁配合使用,确保条件判断和操作的原子性。

完整示例:生产者-消费者模型
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define BUFFER_SIZE 5  // 缓冲区大小
int buffer[BUFFER_SIZE];  // 共享缓冲区
int in = 0, out = 0;      // 生产者/消费者索引(环形缓冲区)

// 同步工具
pthread_mutex_t mutex;
pthread_cond_t not_full;  // 缓冲区未满条件(生产者等待)
pthread_cond_t not_empty; // 缓冲区非空条件(消费者等待)

// 生产者线程:向缓冲区添加数据
void *producer(void *arg) {
    for (int i = 0; i < 10; i++) {  // 生产10个数据
        int data = rand() % 100;    // 随机生成数据
        sleep(1);                   // 模拟生产耗时

        pthread_mutex_lock(&mutex);  // 加锁保护缓冲区

        // 若缓冲区满,等待"缓冲区未满"信号(防止虚假唤醒,用while循环)
        while ((in + 1) % BUFFER_SIZE == out) {
            printf("生产者:缓冲区满,等待消费者取数据...\n");
            // 阻塞等待not_full信号,同时自动释放mutex锁
            pthread_cond_wait(&not_full, &mutex);
            // 被唤醒后重新获取mutex锁,需再次检查条件
        }

        // 向缓冲区放入数据
        buffer[in] = data;
        printf("生产者:放入数据 %d(位置:%d)\n", data, in);
        in = (in + 1) % BUFFER_SIZE;  // 更新生产者索引

        pthread_cond_signal(&not_empty);  // 通知消费者:缓冲区非空
        pthread_mutex_unlock(&mutex);     // 解锁
    }
    return NULL;
}

// 消费者线程:从缓冲区取数据
void *consumer(void *arg) {
    for (int i = 0; i < 10; i++) {
        sleep(2);  // 模拟消费耗时

        pthread_mutex_lock(&mutex);  // 加锁保护缓冲区

        // 若缓冲区空,等待"缓冲区非空"信号
        while (in == out) {
            printf("消费者:缓冲区空,等待生产者放数据...\n");
            pthread_cond_wait(&not_empty, &mutex);  // 释放锁并阻塞
        }

        // 从缓冲区取数据
        int data = buffer[out];
        printf("消费者:取出数据 %d(位置:%d)\n", data, out);
        out = (out + 1) % BUFFER_SIZE;  // 更新消费者索引

        pthread_cond_signal(&not_full);   // 通知生产者:缓冲区未满
        pthread_mutex_unlock(&mutex);     // 解锁
    }
    return NULL;
}

int main() {
    // 初始化同步工具
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_full, NULL);
    pthread_cond_init(&not_empty, NULL);

    // 创建生产者和消费者线程
    pthread_t prod_tid, cons_tid;
    pthread_create(&prod_tid, NULL, producer, NULL);
    pthread_create(&cons_tid, NULL, consumer, NULL);

    // 等待线程结束
    pthread_join(prod_tid, NULL);
    pthread_join(cons_tid, NULL);

    // 销毁同步工具
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_full);
    pthread_cond_destroy(&not_empty);

    return 0;
}
条件变量核心要点
  • 配合互斥锁使用:条件变量必须与互斥锁结合,确保条件判断和操作的原子性。
  • while循环检查条件pthread_cond_wait 可能被“虚假唤醒”(无信号却唤醒),需用 while 而非 if 循环检查条件,确保被唤醒后条件确实满足。
  • 自动释放锁pthread_cond_wait 阻塞时会自动释放互斥锁,被唤醒后重新获取锁,避免死锁。
  • 唤醒机制pthread_cond_signal 唤醒一个等待线程,pthread_cond_broadcast 唤醒所有等待线程(适合多个线程等待同一条件)。

3. 信号量(Semaphore):多资源同步

信号量是比互斥锁更灵活的同步工具,本质是一个计数器,通过 P(等待)V(释放) 操作控制资源访问,支持多个线程同时访问有限资源(如限制并发数)。

核心操作函数(POSIX信号量)
函数功能
sem_init(sem_t *sem, int pshared, unsigned int value)初始化信号量。pshared=0 表示线程间共享,value 为初始计数。
sem_wait(sem_t *sem)P操作:计数器减1,若计数≤0则阻塞等待。
sem_post(sem_t *sem)V操作:计数器加1,若有线程阻塞则唤醒。
sem_destroy(sem_t *sem)销毁信号量。
示例:限制并发线程数
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <unistd.h>

#define MAX_CONCURRENT 2  // 最大并发线程数
sem_t sem;

void *worker(void *arg) {
    int id = *(int *)arg;
    free(arg);

    sem_wait(&sem);  // P操作:获取资源(计数器-1)
    printf("线程 %d:开始执行(当前并发数:%d)\n", id, MAX_CONCURRENT - sem_trywait(&sem));
    sleep(3);  // 模拟任务耗时
    printf("线程 %d:执行完毕\n", id);
    sem_post(&sem);  // V操作:释放资源(计数器+1)

    return NULL;
}

int main() {
    sem_init(&sem, 0, MAX_CONCURRENT);  // 初始化信号量,初始计数=2

    pthread_t tids[5];
    for (int i = 0; i < 5; i++) {
        int *id = malloc(sizeof(int));
        *id = i;
        pthread_create(&tids[i], NULL, worker, id);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(tids[i], NULL);
    }

    sem_destroy(&sem);
    return 0;
}

输出说明:同一时间最多有2个线程执行“任务”,其他线程需等待前序线程释放信号量后才能执行,实现了并发数限制。

4. 读写锁(Read-Write Lock):读多写少场景优化

读写锁(pthread_rwlock)专为“读多写少”场景设计,允许多个读者同时读取共享资源,但写者必须独占资源(读写互斥、写写互斥),比互斥锁更高效。

核心操作函数
函数功能
pthread_rwlock_init()初始化读写锁。
pthread_rwlock_rdlock()获取读锁(多个读者可同时持有)。
pthread_rwlock_wrlock()获取写锁(独占,需等待所有读锁释放)。
pthread_rwlock_unlock()释放读锁或写锁。
pthread_rwlock_destroy()销毁读写锁。
示例:读写锁保护共享数据
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int shared_data = 0;
pthread_rwlock_t rwlock;

// 读者线程:读取数据
void *reader(void *arg) {
    int id = *(int *)arg;
    while (1) {
        pthread_rwlock_rdlock(&rwlock);  // 获取读锁
        printf("读者 %d:读取数据 = %d(当前可并发读)\n", id, shared_data);
        sleep(1);  // 模拟读操作耗时
        pthread_rwlock_unlock(&rwlock);  // 释放读锁
        sleep(1);  // 间隔一段时间再次读取
    }
    return NULL;
}

// 写者线程:更新数据
void *writer(void *arg) {
    int id = *(int *)arg;
    while (1) {
        pthread_rwlock_wrlock(&rwlock);  // 获取写锁(需等待所有读锁释放)
        shared_data++;
        printf("===== 写者 %d:更新数据 = %d(独占访问) =====\n", id, shared_data);
        sleep(2);  // 模拟写操作耗时
        pthread_rwlock_unlock(&rwlock);  // 释放写锁
        sleep(3);  // 间隔一段时间再次更新
    }
    return NULL;
}

int main() {
    pthread_rwlock_init(&rwlock, NULL);

    pthread_t readers[3], writers[1];
    int rids[3] = {1, 2, 3}, wid = 1;

    // 创建3个读者线程
    for (int i = 0; i < 3; i++) {
        pthread_create(&readers[i], NULL, reader, &rids[i]);
    }
    // 创建1个写者线程
    pthread_create(&writers[0], NULL, writer, &wid);

    // 等待线程(实际中用信号退出,此处简化)
    for (int i = 0; i < 3; i++) {
        pthread_join(readers[i], NULL);
    }
    pthread_join(writers[0], NULL);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

输出特点:多个读者可同时读取数据(打印信息重叠),写者更新时会独占资源(读者需等待写锁释放后才能继续读)。

5. 自旋锁(Spin Lock):短时间锁场景优化

自旋锁与互斥锁功能类似,但等待锁时不会阻塞睡眠,而是循环尝试获取锁(“自旋”),适合锁持有时间极短的场景(如内核态操作),避免线程上下文切换开销。

核心操作函数
函数功能
pthread_spin_init(pthread_spinlock_t *lock, int pshared)初始化自旋锁,pshared=0 表示线程间共享。
pthread_spin_lock(pthread_spinlock_t *lock)获取自旋锁,若未获取则循环等待。
pthread_spin_unlock(pthread_spinlock_t *lock)释放自旋锁。
pthread_spin_destroy(pthread_spinlock_t *lock)销毁自旋锁。
注意事项
  • 适用场景:锁持有时间短(如几微秒),且CPU核心数充足(避免单核心下自旋浪费资源)。
  • 禁用阻塞操作:持有自旋锁时不能调用 sleeppthread_cond_wait 等阻塞函数,否则会导致其他线程自旋等待,浪费CPU。
  • 避免递归:自旋锁不支持递归获取(同一线程多次获取会死锁)。

五、线程特有数据(Thread-Specific Data, TSD)

线程特有数据(TSD)用于线程需要私有数据但无法通过函数参数传递的场景(如每个线程有独立的日志句柄、内存分配器)。TSD通过“键-值”映射实现,每个线程可通过全局键访问自己的私有值。

核心操作函数
函数功能
pthread_key_create(pthread_key_t *key, void (*destructor)(void *))创建TSD键,destructor 为线程退出时的清理函数。
pthread_setspecific(pthread_key_t key, const void *value)为当前线程设置键对应的私有值。
pthread_getspecific(pthread_key_t key)获取当前线程键对应的私有值。
pthread_key_delete(pthread_key_t key)删除TSD键(不调用清理函数,需手动释放资源)。
示例:线程私有日志句柄
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

pthread_key_t log_key;  // TSD全局键

// 日志句柄清理函数:线程退出时关闭文件
void log_destructor(void *value) {
    FILE *fp = (FILE *)value;
    if (fp) {
        fclose(fp);
        printf("线程 %lu:日志文件关闭\n", pthread_self());
    }
}

// 初始化TSD键
void init_log_key() {
    pthread_key_create(&log_key, log_destructor);
}

// 线程日志函数:每个线程写入自己的日志文件
void thread_log(const char *msg) {
    FILE *fp = (FILE *)pthread_getspecific(log_key);
    if (!fp) {
        // 首次调用:为当前线程创建日志文件
        char filename[20];
        sprintf(filename, "thread_%lu.log", pthread_self());
        fp = fopen(filename, "a");
        pthread_setspecific(log_key, fp);  // 绑定到TSD键
    }
    fprintf(fp, "%s\n", msg);
    fflush(fp);
}

// 线程函数:写入日志
void *thread_func(void *arg) {
    thread_log("线程开始执行");
    thread_log("处理任务中...");
    thread_log("线程即将退出");
    return NULL;  // 退出时自动调用log_destructor关闭文件
}

int main() {
    init_log_key();

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, thread_func, NULL);
    pthread_create(&tid2, NULL, thread_func, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_key_delete(log_key);  // 删除TSD键
    return 0;
}

效果:两个线程分别写入自己的日志文件(thread_<tid>.log),线程退出时自动关闭文件,实现了数据隔离。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值