一、多线程编程基础

1.1 什么是线程

线程是操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位。一个进程可以包含多个线程,这些线程共享进程的资源(如内存空间、文件描述符等),但拥有各自的栈空间和寄存器状态。

1.2 多线程的优势

  • 提高响应性:主线程保持响应,后台线程处理耗时任务
  • 提高资源利用率:充分利用多核CPU的计算能力
  • 经济高效:创建线程比创建进程开销小
  • 简化复杂任务:将复杂任务分解为多个线程协同处理

1.3 pthread库简介

POSIX线程(pthread)是C/C++中实现多线程的标准API,在Linux/Unix系统中广泛使用。使用时需包含头文件<pthread.h>,编译时需添加-pthread选项。

gcc program.c -o program -pthread

pthread选项的作用

1.链接POSIX线程库,但是在新的Linux发行版中已经被合并进了libc主库。

2.会在预处理阶段自动定义一个宏:_REENTRANT,这个宏告诉C标准库的头文件程序运行在多线环境下,告诉链接器要链接**libpthread.so**。

二、核心线程管理函数

2.1 创建线程 - pthread_create

int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr,
                   void *(*start_routine)(void *), 
                   void *arg);

参数说明:

  • thread:指向线程标识符的指针
  • attr:设置线程属性,通常设为NULL
  • start_routine:线程运行的函数指针
  • arg:传递给线程函数的参数

返回值: 成功返回0,失败返回错误码

示例代码:

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

void* print_message(void *msg) {
    char *message = (char*)msg;
    printf("%s\n", message);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;
    char *msg1 = "Thread 1";
    char *msg2 = "Thread 2";
    
    pthread_create(&thread1, NULL, print_message, (void*)msg1);
    pthread_create(&thread2, NULL, print_message, (void*)msg2);
    
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    
    return 0;
}

2.2 等待线程结束 - pthread_join

int pthread_join(pthread_t thread, void **retval);

功能:让当前线程阻塞,直到调用线程执行完毕。

参数说明:

  • thread:要等待的线程ID
  • retval:存储线程返回值的指针,可为NULL

返回值: 成功返回0,失败返回错误码

注意事项:

  • 调用线程将阻塞,直到目标线程结束
  • 每个线程只能被一个线程join一次
  • 未join的线程会产生"僵尸线程",浪费系统资源

2.3 分离线程 - pthread_detach

int pthread_detach(pthread_t thread);

功能: 将线程设置为分离状态,线程结束后自动回收资源

使用场景: 不需要获取线程返回值,也不关心线程何时结束时使用

注意事项:

  • 分离状态的线程不能被join
  • 如果线程已结束,分离操作可能失败
  • 主线程退出后,分离线程也会终止

2.4 线程终止 - pthread_exit

void pthread_exit(void *retval);

功能: 显式终止当前线程,并返回一个值

注意事项:

  • 主线程调用pthread_exit不会导致进程退出
  • 线程函数执行return语句也会隐式调用pthread_exit

三、线程同步机制

3.1 互斥锁(Mutex)

初始化与销毁
// 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 动态初始化
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_trylock(pthread_mutex_t *mutex); // 非阻塞加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);  // 解锁
  • **阻塞枷锁:**若锁被其他线程占用,当前线程休眠阻塞,放入锁的等待队列;CPU 切走,不消耗 CPU;等到锁释放后,内核唤醒本线程,拿到锁后返回。阻塞一定会拿到锁,会发生上下文切换。适用于保护临界区,必须访问共享资源。

  • **非阻塞加锁:**锁被占用:不阻塞、不休眠,立刻返回失败,线程继续往下执行。非阻塞可能拿不到锁,不会发生上下文切换。适用于不想卡死,做别的任务;避免死锁检测。

互斥锁示例:线程安全计数器
#include <pthread.h>

int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

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_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);
    
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    
    printf("Final counter value: %d\n", counter);
    return 0;
}

3.2 条件变量(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_destroy(pthread_cond_t *cond);
等待与通知
//调用 pthread_cond_wait(cond, mutex) 的那个线程,会把自己加入这个 cond 对应的等待队列。
int pthread_cond_wait(pthread_cond_t *cond, 
                      pthread_mutex_t *mutex);
                      
int pthread_cond_signal(pthread_cond_t *cond);   // 唤醒一个等待线程
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有等待线程

条件变量必须搭配mutex使用,wait调用前该线程必须持有锁。

条件变量示例:生产者-消费者模型
#define BUFFER_SIZE 10

int buffer[BUFFER_SIZE];
int count = 0;
int in = 0, out = 0;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_empty = PTHREAD_COND_INITIALIZER;

void* producer(void *arg) {
    for (int i = 0; i < 20; i++) {
        pthread_mutex_lock(&mutex);
        
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_empty, &mutex);
        }
        
        buffer[in] = i;
        in = (in + 1) % BUFFER_SIZE;
        count++;
        
        pthread_cond_signal(&cond_full);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void* consumer(void *arg) {
    for (int i = 0; i < 20; i++) {
        pthread_mutex_lock(&mutex);
        
        while (count == 0) {
            pthread_cond_wait(&cond_full, &mutex);
        }
        
        int item = buffer[out];
        out = (out + 1) % BUFFER_SIZE;
        count--;
        
        printf("Consumed: %d\n", item);
        pthread_cond_signal(&cond_empty);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

四、高级线程技术

4.1 线程属性设置

pthread_attr_t attr;
pthread_attr_init(&attr);

// 设置分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

// 设置栈大小
size_t stack_size = 1024 * 1024; // 1MB
pthread_attr_setstacksize(&attr, stack_size);

// 使用属性创建线程
pthread_t thread;
pthread_create(&thread, &attr, thread_function, NULL);

// 销毁属性对象
pthread_attr_destroy(&attr);

4.2 线程局部存储(Thread-Local Storage)

线程局部存储,就是变量,逻辑上全局,但每个线程拥有该变量独立的副本 。A 线程修改它,不会影响 B 线程;线程之间互相隔离。

#include <pthread.h>

static __thread int tls_var;  // GCC扩展语法

// POSIX标准方法
pthread_key_t key;	//全局变量

// 销毁回调:线程退出的时候,如果key关联了value,调用这个析构函数
void destructor(void *value) {
    free(value);
}

void init_key() {
    // 创建一个key,注册析构回调
    pthread_key_create(&key, destructor);
}

void* thread_func(void *arg) {
    int *data = malloc(sizeof(int));
    *data = pthread_self();

    // 设置本线程下,这个key对应的值(void*指针)
    pthread_setspecific(key, data);
    
    // 获取当前线程,key对应的指针
    int *value = pthread_getspecific(key);
    printf("Thread %lu: value=%d\n", pthread_self(), *value);
    
    return NULL;
}

4.3 线程取消

作用:一个线程可以向另一个线程发送「取消请求」,请求对方终止执行,不是直接暴力杀死线程。

// 向目标线程发送取消请求
int pthread_cancel(pthread_t thread);

// 设置是否接受取消请求
int pthread_setcancelstate(int state, int *oldstate);
// state: PTHREAD_CANCEL_ENABLE 接收取消请求, PTHREAD_CANCEL_DISABLE 拒绝取消请求

// 设置取消类型,收到请求后什么时候响应
int pthread_setcanceltype(int type, int *oldtype);
// type: PTHREAD_CANCEL_DEFERRED 默认延迟取消(到取消点取消), PTHREAD_CANCEL_ASYNCHRONOUS 异步取消(随时可能终止)

// 设置取消点
void pthread_testcancel(void);

五、多线程编程最佳实践

  1. 避免竞态条件:始终使用同步机制保护共享资源
  2. 最小化锁的粒度:减少锁的持有时间,避免嵌套锁
  3. 防止死锁:按固定顺序获取锁,使用超时机制
  4. 优先使用读写锁:当读操作远多于写操作时
  5. 合理设置线程数:通常为CPU核心数的1-2倍
  6. 避免频繁创建销毁:使用线程池管理线程
  7. 检查返回值:所有pthread函数都应检查返回值

六、完整示例:多线程计算素数

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

#define NUM_THREADS 4
#define MAX_NUMBER 1000000

typedef struct {
    int start;
    int end;
    int count;
} ThreadData;

bool is_prime(int n) {
    if (n <= 1) return false;
    if (n == 2) return true;
    if (n % 2 == 0) return false;
    
    for (int i = 3; i * i <= n; i += 2) {
        if (n % i == 0) {
            return false;
        }
    }
    return true;
}

void* count_primes(void *arg) {
    ThreadData *data = (ThreadData*)arg;
    data->count = 0;
    
    for (int i = data->start; i <= data->end; i++) {
        if (is_prime(i)) {
            data->count++;
        }
    }
    
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    ThreadData data[NUM_THREADS];
    
    int range = MAX_NUMBER / NUM_THREADS;
    int total_primes = 0;
    
    // 创建线程
    for (int i = 0; i < NUM_THREADS; i++) {
        data[i].start = i * range + 1;
        data[i].end = (i == NUM_THREADS - 1) ? MAX_NUMBER : (i + 1) * range;
        pthread_create(&threads[i], NULL, count_primes, &data[i]);
    }
    
    // 等待线程完成并汇总结果
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
        total_primes += data[i].count;
        printf("Thread %d found %d primes in [%d, %d]\n", 
               i, data[i].count, data[i].start, data[i].end);
    }
    
    printf("\nTotal primes between 1 and %d: %d\n", MAX_NUMBER, total_primes);
    return 0;
}

七、常见问题与调试技巧

  1. 段错误(Segmentation Fault)

    • 检查线程栈是否溢出(可能诱因:函数内部定义巨大局部数组)
    • 确保共享数据访问受到保护(可能诱因:多个线程同时写同一个全局结构体)
    • 避免返回局部变量的指针(可能诱因:指针释放后继续使用(野指针))
  2. 死锁检测(现象:程序卡住不动,CPU 很低,线程全部阻塞,没有业务输出。)

    • 使用gdb调试:thread apply all bt # 打印所有线程调用栈
    gdb ./your_program
    (gdb) run
    # 程序卡死之后 Ctrl+C打断程序
    (gdb) info threads        # 列出所有线程
    (gdb) thread apply all bt # 打印**所有线程**调用栈,重点!
    (gdb) q				      # 退出调试
    
    • 使用helgrind工具检测数据竞争
    valgrind --tool=helgrind ./your_program
    
  3. 性能分析

    • 使用perf工具分析CPU使用情况
    perf record -g ./your_program
    perf report
    
    • 使用pthread_mutex_timedlock检测锁争用
  4. 线程安全函数

    • 使用可重入函数(函数可以被中断,再次进入调用,运行结果正确,如rand_r替代rand
    • 线程安全(多线程调用这个函数不会产生bug,可重入一定线程安全,反过来不一定)
    • 避免使用非线程安全的库函数(函数内部使用静态全局变量

总结

C语言的多线程编程是高性能计算和并发处理的关键技术。掌握pthread库的核心函数、理解线程同步机制、遵循多线程最佳实践,能够帮助开发者构建高效可靠的并发程序。在实际开发中,应特别注意资源共享和线程安全的问题,合理使用同步原语避免竞态条件和死锁。

通过本文的学习,您应该已经掌握了:

  • 线程的创建、管理和终止
  • 互斥锁和条件变量的使用
  • 生产者-消费者等经典线程模型
  • 线程属性和高级线程技术
  • 多线程调试和优化方法

C语言多线程编程详解:从入门到实战 - Rare_30 - 博客园

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐