C语言多线程编程
前言
早期的计算机只能执行一个任务,任务完成后就等待下一个,效率极低。后来操作系统引入多进程,可以同时运行多个程序,但进程间通信开销大、资源消耗多。为了进一步提高并发性能,多线程技术诞生了——一个进程内可以包含多个线程,它们共享进程资源,轻量且高效。
本文基于Linux POSIX线程库(pthread),从基础概念到同步机制,带你一步步掌握C语言多线程编程。所有代码均来自实际教学案例,可直接运行。
一、线程基础概念
- 线程是操作系统能够进行运算调度的最小单位,被包含在进程中,是进程中实际运行工作的单位。
- 一个进程至少有一个线程(主线程),也可以创建多个子线程。
- 同一进程内的多个线程共享进程的内存空间和资源(全局变量、文件描述符等),但每个线程有自己的栈和程序计数器。
2. 线程 vs 进程
| 资源分配 | 独立地址空间,资源多 | 共享进程资源,轻量 |
| 通信方式 | 需要IPC(管道、消息队列等) | 直接读写共享内存 |
| 创建开销 | 大 | 小 |
| 隔离性 | 强,一个进程崩溃不影响其他 | 弱,一个线程崩溃可能影响整个进程 |
3. 线程的好处
- 资源消耗少:创建线程比创建进程快得多,内存占用小。
- 数据共享方便:直接使用全局变量即可通信。
- 提高响应性:UI线程与后台任务分离,界面不卡顿。
- 充分利用多核CPU:将任务分配到不同核心并行执行。
二、线程的基本使用(pthread)
使用线程需要引入头文件 <pthread.h>,编译时加 -lpthread。
1. 创建线程:pthread_create
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg);
- thread:输出参数,存储新线程ID。//需要传入一个指向 pthread_t* 类型变量的指针也就是传入变量的地址
- attr:线程属性,传NULL使用默认。
- start_routine:线程执行的函数,返回void*,参数void*。//就是一个函数指针,可以接受void * func(void*) 类型的函数,只要传给它函数地址就行,在c语言里,函数名也代表函数地址,和数组名一样
- arg:传递给线程函数的参数。
示例:创建线程并传递参数
#include <stdio.h>
#include <pthread.h>
void* print_num(void* arg) {
int num = *(int*)arg;//强值转换型成指向int* 类型的指针,然后在解引用
printf("子线程收到参数:%d\\n", num);
return NULL;
}
int main() {
pthread_t tid;//定义了一个名为 tid 的变量,它的类型是 pthread_t
int value = 42;
pthread_create(&tid, NULL, print_num, &value);
//只有第三个参数很重要,需要去定义一个 void* func(void* arg) 这种类型函数,第四个函数是 void*类型的,需要一个指向void*类型的指针,传地址就行 这个参数就是func(参数),函数内部需要时就传参,不需要就NULL
pthread_join(tid, NULL); // 等待子线程结束
return 0;
}
2. 等待线程结束:pthread_join
- 阻塞等待指定线程结束,并回收其资源。
- 可以获取线程返回值(通过二级指针)。
void* compute(void* arg) {
int *result = malloc(sizeof(int));
*result = 100;
return result;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, compute, NULL);
int *ret;
pthread_join(tid, (void**)&ret);
printf("线程返回值:%d\\n", *ret);
free(ret);
return 0;
}
3. 线程退出:pthread_exit vs exit
- pthread_exit(NULL):仅结束当前线程,不影响其他线程。
- exit(0):结束整个进程,所有线程一并终止。
4. 获取线程ID:pthread_self
pthread_t my_id = pthread_self();
printf("当前线程ID:%ld\\n", my_id);
5. 线程分离:pthread_detach
设置线程为分离状态,系统自动回收资源,无需pthread_join。
pthread_detach(tid); // 与join互斥,二选一
6. 线程取消:pthread_cancel
向线程发送取消请求,线程是否响应取决于其取消状态和取消点。
pthread_cancel(tid);
// 线程函数中需设置取消点:
pthread_testcancel();
三、线程同步——解决数据竞争
当多个线程同时访问共享资源(如全局变量)时,会发生数据混乱。例如三个窗口同时卖100张票,不加锁会导致超卖。作用范围是修改全局变量的区域,即临界区变量,加锁pv都是这个区域
1. 互斥锁(Mutex)
原理:同一时间只有一个线程能持有锁,其他线程阻塞等待。
卖票问题(无锁版 → 有锁版)
无锁版(错误):
int total = 100;
void* sale(void* arg) {
char* name = (char*)arg;
int count = 0;
while (1) {
if (total > 0) {
count++;
usleep(100000); // 模拟耗时
total–;
printf("%s 卖第%d张,剩余%d\\n", name, count, total);
} else break;
}
return NULL;
}
// 三个线程同时运行,会出现 total 变成负数等错误
加互斥锁(正确):
int total = 100;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化
void* sale(void* arg) {
char* name = (char*)arg;
int count = 0;
while (1) {
pthread_mutex_lock(&mutex); // 加锁
if (total > 0) {
count++;
total–;
printf("%s 卖第%d张,剩余%d\\n", name, count, total);
pthread_mutex_unlock(&mutex); // 解锁
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
return NULL;
}
int main() {
pthread_t t1, t2, t3;
pthread_create(&t1, NULL, sale, "12306");
pthread_create(&t2, NULL, sale, "美团");
pthread_create(&t3, NULL, sale, "携程");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
// 最终 total == 0
return 0;
}
动态初始化:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // 使用前
pthread_mutex_destroy(&mutex); // 使用后,要销毁
2. 信号量(Semaphore)
信号量是一个计数器,用于控制多个线程对资源的访问或实现顺序执行。
常用函数:
- sem_init(sem_t *sem, int pshared, unsigned int value):初始化,pshared=0表示线程间共享。
- sem_wait(sem_t *sem):P操作,值减1,若为0则阻塞。
- sem_post(sem_t *sem):V操作,值加1,唤醒一个等待线程。
- sem_destroy(sem_t *sem):销毁。
示例:三个线程按顺序打印 A→B→C
#include <semaphore.h>
sem_t sem1, sem2, sem3;
void* printA(void* arg) {
for (int i = 0; i < 3; i++) {
sem_wait(&sem1);
printf("A");
sem_post(&sem2);
}
return NULL;
}
void* printB(void* arg) {
for (int i = 0; i < 3; i++) {
sem_wait(&sem2);
printf("B");
sem_post(&sem3);
}
return NULL;
}
void* printC(void* arg) {
for (int i = 0; i < 3; i++) {
sem_wait(&sem3);
printf("C");
sem_post(&sem1);
}
return NULL;
}
int main() {
sem_init(&sem1, 0, 1); // 初始1,A先执行
sem_init(&sem2, 0, 0);
sem_init(&sem3, 0, 0);
pthread_t a, b, c;
pthread_create(&a, NULL, printA, NULL);
pthread_create(&b, NULL, printB, NULL);
pthread_create(&c, NULL, printC, NULL);
pthread_join(a, NULL);
pthread_join(b, NULL);
pthread_join(c, NULL);
sem_destroy(&sem1);
sem_destroy(&sem2);
sem_destroy(&sem3);
printf("\\n");
return 0;
}
// 输出:ABCABCABC
3. 条件变量(Condition Variable)
条件变量与互斥锁配合,实现复杂的线程同步(如生产者-消费者模型)。线程在条件不满足时阻塞,条件满足时被唤醒。
常用函数:
- pthread_cond_init / pthread_cond_destroy
- pthread_cond_wait(&cond, &mutex):释放锁(解锁)并阻塞,唤醒后重新获取锁。
- pthread_cond_signal(&cond):唤醒一个等待线程。
- pthread_cond_broadcast(&cond):唤醒所有等待线程。
| 代码 | pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; | pthread_mutex_t m; pthread_mutex_init(&m, NULL); |
| 初始化时机 | 程序加载时(编译期常量) | 运行时调用函数 |
| 销毁 | 通常不需要 | 必须调用 pthread_mutex_destroy |
| 自定义属性 | 不支持 | 支持,通过 pthread_mutexattr_t |
| 进程间共享 | 不支持 | 可通过属性设置 PTHREAD_PROCESS_SHARED |
注意:必须用 while 而不是 if 检查条件,防止虚假唤醒。
生产者-消费者模型(单次生产消费)
int shared_data = 0;
int data_ready = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//静态初始化
//静态初始化不需要额外初始化和销毁变量
void* producer(void* arg) {
pthread_mutex_lock(&mutex);
shared_data = 100;
data_ready = 1;
printf("生产者生产:%d\\n", shared_data);
pthread_cond_signal(&cond); // 通知消费者
pthread_mutex_unlock(&mutex);
return NULL;
}
void* consumer(void* arg) {
pthread_mutex_lock(&mutex);
while (data_ready == 0) {
pthread_cond_wait(&cond, &mutex); // 阻塞等待
}
printf("消费者消费:%d\\n", shared_data);
data_ready = 0;
pthread_mutex_unlock(&mutex);
return NULL;
}
生产者-消费者(多次生产消费,交替执行)
void* producer(void* arg) {
for (int i = 1; i <= 10; i++) {
pthread_mutex_lock(&mutex);
while (data_ready == 1) { // 上次数据未被消费,等待
pthread_cond_wait(&cond, &mutex);
}
shared_data = i * 10;
data_ready = 1;
printf("生产:%d\\n", shared_data);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 1; i <= 10; i++) {
pthread_mutex_lock(&mutex);
while (data_ready == 0) { // 无数据,等待
pthread_cond_wait(&cond, &mutex);
}
printf("消费:%d\\n", shared_data);
data_ready = 0;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
4. 环形缓冲区(经典生产者消费者)
缓冲区大小固定,生产者放包子,消费者取包子,使用两个条件变量控制满和空。
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0, in = 0, out = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void* producer(void* arg) {
int total = 0;
while (1) {
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) { // 满则等待
pthread_cond_wait(¬_full, &mutex);
}
total++;
buffer[in] = total;
count++;
printf("生产者放入第%d个包子,位置%d,当前%d个\\n", total, in, count);
in = (in + 1) % BUFFER_SIZE;
pthread_cond_signal(¬_empty); // 通知消费者
pthread_mutex_unlock(&mutex);
usleep(500000);//单位微妙,sleep()单位秒,只能是整型
}
return NULL;
}
void* consumer(void* arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (count == 0) { // 空则等待
pthread_cond_wait(¬_empty, &mutex);
}
int data = buffer[out];
count–;
printf("消费者取走第%d个包子,位置%d,剩余%d\\n", data, out, count);
out = (out + 1) % BUFFER_SIZE;
pthread_cond_signal(¬_full); // 通知生产者
pthread_mutex_unlock(&mutex);
usleep(500000);
}
return NULL;
}
四、总结
| 互斥锁 | 保护共享资源,防止数据竞争 | pthread_mutex_lock/unlock |
| 信号量 | 控制访问数量、顺序执行 | sem_wait/sem_post |
| 条件变量 | 复杂同步,如生产者消费者 | pthread_cond_wait/signal |
附录:编译命令
gcc xxx.c -o xxx.out -lpthread
/* -lpthread 如果不加这个,低版本的编译器会报错,不报错也可以不加*/






