ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux多线程编程:从基础到线程池实现

Linux多线程编程:从基础到线程池实现

1. Linux线程控制基础

在Linux系统中,线程是程序执行的最小单位,而进程则是资源分配的最小单位。理解线程控制是掌握多线程编程的基础,我们先来看一个最基本的线程创建示例:

#include <pthread.h> #include <stdio.h> void* thread_func(void* arg) { printf("New thread created\n"); return NULL; } int main() { pthread_t tid; int ret = pthread_create(&tid, NULL, thread_func, NULL); if(ret != 0) { perror("pthread_create failed"); return 1; } pthread_join(tid, NULL); return 0; }

这个简单例子展示了线程创建的基本流程,但实际开发中我们需要考虑更多细节。pthread_create函数的四个参数分别是:

  1. 线程ID指针
  2. 线程属性(通常设为NULL使用默认值)
  3. 线程函数
  4. 传递给线程函数的参数

注意:创建线程后必须调用pthread_join或pthread_detach,否则会导致资源泄漏。join会阻塞当前线程直到目标线程结束,而detach则让线程在结束后自动释放资源。

1.1 线程属性控制

线程属性可以通过pthread_attr_t结构体进行精细控制,以下是常见属性的设置示例:

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 tid; pthread_create(&tid, &attr, thread_func, NULL); pthread_attr_destroy(&attr);

实际项目中,合理设置线程属性可以避免许多潜在问题。比如在嵌入式系统中,栈大小需要特别关注,过小会导致栈溢出,过大则浪费内存。

2. 线程终止的三种方式

线程终止不是简单地让它停止运行,而是需要确保资源被正确释放。Linux线程有三种终止方式:

2.1 从线程函数返回

这是最推荐的终止方式,线程函数执行完毕自然退出:

void* thread_func(void* arg) { // 线程工作代码 return (void*)42; // 返回退出码 }

返回的值可以通过pthread_join获取:

void* retval; pthread_join(tid, &retval); printf("Thread exited with code %d\n", (int)retval);

2.2 调用pthread_exit

线程可以在任何地方调用pthread_exit主动退出:

void* thread_func(void* arg) { if(error_condition) { pthread_exit((void*)1); // 提前退出 } // 正常执行 pthread_exit((void*)0); }

警告:在主线程中调用pthread_exit会导致程序继续运行直到所有线程结束,这可能不是你想要的行为。

2.3 被其他线程取消

线程可以被同一进程中的其他线程取消:

pthread_cancel(tid); // 请求取消线程

被取消的线程需要在适当的位置设置取消点:

void* thread_func(void* arg) { // 设置取消类型 pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); while(1) { // 这是一个取消点 pthread_testcancel(); // 工作代码 } return NULL; }

取消线程是危险操作,可能导致资源泄漏,应该尽量避免使用。

3. 线程异常问题处理

多线程环境下的异常处理比单线程复杂得多,下面介绍几种常见问题及解决方案。

3.1 竞态条件

当多个线程同时访问共享资源时可能产生竞态条件。看这个典型例子:

int counter = 0; void* increment(void* arg) { for(int i = 0; i < 100000; i++) { counter++; // 这不是原子操作! } return NULL; }

运行两个这样的线程后,counter的值通常不会如预期的200000。解决方法是用互斥锁:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int counter = 0; void* increment(void* arg) { for(int i = 0; i < 100000; i++) { pthread_mutex_lock(&mutex); counter++; pthread_mutex_unlock(&mutex); } return NULL; }

3.2 死锁问题

死锁通常发生在多个锁的使用场景中。例如:

// 线程1 pthread_mutex_lock(&mutexA); pthread_mutex_lock(&mutexB); // 操作共享资源 pthread_mutex_unlock(&mutexB); pthread_mutex_unlock(&mutexA); // 线程2 pthread_mutex_lock(&mutexB); pthread_mutex_lock(&mutexA); // 操作共享资源 pthread_mutex_unlock(&mutexA); pthread_mutex_unlock(&mutexB);

避免死锁的几个原则:

  1. 总是以相同的顺序获取锁
  2. 使用pthread_mutex_trylock而非阻塞锁
  3. 设置锁超时时间

3.3 条件变量的正确使用

条件变量常用于线程间同步,但使用不当会导致问题:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; bool ready = false; // 等待线程 void* waiter(void* arg) { pthread_mutex_lock(&mutex); while(!ready) { pthread_cond_wait(&cond, &mutex); } // 处理条件满足的情况 pthread_mutex_unlock(&mutex); return NULL; } // 通知线程 void* notifier(void* arg) { pthread_mutex_lock(&mutex); ready = true; pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); return NULL; }

常见错误包括:

  1. 在调用pthread_cond_wait前未加锁
  2. 使用if而非while检查条件
  3. 修改条件后忘记通知

4. 线程局部存储

线程局部存储(TLS)允许每个线程拥有变量的独立副本,非常适合存储线程特定数据:

__thread int tls_var = 0; // GCC扩展语法 void* thread_func(void* arg) { tls_var = (int)(long)arg; printf("Thread %ld has tls_var = %d\n", (long)pthread_self(), tls_var); return NULL; }

POSIX标准也提供了pthread_key_create等函数实现TLS:

pthread_key_t key; void destructor(void* value) { free(value); } void init_key() { pthread_key_create(&key, destructor); } void* thread_func(void* arg) { int* data = malloc(sizeof(int)); *data = (int)(long)arg; pthread_setspecific(key, data); // 获取线程特定数据 int* value = pthread_getspecific(key); printf("Value = %d\n", *value); return NULL; }

5. 线程安全函数设计

编写线程安全函数需要遵循以下原则:

  1. 避免使用全局变量和静态变量
  2. 如果必须使用共享数据,用互斥锁保护
  3. 使用线程安全版本的库函数(如strtok_r替代strtok)
  4. 避免在函数内分配需要调用者释放的内存

一个线程安全的计数器实现示例:

typedef struct { int value; pthread_mutex_t mutex; } ThreadSafeCounter; void counter_init(ThreadSafeCounter* counter) { counter->value = 0; pthread_mutex_init(&counter->mutex, NULL); } void counter_increment(ThreadSafeCounter* counter) { pthread_mutex_lock(&counter->mutex); counter->value++; pthread_mutex_unlock(&counter->mutex); } int counter_get(ThreadSafeCounter* counter) { pthread_mutex_lock(&counter->mutex); int value = counter->value; pthread_mutex_unlock(&counter->mutex); return value; }

6. 线程池实现

直接创建销毁线程开销很大,线程池是更好的选择。下面是一个简单线程池的实现框架:

typedef struct { void (*function)(void*); void* argument; } Task; typedef struct { Task* queue; int queue_size; int head; int tail; int count; pthread_mutex_t lock; pthread_cond_t not_empty; pthread_cond_t not_full; } TaskQueue; typedef struct { pthread_t* threads; int thread_count; TaskQueue* queue; int shutdown; } ThreadPool; // 初始化线程池 ThreadPool* thread_pool_init(int thread_count, int queue_size) { ThreadPool* pool = malloc(sizeof(ThreadPool)); // 初始化代码... return pool; } // 添加任务 void thread_pool_add_task(ThreadPool* pool, void (*function)(void*), void* arg) { pthread_mutex_lock(&pool->queue->lock); // 等待队列有空位 while(pool->queue->count == pool->queue->queue_size && !pool->shutdown) { pthread_cond_wait(&pool->queue->not_full, &pool->queue->lock); } if(pool->shutdown) { pthread_mutex_unlock(&pool->queue->lock); return; } // 添加任务到队列 pool->queue->queue[pool->queue->tail].function = function; pool->queue->queue[pool->queue->tail].argument = arg; pool->queue->tail = (pool->queue->tail + 1) % pool->queue->queue_size; pool->queue->count++; pthread_cond_signal(&pool->queue->not_empty); pthread_mutex_unlock(&pool->queue->lock); } // 工作线程函数 void* worker_thread(void* arg) { ThreadPool* pool = (ThreadPool*)arg; while(1) { pthread_mutex_lock(&pool->queue->lock); // 等待任务 while(pool->queue->count == 0 && !pool->shutdown) { pthread_cond_wait(&pool->queue->not_empty, &pool->queue->lock); } if(pool->shutdown && pool->queue->count == 0) { pthread_mutex_unlock(&pool->queue->lock); pthread_exit(NULL); } // 取出任务 Task task = pool->queue->queue[pool->queue->head]; pool->queue->head = (pool->queue->head + 1) % pool->queue->queue_size; pool->queue->count--; pthread_cond_signal(&pool->queue->not_full); pthread_mutex_unlock(&pool->queue->lock); // 执行任务 (task.function)(task.argument); } return NULL; }

这个线程池实现包含了任务队列、工作线程管理和基本的同步机制,可以根据实际需求进行扩展。

7. 性能优化与调试技巧

多线程程序的性能优化和调试有其特殊性,下面分享一些实用技巧:

7.1 锁粒度控制

锁的粒度太大(锁住太多代码或数据)会降低并发性,太小则增加锁开销。一个好的经验法则是:

  • 锁持续时间不超过必要的操作
  • 锁保护的数据刚好满足需求
  • 读多写少时考虑读写锁
// 粗粒度锁 - 不推荐 pthread_mutex_lock(&global_lock); // 大量不相关的操作 pthread_mutex_unlock(&global_lock); // 细粒度锁 - 更好 pthread_mutex_lock(&data1_lock); // 只操作data1 pthread_mutex_unlock(&data1_lock); pthread_mutex_lock(&data2_lock); // 只操作data2 pthread_mutex_unlock(&data2_lock);

7.2 避免锁争用

锁争用是性能杀手,可以通过以下方式减少:

  1. 使用原子操作替代简单锁
  2. 采用无锁数据结构
  3. 分区锁(将数据分到不同锁保护的区域)
// 使用原子操作替代锁 #include <stdatomic.h> atomic_int counter = ATOMIC_VAR_INIT(0); void increment() { atomic_fetch_add(&counter, 1); }

7.3 调试工具

Linux下常用的多线程调试工具:

  1. gdb:支持多线程调试,常用命令:

    • info threads:查看所有线程
    • thread n:切换到线程n
    • thread apply all bt:查看所有线程的调用栈
  2. Valgrind:检测内存错误和竞态条件

    valgrind --tool=helgrind ./your_program
  3. strace/ltrace:跟踪系统调用和库函数调用

  4. perf:性能分析工具

    perf stat ./your_program perf record ./your_program perf report

7.4 常见陷阱

  1. 信号处理:信号是发送给整个进程的,但哪个线程执行信号处理函数是不确定的。应该:

    • 尽量避免在多线程程序中使用信号
    • 使用专门的信号处理线程(通过pthread_sigmask屏蔽其他线程的信号)
    • 考虑使用signalfd将信号转换为文件描述符事件
  2. errno问题:errno是线程安全的,但下面的代码有问题:

    if(some_call() == -1) { // 这里可能被其他线程修改errno printf("Error: %s\n", strerror(errno)); }

    应该立即保存errno:

    if(some_call() == -1) { int saved_errno = errno; printf("Error: %s\n", strerror(saved_errno)); }
  3. fork与线程:在多线程程序中调用fork,只有调用fork的线程被复制到子进程,这可能导致死锁(其他线程持有的锁在子进程中永远不会被释放)。解决方案:

    • 避免在多线程程序中使用fork
    • 如果必须使用,fork后立即调用exec
    • 使用pthread_atfork注册处理函数

8. 现代C++中的线程管理

虽然本文主要讨论POSIX线程,但现代C++(C++11及以上)提供了更高级的线程支持:

#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <atomic> #include <vector> std::mutex mtx; std::condition_variable cv; bool ready = false; std::atomic<int> counter(0); void worker(int id) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return ready; }); std::cout << "Worker " << id << " started\n"; counter++; } int main() { std::vector<std::thread> threads; for(int i = 0; i < 5; ++i) { threads.emplace_back(worker, i); } { std::lock_guard<std::mutex> lock(mtx); ready = true; } cv.notify_all(); for(auto& t : threads) { t.join(); } std::cout << "Counter: " << counter << "\n"; return 0; }

C++线程库的优点:

  1. 类型安全
  2. RAII风格管理(锁、线程等)
  3. 更高级的抽象(future/promise等)
  4. 可移植性更好

但在性能关键或需要精细控制的场景下,POSIX线程仍然是必要的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进