一、核心知识点介绍
-
线程概念
- 线程是进程内的一个执行流,是操作系统进行 CPU 调度的基本单位。
- 同一个进程内的多个线程共享进程的地址空间(代码段、数据段、堆、打开的文件描述符等)。
- 每个线程拥有自己独立的栈空间、程序计数器 (PC) 和线程本地存储 (TLS)。
- 线程比进程更轻量级,创建和切换开销更小,更适用于并发任务。
-
POSIX 线程 (pthreads)
- Linux 主要使用 POSIX 线程标准(
pthread)来实现线程编程。 - 相关的函数和数据类型都定义在头文件
<pthread.h>中。 - 编译时需要链接
pthread库,通常使用-pthread或-lpthread编译选项。
- Linux 主要使用 POSIX 线程标准(
-
线程创建与终止
- 创建线程:
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);显式退出。 - 主线程调用
exit或main函数返回会导致整个进程终止,所有线程也随之结束(不建议)。
- 线程函数
- 分离线程:
int pthread_detach(pthread_t thread);- 将线程标记为分离状态。分离状态的线程终止时,其资源会自动回收,无需其他线程调用
pthread_join。 - 不能在已分离的线程上调用
pthread_join。
- 将线程标记为分离状态。分离状态的线程终止时,其资源会自动回收,无需其他线程调用
- 创建线程:
-
线程同步
多个线程并发访问共享资源时,需要同步机制来保证数据的一致性和避免竞态条件。
- 互斥锁 (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);
- 互斥锁 (Mutex):
-
线程安全与可重入
- 线程安全: 一个函数或代码段可以被多个线程同时调用而不会产生错误结果。
- 可重入: 一个函数可以在其自身尚未执行完毕时再次被调用(例如信号处理函数调用它)。可重入函数必然是线程安全的,但线程安全函数不一定是可重入的(例如使用全局静态数据的线程安全函数)。
- 编写线程安全代码的关键:避免使用全局或静态变量,或使用同步机制保护对它们的访问;使用局部变量或线程本地存储 (TLS)。
-
线程取消
- 一个线程可以请求取消另一个线程 (
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;
}
三、注意事项
- 错误处理: 始终检查
pthread_*函数的返回值,它们失败时会返回非零错误码(可通过strerror查看)。 - 资源泄露: 确保创建的线程被正确等待 (
pthread_join) 或分离 (pthread_detach),避免僵尸线程。动态初始化的互斥锁和条件变量需要销毁 (pthread_mutex_destroy,pthread_cond_destroy)。 - 死锁: 仔细设计锁的获取顺序,避免循环等待。
- 竞态条件: 使用同步机制(互斥锁、条件变量)保护所有对共享资源的访问。
- 虚假唤醒: 在使用
pthread_cond_wait时,必须在循环中检查条件,因为即使没有收到信号,等待也可能返回。 - 性能: 锁操作有开销,尽量减少临界区的范围。考虑使用读写锁 (
pthread_rwlock_t) 或原子操作 (C11_Atomic或 GCC__sync_*,__atomic_*内置函数) 优化特定场景。
1. 线程属性
线程属性(Thread Attributes)用于在创建线程时配置其行为。通过设置属性,可以控制线程的栈大小、调度策略、是否可分离等特性。
关键属性:
- 栈大小 :
pthread_attr_setstacksize()可设置线程栈大小。 - 调度策略 :
pthread_attr_setschedpolicy()设置调度策略(如SCHED_FIFO、SCHED_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)。 - POSIX :
pthread_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 | 多阶段并行任务同步 | 等待所有线程到达指定点 |
| 读写锁 | 读多写少的资源访问 | 读共享,写独占 |
| 信号量 | 控制并发资源访问数量 | 支持计数,灵活控制并发度 |
这些同步机制在多线程编程中各有侧重,需根据实际场景选择最合适的工具。