二进制信号量:线程同步的基础
什么是二进制信号量?
二进制信号量是一种特殊的信号量,其值只能是0或1。它是最简单的线程同步机制之一,常用于线程间的简单协调。
#include <semaphore.h> sem_t sem; // 声明二进制信号量 sem_init(&sem, 0, 1); // 初始化,第二个参数0表示线程间共享,初始值为1
二进制信号量的工作原理:
-
值为1:资源可用,线程可以继续执行
-
值为0:资源不可用,线程必须等待
二进制信号量的基本操作
-
sem_wait() - 获取信号量
sem_wait(&sem); // 如果sem>0则减1,否则阻塞
-
sem_post() - 释放信号量
sem_post(&sem); // 信号量值加1
-
sem_getvalue() - 获取当前信号量值
int val; sem_getvalue(&sem, &val);
二进制信号量与互斥锁的区别
虽然二进制信号量和互斥锁看起来很相似,但它们有重要区别:
特性 | 二进制信号量 | 互斥锁 |
---|---|---|
所有者 | 无所有者概念 | 必须由加锁线程解锁 |
初始值 | 可设为0或1 | 通常初始为1 |
用途 | 线程同步、事件通知 | 保护临界区 |
性能 | 稍慢 | 更快 |
实际应用中的选择
-
使用信号量的场景:
-
需要线程间通知(如生产者-消费者)
-
需要限制并发访问数量
-
-
使用互斥锁的场景:
-
保护共享资源的访问
-
需要确保解锁由同一线程完成
-
获取信号量的值
获取信号量的当前值可以使用sem_getvalue()
在并发编程中,条件变量(cond)
、信号量(semaphore)
和互斥锁(mutex)
是三种核心的线程同步机制,它们有各自的特点和适用场景。以下是它们的详细对比和解释:
1. 互斥锁(Mutex)
作用
-
保护共享资源:确保同一时间只有一个线程能访问临界区(critical section),防止数据竞争。
特点
-
二元状态:只有锁定(locked)和解锁(unlocked)两种状态。
-
所有权:必须由加锁的线程解锁(不能跨线程解锁)。
-
阻塞行为:其他线程尝试获取已锁定的互斥锁时会被阻塞。
C语言示例
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* thread_func() { pthread_mutex_lock(&mutex); // 加锁 // 临界区代码 pthread_mutex_unlock(&mutex); // 解锁 }
适用场景
-
保护共享变量(如全局计数器)。
-
需要确保操作的原子性时。
2. 信号量(Semaphore)
作用
-
控制并发访问数量:允许多个线程(数量可配置)同时访问资源。
-
线程同步:协调线程的执行顺序(如生产者-消费者模型)。
特点
-
计数器:信号量是一个非负整数,表示可用资源数。
-
无所有者:任何线程都可以释放(
post
)信号量。 -
阻塞行为:当信号量为0时,
wait
操作会阻塞线程。
C语言示例
#include <semaphore.h>
sem_t sem;
void init() {
sem_init(&sem, 0, 3); // 初始值为3,允许3个线程并发
}
void* thread_func() {
sem_wait(&sem); // 信号量减1(若为0则阻塞)
// 访问共享资源
sem_post(&sem); // 信号量加1
}
适用场景
-
限制数据库连接池的并发数。
-
实现生产者-消费者队列。
3. 条件变量(Condition Variable, cond)
作用
-
线程间通知:允许线程在某个条件满足时被唤醒,避免忙等待(busy-waiting)。
-
配合互斥锁使用:通常与互斥锁一起实现复杂的同步逻辑。
特点
-
无状态:本身不存储条件,需依赖外部条件判断。
-
等待/通知机制:线程通过
wait
主动阻塞,通过signal
或broadcast
唤醒。
C语言示例
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0; // 条件标志
// 等待线程
void* consumer() {
pthread_mutex_lock(&mutex);
while (!ready) {
pthread_cond_wait(&cond, &mutex); // 释放锁并阻塞
}
// 条件满足后的操作
pthread_mutex_unlock(&mutex);
}
// 通知线程
void* producer() {
pthread_mutex_lock(&mutex);
ready = 1;
pthread_cond_signal(&cond); // 唤醒一个等待线程
pthread_mutex_unlock(&mutex);
}
适用场景
-
实现线程间的事件通知(如任务队列非空时唤醒工作线程)。
-
避免轮询检查条件(节省CPU资源)。
三者的核心区别
特性 | 互斥锁(Mutex) | 信号量(Semaphore) | 条件变量(cond) |
---|---|---|---|
用途 | 保护临界区 | 控制并发访问数量 | 线程间事件通知 |
是否持有资源 | 是(锁的持有者) | 否(仅计数器) | 否(需外部条件判断) |
释放操作 | 必须由加锁线程解锁 | 任意线程可post | 任意线程可signal |
阻塞行为 | 获取锁失败时阻塞 | 信号量为0时阻塞 | 显式调用wait 时阻塞 |
典型场景 | 共享变量修改 | 连接池限流 | 生产者-消费者模型 |
如何选择?
-
需要保护共享数据 → 互斥锁
(如:多个线程修改同一个全局变量) -
需要限制并发数 → 信号量
(如:最多5个线程同时访问数据库) -
需要等待某个条件成立 → 条件变量 + 互斥锁
(如:任务队列非空时唤醒线程)
常见问题解答
Q1: 为什么条件变量需要配合互斥锁?
-
条件变量的
wait
操作会临时释放锁并阻塞,被唤醒后重新获取锁,确保条件检查的原子性。
Q2: 二进制信号量(初始值为1)和互斥锁的区别?
-
二进制信号量可以被任意线程释放,而互斥锁必须由加锁线程解锁。
Q3: 信号量的post
和条件变量的signal
有何不同?
-
sem_post
会增加信号量计数器,可能唤醒多个线程(取决于wait
的线程数)。 -
pthread_cond_signal
仅唤醒一个等待线程(不涉及计数器)。
深入理解并行与并发及C语言线程池实现
并行(Parallelism) vs 并发(Concurrency)
并行是指多个任务真正同时执行,需要多核CPU硬件支持。在C语言中,我们可以通过创建多个线程并分配到不同CPU核心上来实现并行计算。
// 并行计算数组元素平方的示例
void* parallel_square(void* arg) {
int* data = (int*)arg;
for (int i = 0; i < CHUNK_SIZE; i++) {
data[i] = data[i] * data[i]; // 无共享数据,可真正并行
}
return NULL;
}
// 创建线程并行处理
pthread_t threads[4];
int chunks[4][CHUNK_SIZE];
for (int i = 0; i < 4; i++) {
pthread_create(&threads[i], NULL, parallel_square, chunks[i]);
}
并发是指多个任务交替执行,在单核CPU上通过时间片轮转实现"看似同时"的效果。典型的并发模式是使用互斥锁保护共享资源:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;
void* concurrent_increment(void* arg) {
pthread_mutex_lock(&lock);
shared_counter++; // 临界区
pthread_mutex_unlock(&lock);
return NULL;
}
关键区别:
-
并行需要多核硬件,并发可在单核实现
-
并行是物理上的同时执行,并发是逻辑上的交替执行
-
并行性能可线性扩展,并发性能受限于上下文切换开销
这段代码实现了一个多线程任务队列系统,结合了互斥锁(pthread_mutex_t
)和条件变量(pthread_cond_t
)来安全地分配和处理任务。以下是详细解析:
1. 核心组件
(1)数据结构
typedef struct Task { int a, b; // 任务参数 } Task; Task taskQueue[256]; // 任务队列 int taskCount = 0; // 当前任务数
-
任务队列:固定大小的数组(256),存储待处理的
Task
结构体。 -
任务计数:记录队列中当前的任务数量。
(2)同步机制
pthread_mutex_t mutexQueue; // 保护任务队列的互斥锁 pthread_cond_t condQueue; // 通知线程有新任务的条件变量
-
互斥锁:确保对
taskQueue
和taskCount
的访问是线程安全的。 -
条件变量:当队列为空时,让线程等待;当有新任务时,唤醒线程。
2. 关键函数
(1)提交任务 (submitTask
)
void submitTask(Task task) { pthread_mutex_lock(&mutexQueue); taskQueue[taskCount] = task; // 添加任务到队列尾部 taskCount++; pthread_mutex_unlock(&mutexQueue); pthread_cond_signal(&condQueue); // 唤醒一个等待的线程 }
-
线程安全:通过互斥锁保护队列操作。
-
信号通知:调用
pthread_cond_signal
唤醒一个阻塞的线程(如果有)。
(2)线程执行逻辑 (startThread
)
void* startThread(void* args) { while (1) { Task task; // 获取任务(加锁) pthread_mutex_lock(&mutexQueue); while (taskCount == 0) { pthread_cond_wait(&condQueue, &mutexQueue); // 队列空则等待 } // 从队列头部取出任务 task = taskQueue[0]; for (int i = 0; i < taskCount - 1; i++) { taskQueue[i] = taskQueue[i + 1]; // 队列前移 } taskCount--; pthread_mutex_unlock(&mutexQueue); // 执行任务(无锁,可并行) executeTask(&task); } }
-
队列空时等待:
pthread_cond_wait
会释放锁并阻塞,直到被pthread_cond_signal
唤醒。 -
任务调度:采用 FIFO(先进先出) 策略处理任务。
-
并行执行:
executeTask
在锁外执行,多个线程可同时处理不同任务。
(3)任务执行 (executeTask
)
void executeTask(Task* task) { int result = task->a + task->b; printf("The sum of %d and %d is %d\n", task->a, task->b, result); }
-
无锁操作:纯计算任务,可完全并行。
3. 主程序流程
-
初始化:
-
创建 4 个线程(
THREAD_NUM=4
)。 -
初始化互斥锁和条件变量。
-
-
生成任务:
for (i = 0; i < 100; i++) { Task t = { .a = rand() % 100, .b = rand() % 100 }; submitTask(t); // 提交100个随机任务 }
-
线程结束:
-
主线程等待所有子线程完成(实际因
while(1)
永不结束,需改进)。
-
4. 并发与并行分析
特性 | 实现方式 |
---|---|
并发控制 | 互斥锁保护任务队列,条件变量优化线程等待/唤醒。 |
并行计算 | 多个线程同时执行 executeTask (无锁部分)。 |
任务调度 | 线程竞争获取任务,队列为空时阻塞,避免忙等待。 |
C语言的线程池API设计与实现
线程池是一种管理多个线程的技术,可以避免频繁创建销毁线程的开销。
基本结构
typedef struct { pthread_t* threads; TaskQueue* queue; pthread_mutex_t lock; pthread_cond_t notify; int thread_count; int shutdown; } ThreadPool;
核心API
-
创建线程池:
ThreadPool* tp_create(int thread_count) { ThreadPool* pool = malloc(sizeof(ThreadPool)); // 初始化互斥锁、条件变量等 return pool; }
-
添加任务:
void tp_add_task(ThreadPool* pool, void (*func)(void*), void* arg) { Task task = {func, arg}; pthread_mutex_lock(&pool->lock); enqueue(pool->queue, task); pthread_cond_signal(&pool->notify); pthread_mutex_unlock(&pool->lock); }
-
工作线程函数:
void* worker_thread(void* arg) { ThreadPool* pool = (ThreadPool*)arg; while(1) { pthread_mutex_lock(&pool->lock); while(pool->queue->size == 0 && !pool->shutdown) { pthread_cond_wait(&pool->notify, &pool->lock); } // 获取并执行任务 pthread_mutex_unlock(&pool->lock); } return NULL; }
线程池中的C函数指针
在线程池实现中,函数指针用于表示任务:
typedef struct { void (*function)(void*); void* argument; } Task;
使用方法
-
定义任务函数:
void print_hello(void* arg) { int id = *(int*)arg; printf("Hello from task %d\n", id); }
-
提交任务:
int task_id = 42; tp_add_task(pool, print_hello, &task_id);
高级技巧
使用函数指针数组实现多态任务处理:
void (*handlers[])(void*) = {handle_type_a, handle_type_b, handle_type_c}; // 根据任务类型调用不同处理函数 void process_task(Task* task) { int type = get_task_type(task); handlers[type](task->argument); }
遇到的问题与解决方案
问题1:为什么我的线程池没有输出?
现象:提交任务后程序无输出,直接退出。
原因:主线程在提交任务后立即退出,导致整个进程终止。
解决方案:
// 添加等待机制 while(tasks_not_completed) { sleep(1); }
问题2:如何避免线程池中的任务饥饿?
解决方案:
-
实现任务优先级队列
-
使用工作窃取(Work Stealing)算法
-
限制每个线程处理的任务数量
问题3:信号量和互斥锁混用时死锁
错误代码:
void foo() { pthread_mutex_lock(&mutex); sem_wait(&sem); // 可能死锁 // ... }
正确做法:保持锁的获取顺序一致,或重构代码减少锁的嵌套。