C风格线程的基本使用

ZaynPei Lv6

在 C 语言中,线程的创建和管理通常通过 POSIX 线程库(pthread)来实现。

线程的生命周期管理

这部分API用于“启动”和“停止”并发任务。

线程创建 (pthread_create) - 功能:这是启动一个新线程的唯一方法。你可以把它想象成“发起一个并发的函数调用”。

1
2
3
4
5
6
int pthread_create(
pthread_t * thread, // [出参] 指向线程ID的指针,用于存储新线程的句柄
const pthread_attr_t * attr, // [入参] 线程的属性(如栈大小、调度策略),NULL为默认
void * (*start_routine)(void*), // [入参] 函数指针:新线程要执行的函数, 函数签名必须是 void* func(void*). 如果不是, 需要进行适当的转换
void * arg // [入参] 传递给 start_routine 的(单个)参数, 无参时传 NULL
);
- thread:这是一个“出参”。你传入一个pthread_t变量的地址,pthread_create会把新线程的ID(句柄)填入其中。你将来会用这个ID来join(等待)该线程。 - start_routine:这是新线程的“main函数”。你告诉create:“请在一个新线程里执行这个函数”。 - 函数的返回值类型必须是void,参数类型也必须是void。这是C语言中实现“泛型函数指针”的一种方式。 - 你可以在函数内部将void参数转换为具体类型的指针,然后使用它。 - 返回的void值可以用来传递线程的结果,主线程可以通过pthread_join获取这个返回值。 - void * 和 arg:这是C语言中实现“泛型”的技巧。 - start_routine 必须是一个“接受void*参数,返回void*”的函数。 - arg 则是要传递给start_routine的那个void*参数。 - 返回值:成功返回0,失败返回错误码。

如何传递多个参数?你不能直接传, 标准做法是定义一个struct,把所有参数(如a和b)放进去,然后将这个struct的指针(&args)强制转换为void作为arg传递。线程函数(mythread)接收到void arg后,再将其强制转换回struct *来解包。

例如, 创建一个新线程来执行 threadFunc 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <pthread.h>
#include <iostream>
void* threadFunc(void* arg) {
int* num = static_cast<int*>(arg);
std::cout << "Thread number: " << *num << std::endl;
char* ch = static_cast<char*>(arg);
std::cout << "Thread character: " << *ch << std::endl;
return nullptr;
}
int main() {
pthread_t thread;
int threadArg1 = 42;
char threadArg2 = 'A';
// 将多个参数打包到一个结构体中
struct ThreadArgs {
int a;
char b;
}
ThreadArgs threadArg = {threadArg1, threadArg2};
// 创建线程,传递结构体指针作为参数
pthread_create(&thread, nullptr, threadFunc, static_cast<void*>(&threadArg));
pthread_join(thread, nullptr); // 等待线程结束
return 0;
}
线程完成/等待 (pthread_join) 功能:让一个线程(通常是主线程)阻塞(睡眠),直到另一个目标线程执行完毕。
1
2
3
4
int pthread_join(
pthread_t thread, // [入参] 你要等待的那个线程的ID(由 create 返回)
void **value_ptr // [出参] 一个指针,用于接收目标线程的返回值
);
- thread:你要等待的目标。 - void **value_ptr:这是API中最令人困惑的部分,我们来拆解它: - 目标线程的start_routine返回一个void (这是它的“返回值”)。 - pthread_join函数需要将这个void 返回值写入到main函数的一个变量中。 - 由于C语言中函数参数传递是“值传递”,如果你直接传入一个void *变量,pthread_join只能修改它的副本,无法修改main函数中的变量。 - 因此,你需要传入这个变量的地址,即一个void **,这样pthread_join才能通过这个地址修改main函数中的变量。 - 例如:
1
2
void* retVal; // 用于接收线程的返回值
pthread_join(thread, &retVal); // 传入 retVal 的地址(void **)
- 返回值:成功返回0,失败返回错误码。

线程终止 (pthread_exit) 功能:这是终止当前线程的唯一方法。你可以在当前线程的任何地方调用它来结束线程的执行。

1
void pthread_exit(void *value_ptr);
- value_ptr:这是线程的“返回值”。当线程调用pthread_exit时,它可以传递一个void *值作为它的结果。这个值可以被其他线程通过pthread_join获取。 - 注意:调用pthread_exit不会终止整个进程,只会终止当前线程。

互斥原语:锁

这组API用于解决竞态条件问题。

功能:提供互斥(Mutual Exclusion),确保一个“临界区”在同一时间只被一个线程执行。

1
2
int pthread_mutex_lock(pthread_mutex_t *mutex);  // 加锁, 传入锁对象的指针
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- pthread_mutex_lock:尝试获取锁。如果锁已经被其他线程持有,调用线程将阻塞,直到锁可用。 - pthread_mutex_unlock:释放锁。只有持有锁的线程才能调用此函数。 - 返回值:成功返回0,失败返回错误码。

注意, pthread_mutex_t是一个复杂的数据结构,必须在使用前初始化. 可以使用以下两种方式初始化互斥锁: 1. 静态初始化: 用于全局或静态锁

1
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2. 动态初始化: 用于堆上或栈上的锁
1
2
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr); // 第二个参数为锁属性,通常传入 nullptr 表示默认属性
例如, 使用互斥锁保护临界区:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <pthread.h>
#include <iostream>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化互斥锁
int sharedResource = 0; // 共享资源
void* threadFunc(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
// 临界区开始
sharedResource++;
std::cout << "Shared Resource: " << sharedResource << std::endl;
// 临界区结束
pthread_mutex_unlock(&mutex); // 解锁
return nullptr;
}

协作原语:条件变量

这组API用于解决等待/通知(协作)问题。

功能:允许一个线程睡眠(wait),直到某个“条件”为真,而另一个线程在使该“条件”为真后,通知(signal)睡眠的线程醒来。

1
2
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);  // 等待条件变量, 传入条件变量和互斥锁的指针
int pthread_cond_signal(pthread_cond_t *cond);
- pthread_cond_wait(&cond, &lock) 这一行代码会原子地执行以下三步: - 释放传入的锁(&lock)。(这允许“通知者”线程能有机会进入临界区去修改ready) - 使当前线程睡眠(等待&cond上的信号)。 - (…当被唤醒后…)重新获取那个锁(&lock),然后才从wait函数返回。 这里 wait 和 signal 的标准模式是: 条件变量必须和一个互斥锁(Mutex)配对使用,以保护“条件”本身(例如一个 ready 标志)
等待者(Waiter)的代码:
1
2
3
4
5
6
pthread_mutex_lock(&lock);
while (ready == 0) { // 必须是 while, 以防虚假唤醒
pthread_cond_wait(&cond, &lock);
}
// ... 此刻, ready 必然为 1, 且我们持有锁 ...
pthread_mutex_unlock(&lock);
通知者(Signaler)的代码:
1
2
3
4
pthread_mutex_lock(&lock);
ready = 1; // 改变条件
pthread_cond_signal(&cond); // 唤醒一个等待者
pthread_mutex_unlock(&lock);