线程间通信是多线程编程的核心问题,合理选择通信方式直接影响程序的性能和稳定性。在Linux环境下,线程间通信主要通过共享内存区域实现,但随之而来的资源竞争问题需要开发者特别注意。
所有线程共享进程的数据段、堆区和打开的文件描述符,这为通信提供了天然的基础设施。最常见的实现方式是通过全局变量进行数据交换:
c复制#include <stdio.h>
#include <pthread.h>
int global_counter = 0; // 共享全局变量
void* thread_func(void* arg) {
for(int i=0; i<100000; i++){
global_counter++;
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, thread_func, NULL);
pthread_create(&t2, NULL, thread_func, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter: %d\n", global_counter);
return 0;
}
这段代码看似简单,却隐藏着严重的竞态条件问题。两个线程同时执行global_counter++操作时,可能会得到错误的结果。这是因为++操作并非原子操作,实际由"读取-修改-写入"三个步骤组成。
重要提示:在x86架构上,简单的整数自增可能在测试时看似正确,但这只是表象。在ARM等多核处理器上,问题会立即显现。永远不要依赖未加保护的共享变量。
互斥锁(Mutex)是解决资源竞争的标准方案,其核心原理是通过原子操作保证同一时刻只有一个线程能进入临界区。POSIX线程库提供了完整的互斥锁API:
c复制pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化方式
void* safe_thread_func(void* arg) {
for(int i=0; i<100000; i++){
pthread_mutex_lock(&mutex);
global_counter++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
实际开发中需要注意几个关键点:
死锁是并发编程中最棘手的问题之一,我曾在项目中遇到过因锁顺序不一致导致的死锁,排查过程异常痛苦。以下是几个实用技巧:
c复制struct timespec timeout;
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += 1; // 1秒超时
if(pthread_mutex_timedlock(&mutex, &timeout) == ETIMEDOUT) {
// 超时处理逻辑
log_error("Acquire lock timeout!");
return -1;
}
信号量是比互斥锁更通用的同步机制,由Dijkstra提出,可用于控制对多个共享资源的访问。
c复制#include <semaphore.h>
sem_t semaphore;
void init_semaphore() {
sem_init(&semaphore, 0, 3); // 初始值为3,表示最多3个线程同时访问
}
void* limited_thread(void* arg) {
sem_wait(&semaphore); // P操作
// 访问受限资源
sem_post(&semaphore); // V操作
return NULL;
}
信号量的经典应用场景包括:
下面是一个完整的生产者-消费者示例,使用两个信号量分别控制缓冲区空位和已用位置:
c复制#define BUF_SIZE 10
int buffer[BUF_SIZE];
sem_t empty, full;
pthread_mutex_t mutex;
int in = 0, out = 0;
void init() {
sem_init(&empty, 0, BUF_SIZE);
sem_init(&full, 0, 0);
pthread_mutex_init(&mutex, NULL);
}
void* producer(void* arg) {
for(int i=0; i<100; i++) {
sem_wait(&empty);
pthread_mutex_lock(&mutex);
buffer[in] = i;
in = (in + 1) % BUF_SIZE;
pthread_mutex_unlock(&mutex);
sem_post(&full);
}
return NULL;
}
void* consumer(void* arg) {
for(int i=0; i<100; i++) {
sem_wait(&full);
pthread_mutex_lock(&mutex);
int item = buffer[out];
out = (out + 1) % BUF_SIZE;
pthread_mutex_unlock(&mutex);
sem_post(&empty);
printf("Consumed: %d\n", item);
}
return NULL;
}
性能提示:在x86_64架构上,sem_post和sem_wait的系统调用开销约为200-300纳秒。对于高性能场景,可以考虑用户态的无锁队列实现。
进程间通信(IPC)是Linux系统编程的重要部分,不同的IPC机制适用于不同场景。
c复制#include <unistd.h>
#include <sys/wait.h>
void unnamed_pipe_demo() {
int pipefd[2];
char buf[20];
pipe(pipefd); // 创建管道
pid_t pid = fork();
if(pid == 0) { // 子进程
close(pipefd[1]); // 关闭写端
read(pipefd[0], buf, sizeof(buf));
printf("Child received: %s\n", buf);
close(pipefd[0]);
exit(0);
} else { // 父进程
close(pipefd[0]); // 关闭读端
write(pipefd[1], "Hello from parent", 17);
close(pipefd[1]);
wait(NULL);
}
}
c复制// writer.c
#include <fcntl.h>
#include <sys/stat.h>
int main() {
mkfifo("/tmp/myfifo", 0666);
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello FIFO", 10);
close(fd);
return 0;
}
// reader.c
int main() {
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[20];
read(fd, buf, sizeof(buf));
printf("Received: %s\n", buf);
close(fd);
return 0;
}
管道使用中的常见陷阱:
信号是进程间通信的轻量级机制,正确处理信号对构建健壮应用至关重要。
c复制#include <signal.h>
#include <stdio.h>
volatile sig_atomic_t flag = 0;
void handler(int sig) {
flag = 1; // 只设置标志,不进行复杂操作
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
while(!flag) {
printf("Running...\n");
sleep(1);
}
printf("Graceful shutdown\n");
return 0;
}
信号处理的核心原则:
共享内存是最快的IPC方式,适合大数据量传输:
c复制#include <sys/shm.h>
#include <sys/ipc.h>
#define SHM_SIZE 1024
int main() {
int shmid = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);
char *shm = shmat(shmid, NULL, 0);
if(fork() == 0) { // 子进程
sprintf(shm, "Hello from child");
shmdt(shm);
exit(0);
} else { // 父进程
wait(NULL);
printf("Parent received: %s\n", shm);
shmdt(shm);
shmctl(shmid, IPC_RMID, NULL);
}
return 0;
}
性能数据:在Intel i7处理器上,共享内存的传输速度可达10GB/s以上,而管道通常只有1-2GB/s。
消息队列适合需要持久化或跨机器通信的场景:
c复制#include <sys/msg.h>
struct msgbuf {
long mtype;
char mtext[100];
};
int main() {
int msqid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "Message queue test");
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
if(fork() == 0) {
msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0);
printf("Child received: %s\n", msg.mtext);
exit(0);
}
wait(NULL);
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
| 机制 | 速度 | 容量 | 适用场景 | 复杂度 |
|---|---|---|---|---|
| 管道 | 中 | 小(64KB) | 父子进程简单通信 | 低 |
| FIFO | 中 | 小(64KB) | 任意进程简单通信 | 低 |
| 信号 | 快 | 极小(仅信号值) | 事件通知 | 中 |
| 共享内存 | 极快 | 大(GB级) | 大数据量交换 | 高 |
| 消息队列 | 中 | 中(MB级) | 结构化消息传输 | 中 |
| 套接字 | 慢 | 大(GB级) | 网络通信 | 高 |
在实际项目中,我通常会根据以下因素选择IPC机制:
问题现象:程序偶尔卡死,CPU使用率低
gdb -p <pid>thread apply all bt问题现象:计数器结果偶尔不正确
c复制pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 读线程
pthread_rwlock_rdlock(&rwlock);
// 读操作
pthread_rwlock_unlock(&rwlock);
// 写线程
pthread_rwlock_wrlock(&rwlock);
// 写操作
pthread_rwlock_unlock(&rwlock);
c复制pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
// 等待线程
pthread_mutex_lock(&mutex);
while(!ready) {
pthread_cond_wait(&cond, &mutex);
}
// 处理就绪事件
pthread_mutex_unlock(&mutex);
// 通知线程
pthread_mutex_lock(&mutex);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
c复制#include <stdatomic.h>
atomic_int counter = ATOMIC_VAR_INIT(0);
void increment() {
atomic_fetch_add(&counter, 1);
}
不同Unix-like系统对线程和IPC的支持略有差异,编写可移植代码时需要注意:
-D_REENTRANT或-pthread确保线程安全#include <semaphore.h>#include <sys/ipc.h>-lrt链接实时扩展库c复制#if defined(__linux__)
// Linux特有实现
#elif defined(__APPLE__)
// macOS实现
#endif
在实际项目中,我通常会抽象平台差异,提供统一的接口层,这在开发跨平台中间件时尤为重要。