深入理解系统级并发编程核心概念
并发与并行的本质区别
并发(Concurrency)指的是多个任务在时间上重叠执行,在单处理器上通过时间片轮转实现。而并行(Parallel)是并发的一种特殊形式,指多个任务同时在多核处理器或多CPU上执行。
构建并发程序的三种范式
- 进程模型:每个逻辑控制流作为独立进程,由内核调度管理。进程间拥有独立的虚拟地址空间,共享数据需通过显式IPC机制。
- I/O多路复用:在单一进程内管理多个逻辑流,通过select/epoll等机制调度。逻辑流被抽象为状态机,共享数据自然但编程复杂度较高。
- 线程模型
基于进程的并发实现
void child_handler(int signo) {
pid_t pid;
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0)
;
}
int main() {
signal(SIGCHLD, child_handler);
int listen_sock = create_socket();
while (1) {
int conn_sock = accept(listen_sock, NULL, NULL);
if (fork() == 0) {
close(listen_sock);
handle_connection(conn_sock);
close(conn_sock);
exit(0);
}
close(conn_sock);
}
}
关键点:父子进程共享文件表项,必须正确关闭连接描述符避免资源泄漏。进程间通信成本较高,但内存隔离提供了安全性。
I/O多路复用技术
fd_set master_set, ready_set;
FD_ZERO(&master_set);
FD_SET(STDIN_FILENO, &master_set);
FD_SET(listen_sock, &master_set);
while (1) {
ready_set = master_set;
select(listen_sock + 1, &ready_set, NULL, NULL, NULL);
if (FD_ISSET(STDIN_FILENO, &ready_set))
process_input();
if (FD_ISSET(listen_sock, &ready_set))
handle_new_connection();
}
select函数会修改描述符集合,因此需要保留原始副本。此模型在单线程中实现高并发,但无法利用多核优势。现代高性能服务器如Nginx、Node.js均采用此模式。
线程编程接口详解
typedef void* (*thread_func)(void*);
pthread_t thread_id;
pthread_create(&thread_id, NULL, thread_func, arg);
pthread_detach(thread_id);
void* thread_func(void* arg) {
int* sock_ptr = (int*)arg;
int client_sock = *sock_ptr;
free(sock_ptr);
process_request(client_sock);
return NULL;
}
注意:传递给线程的参数必须动态分配,避免栈变量被覆盖。线程上下文包含独立栈、寄存器等,共享进程的其余资源。
共享变量与内存模型
- 全局变量:存储在数据段,所有线程共享同一实例
- 局部静态变量:同样存储在数据段,但作用域受限
- 局部自动变量:存储在各线程的栈中,互不影响
信号量同步机制
sem_t sem_lock;
sem_init(&sem_lock, 0, 1);
// 生产者
void producer() {
sem_wait(&empty);
sem_wait(&sem_lock);
add_item(buffer);
sem_post(&sem_lock);
sem_post(&full);
}
// 消费者
void consumer() {
sem_wait(&full);
sem_wait(&sem_lock);
item = remove_item(buffer);
sem_post(&sem_lock);
sem_post(&empty);
}
P操作(sem_wait)和V操作(sem_post)保证原子性。读者-写者问题需要特别注意避免写者饥饿现象。
线程安全与可重入性
线程安全函数可被多线程并发调用,可重入函数是最严格的一类,不依赖任何共享状态。常见线程不安全函数的替代方案:
- rand() → rand_r()
- strtok() → strtok_r()
- ctime() → ctime_r()
死锁预防策略
最有效的预防方法是锁排序规则:所有线程按照固定顺序获取锁,相反顺序释放。现代C++支持同时获取多个锁,从根源避免死锁。
并行计算优化技巧
// 优化前:频繁加锁
void* sum_mutex(void* arg) {
for (i = start; i < end; i++) {
pthread_mutex_lock(&lock);
global_sum += i;
pthread_mutex_unlock(&lock);
}
}
// 优化后:局部累加
void* sum_optimized(void* arg) {
long local = 0;
for (i = start; i < end; i++)
local += i;
partial_sums[thread_id] = local;
}
性能对比显示,优化后的版本可获得接近线性的加速比。关键在于减少临界区大小和内存访问竞争。