sem_wait 被信号打断不返回 EINTR?详解 POSIX 坑与解决方案
2025-03-26 21:55:37
sem_wait 被信号打断后不返回 EINTR?挖挖 POSIX 的坑
刚接触信号量(semaphore)想给程序加上多线程?结果碰上个怪事:按理说,只要没设置 SA_RESTART
标记,sem_wait
这个函数在阻塞时如果被信号(比如 SIGUSR1
)打断,应该会解除阻塞,返回 -1,并且把 errno
设置成 EINTR
。可实际情况是,我用 pthread_kill
给那个卡在 sem_wait
的工作线程发了 SIGUSR1
,信号确实收到了,处理函数也执行了,但 sem_wait
愣是不返回,线程继续阻塞在那儿,死活不给我 -1
和 EINTR
。更诡异的是,如果我在主线程里,发完信号之后再调用一下 sem_post
,工作线程里的 sem_wait
居然就返回了 0(表示成功?!),但 errno
却被设置成了 EINTR
(也就是 4)。这操作把我彻底搞懵了。难道是 NetBSD 的实现有毛病?还是我哪里没搞对?man
手册明明说 sem_wait
是遵循 POSIX.1 (ISO/IEC 9945-1:1996) 标准的。
下面是个简化版的代码,复现了这个问题:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <pthread.h>
#include <semaphore.h>
typedef struct workQueue_s
{
int full;
int empty;
sem_t work; // 工作信号量
int sock_c[10];
} workQueue_t;
// 信号处理函数 (我知道这里用 printf 不安全, 只是为了调试)
void signal_handler( int sig )
{
switch( sig )
{
case SIGUSR1:
// 注意:在信号处理函数中使用非异步信号安全的函数 (如printf) 是危险的
// 实际项目中应避免,这里仅作演示
printf( "Signal: 我是线程 %p\n", pthread_self() );
break;
}
}
extern int errno;
workQueue_t queue; // 工作队列结构体
pthread_t workerbees[8]; // 工作线程 ID 数组
void *BeeWork( void *t )
{
int RC;
pthread_t tid = pthread_self(); // 获取自己的线程ID
struct sigaction sa;
// 设置 SIGUSR1 的处理函数
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask); // 清空信号掩码
sa.sa_flags = 0; // 关键点:没有设置 SA_RESTART
if (sigaction( SIGUSR1, &sa, NULL ) == -1) {
perror("sigaction设置失败");
pthread_exit(NULL);
}
printf( "工作线程: 我是线程 %p\n", tid );
// 第一次等待信号量
printf("工作线程: 准备第一次调用 sem_wait\n");
RC = sem_wait( &queue.work );
printf( "工作线程: 第一次 sem_wait 返回, RC = %d, errno = %d\n", RC, errno );
// 第二次等待信号量,预期这里会被 SIGUSR1 打断
printf("工作线程: 准备第二次调用 sem_wait\n");
RC = sem_wait( &queue.work );
// 期望: RC = -1, errno = EINTR
// 实际(无额外sem_post): 阻塞在这里,不返回
// 实际(有额外sem_post): RC = 0, errno = EINTR (4)
printf( "工作线程: 第二次 sem_wait 返回, RC = %d, errno = %d\n", RC, errno );
pthread_exit( ( void * ) t );
}
int main()
{
int RC;
long tid = 0; // 只创建一个工作线程用于演示
pthread_attr_t attr;
pthread_attr_init( &attr );
pthread_attr_setdetachstate( &attr, PTHREAD_CREATE_JOINABLE );
queue.full = 0;
queue.empty = 0;
// 初始化信号量,初始值为 0
sem_init( &queue.work, 0, 0 );
printf( "主线程: 我是线程 %p\n", pthread_self() );
RC = pthread_create( &workerbees[tid], &attr, BeeWork, ( void * ) tid );
if (RC) {
printf("ERROR; return code from pthread_create() is %d\n", RC);
exit(-1);
}
pthread_attr_destroy( &attr );
// 等待工作线程启动并进入第一次 sem_wait
sleep( 1 ); // 等待一下确保工作线程跑起来了
printf("主线程: 调用 sem_post 唤醒第一次 sem_wait\n");
sem_post( &queue.work );
// 等待工作线程进入第二次 sem_wait
sleep( 1 ); // 等待一下确保工作线程进入了第二次 sem_wait
printf("主线程: 发送 SIGUSR1 给工作线程 %p\n", workerbees[tid]);
pthread_kill( workerbees[tid], SIGUSR1 );
// 等待信号处理函数执行
sleep( 1 ); // 等待一下,看看 sem_wait 是否被打断返回
// ---- 分界线:关键行为差异 ----
#ifdef WITH_EXTRA_SEM_POST
printf("主线程: (WITH_EXTRA_SEM_POST) 再调用一次 sem_post\n");
sem_post( &queue.work ); // 如果加上这句,第二次 sem_wait 会返回 RC=0, errno=4
#else
printf("主线程: (NO_EXTRA_SEM_POST) 不调用额外的 sem_post\n");
// 如果没有上面那句 sem_post, 第二次 sem_wait 会一直阻塞
#endif
sleep( 1 ); // 再等一下观察结果
// 等待工作线程结束
void *status;
RC = pthread_join(workerbees[tid], &status);
if (RC) {
printf("ERROR; return code from pthread_join() is %d\n", RC);
exit(-1);
}
printf("主线程: 工作线程 %ld 完成,退出状态 %ld\n", tid, (long)status);
sem_destroy(&queue.work); // 清理信号量
printf("主线程: 退出\n");
return( 0 );
}
/*
编译运行:
# 不加额外 sem_post
gcc test_sem.c -o test_sem_no_post -pthread
./test_sem_no_post
# 加上额外 sem_post
gcc test_sem.c -o test_sem_with_post -pthread -DWITH_EXTRA_SEM_POST
./test_sem_with_post
*/
不加那个额外的 sem_post
的输出:
主线程: 我是线程 0x7f......
工作线程: 我是线程 0x7f......
工作线程: 准备第一次调用 sem_wait
主线程: 调用 sem_post 唤醒第一次 sem_wait
工作线程: 第一次 sem_wait 返回, RC = 0, errno = 0
工作线程: 准备第二次调用 sem_wait
主线程: 发送 SIGUSR1 给工作线程 0x7f......
Signal: 我是线程 0x7f...... // 信号处理函数确实执行了
主线程: (NO_EXTRA_SEM_POST) 不调用额外的 sem_post
// 程序卡在这里,第二次 sem_wait 没有返回
加上那个额外的 sem_post
的输出:
主线程: 我是线程 0x7f......
工作线程: 我是线程 0x7f......
工作线程: 准备第一次调用 sem_wait
主线程: 调用 sem_post 唤醒第一次 sem_wait
工作线程: 第一次 sem_wait 返回, RC = 0, errno = 0
工作线程: 准备第二次调用 sem_wait
主线程: 发送 SIGUSR1 给工作线程 0x7f......
Signal: 我是线程 0x7f...... // 信号处理函数确实执行了
主线程: (WITH_EXTRA_SEM_POST) 再调用一次 sem_post
工作线程: 第二次 sem_wait 返回, RC = 0, errno = 4 // EINTR = 4 on NetBSD/Linux
主线程: 工作线程 0 完成,退出状态 0
主线程: 退出
我知道其实我可以直接在主线程退出时不管工作线程,但我就想搞明白这机制到底是怎么回事。我用 sem_wait
的初衷是想让工作线程池一直活着,主线程收到新的客户端连接(来自 Postfix)时,就用 sem_post
唤醒一个等得最久的线程去处理。我不希望每次来个连接就 pthread_create
,因为连接可能一秒钟来好几次,创建销毁线程太耗性能,搞不好还会让 Postfix 响应变慢。这东西是给 Postfix 做的一个策略守护进程,服务器挺忙的。
我到底漏了啥?还是 NetBSD(或者说 POSIX 标准本身)在这个点上就是有点绕?
一、问题剖析:信号与 sem_wait
的恩怨情仇
这事儿吧,还真不一定是 NetBSD 的锅,更可能是对 POSIX 标准里关于信号如何中断阻塞系统调用(比如 sem_wait
)的理解有点偏差。
关键点在于 :POSIX 标准规定,如果一个信号的处理函数被执行,并且该信号没有设置 SA_RESTART
标志,那么 ,如果 线程当时正阻塞在一个“慢”系统调用(比如 read
, write
, wait
, sem_wait
等)上,那么 这个系统调用 可能会 被中断。中断后,该调用通常会返回 -1,并将 errno
设置为 EINTR
。
注意这里的措辞:“可能” (may be interrupted)。 这不是百分之百保证的。
具体到 sem_wait
,情况更微妙一些:
- 信号到达的时机很重要:
- 如果信号到达,并且信号处理函数在
sem_wait
真正 让线程进入内核态阻塞之前 就执行完了,那sem_wait
根本就没机会被打断。它后面该怎么等信号量还是怎么等。 - 如果信号在线程阻塞期间到达,信号处理函数执行。当处理函数返回后,内核 会 检查到这个系统调用被信号打断了。但是 ,对于
sem_wait
这类等待资源的调用,内核在决定返回EINTR
之前,可能 会 再次检查 它等待的条件(也就是信号量的值)是否满足。- 情况A(你没调
sem_post
时): 内核一看,信号量还是 0 啊,等不到资源。虽然刚才是有信号打岔,但资源还没来,那就……继续等吧!所以sem_wait
就跟没事人一样继续阻塞了。POSIX 标准是允许 这种行为的,它并没有强制要求只要有信号打断(且无SA_RESTART
)就必须立刻返回EINTR
。内核可以选择重新尝试等待。 - 情况B(你调了
sem_post
时): 信号处理函数跑完了。你紧接着调用了sem_post
,把信号量加了 1。现在内核回来干活,它发现:嘿,这个sem_wait
刚才被信号打断过!同时,它检查等待条件,发现信号量现在大于 0 了!它可以成功完成sem_wait
操作(把信号量减 1)。这种情况下,POSIX 定义的行为有点反直觉:sem_wait
会 成功返回 0 (因为它确实等到了信号量并且减了 1),但同时会设置errno
为EINTR
,表示“我成功了,但过程中被信号打扰过”。
- 情况A(你没调
- 如果信号到达,并且信号处理函数在
所以,你观察到的两种现象:
- 不
sem_post
就一直阻塞:这是允许的行为(内核重新尝试等待)。 sem_post
后返回 0,errno
为EINTR
:这是标准定义的“成功但被打断”的特殊情况。
这事儿在 Linux 的 sem_timedwait(3)
man page 里也有提到类似的行为:如果信号中断了等待,调用失败,errno
为 EINTR
。但如果是在 sem_post
之后被信号中断(或在检查信号之前 sem_post
已经发生),调用可能 成功返回 0,同时 errno
设为 EINTR
。虽然 sem_wait
的 man page 可能没写这么细,但底层机制是相通的。
补充一点: 像你在信号处理函数里用 printf
这种非异步信号安全(async-signal-safe)的函数,确实是不推荐的,可能导致各种奇怪问题,甚至死锁。不过在这个特定案例中,去掉 printf
结果一样,说明问题核心还是 sem_wait
和信号交互的机制本身。
二、靠谱的解决方案:如何优雅地中断等待中的线程
既然直接依赖 sem_wait
被信号打断返回 EINTR
不那么靠谱,我们需要更稳健的方法来通知并唤醒阻塞在 sem_wait
上的线程,让它们干净地退出。
下面是几种常用的处理方式:
方案一:信号 + 退出标志位 (配合 sem_post
唤醒)
这是最接近你原始思路的改进版,也是比较通用的模式。
-
原理:
- 定义一个全局(或线程共享的)
volatile sig_atomic_t
类型的标志位,比如quit_flag
,初始化为 0。volatile
告诉编译器不要优化掉对这个变量的读写,sig_atomic_t
保证了在信号处理函数中对它的写入是原子操作,不会被其他信号中断。 - 信号处理函数(比如
SIGUSR1
或SIGTERM
的处理函数)里只做一件事:把quit_flag
设置为 1。这是异步信号安全的操作。 - 工作线程的主循环里,每次
sem_wait
返回后(无论是正常等到信号量,还是因为别的什么原因醒来),都立刻检查quit_flag
的值。 - 如果
quit_flag
是 1,说明收到了退出信号,线程就执行清理工作然后退出循环。 - 主线程想要结束工作线程时:先发送信号(比如
SIGUSR1
),让信号处理函数设置quit_flag
。然后,调用sem_post
!这一步是关键,目的是确保即使信号本身没能让sem_wait
返回EINTR
,这个sem_post
也能把阻塞的线程唤醒,让它有机会去检查quit_flag
。可能需要对每个等待的工作线程都sem_post
一次(或者根据你的逻辑,如果信号量是计数型的,post 足够多次)。
- 定义一个全局(或线程共享的)
-
代码示例(修改
BeeWork
和main
):#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <signal.h> #include <pthread.h> #include <semaphore.h> #include <unistd.h> // for sleep // ... (workQueue_t 定义不变) ... volatile sig_atomic_t quit_flag = 0; // 全局退出标志 // 信号处理函数 - 异步信号安全 void signal_handler( int sig ) { // 只做原子性、异步信号安全的操作 quit_flag = 1; // 这里不能有 printf 或其他不安全的函数 } // ... (queue, workerbees 定义不变) ... void *BeeWork( void *t ) { int RC; pthread_t tid = pthread_self(); struct sigaction sa; // 设置信号处理 (SIGUSR1 或更常用的 SIGTERM, SIGINT) sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; // 不设置 SA_RESTART // 通常捕获 SIGTERM 或 SIGINT 用于正常退出 if (sigaction( SIGTERM, &sa, NULL ) == -1 || sigaction( SIGINT, &sa, NULL ) == -1) { perror("sigaction设置失败"); pthread_exit(NULL); } printf( "工作线程 %p: 开始工作...\n", tid ); while (1) { printf("工作线程 %p: 等待任务 (sem_wait)...\n", tid); // 循环等待信号量,处理 EINTR (虽然可能不按预期出现) while ((RC = sem_wait( &queue.work )) == -1 && errno == EINTR) { printf("工作线程 %p: sem_wait 被 EINTR 中断,继续等待...\n", tid); // 检查退出标志,即使在 EINTR 时也要检查 if (quit_flag) { printf("工作线程 %p: 在 EINTR 后检测到退出标志,退出。\n", tid); goto cleanup; // 或者 break 后在循环外检查 } continue; // 继续循环调用 sem_wait } // 检查 sem_wait 是否失败 (非 EINTR 错误) if (RC == -1) { perror("工作线程: sem_wait 失败"); break; // 出现其他错误,退出循环 } // 关键:每次 sem_wait 返回后检查退出标志 if (quit_flag) { printf("工作线程 %p: 检测到退出标志,准备退出...\n", tid); // 注意:如果 sem_wait 是因为主线程 post 而成功返回的, // 但目的是让线程退出,这里可能需要把信号量“还”回去, // 或者主线程在 post 之后就不再期望这个信号量被用于工作。 // sem_post(&queue.work); // 视你的具体逻辑决定是否需要 break; // 跳出循环 } // --- 处理正常工作 --- printf("工作线程 %p: 收到任务!(模拟处理...)\n", tid); // 假设这里从队列取出数据并处理... sleep(1); // 模拟工作耗时 printf("工作线程 %p: 任务处理完毕。\n", tid); // --- 工作处理结束 --- } cleanup: printf("工作线程 %p: 清理并退出。\n", tid); pthread_exit( ( void * ) t ); } int main() { long tid = 0; pthread_attr_t attr; // ... (初始化 queue, semaphore, attr) ... sem_init( &queue.work, 0, 0 ); // 初始值为0,需要 post 才有任务 pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); printf( "主线程: 创建工作线程...\n"); pthread_create( &workerbees[tid], &attr, BeeWork, ( void * ) tid ); pthread_attr_destroy(&attr); // 模拟发放几个任务 sleep(1); printf("主线程: 发放第一个任务\n"); sem_post(&queue.work); sleep(2); // 让工作线程处理一下 printf("主线程: 发放第二个任务\n"); sem_post(&queue.work); sleep(2); // ---- 请求工作线程退出 ---- printf("主线程: 发送退出信号 (SIGTERM)...\n"); // 给工作线程发送信号,让其设置 quit_flag pthread_kill( workerbees[tid], SIGTERM ); // 使用 SIGTERM 更标准 // 关键一步:发送一次 sem_post 确保线程能从 sem_wait 唤醒来检查标志 printf("主线程: 调用 sem_post 确保线程被唤醒...\n"); sem_post( &queue.work ); // 等待工作线程结束 printf("主线程: 等待工作线程退出 (pthread_join)...\n"); pthread_join( workerbees[tid], NULL ); printf("主线程: 工作线程已退出。\n"); sem_destroy(&queue.work); printf("主线程: 退出。\n"); return 0; }
-
安全建议:
- 信号处理函数一定要尽可能简单,只做设置
volatile sig_atomic_t
标志位这种原子操作。 - 使用
SIGTERM
或SIGINT
作为退出信号通常比SIGUSR1
更符合常规实践。SIGUSR1/2
一般用于应用自定义的特殊目的。
- 信号处理函数一定要尽可能简单,只做设置
-
进阶使用技巧:
- 如果需要给多个工作线程发送退出信号并确保它们都退出,主线程可能需要
pthread_kill
每个线程,并且sem_post
相应次数(或者使用广播机制,如下面的方案)。 - 可以考虑使用
pthread_sigmask
在工作线程的关键代码段(比如修改共享数据时)临时阻塞退出信号,防止在不恰当的时候被中断。
- 如果需要给多个工作线程发送退出信号并确保它们都退出,主线程可能需要
方案二:使用管道(Pipe)或 eventfd 进行通知
不用信号,改用 I/O 事件来通知。这种方法更清晰,也更容易集成到基于 select/poll/epoll
的事件循环中。
-
原理:
- 主线程和工作线程之间创建一个管道(
pipe()
)或者 Linux 特有的eventfd()
。 - 工作线程不只是等
sem_wait
,而是可以设计成同时等待信号量和管道(或 eventfd)的可读事件。不过,更简单且符合你场景的做法是:工作线程主要还是阻塞在sem_wait
上等待工作 。 - 主线程想让工作线程退出时,往管道里写一个字节(或者给 eventfd 写入一个值)。
- 同时,主线程必须调用
sem_post
把阻塞在sem_wait
的线程唤醒。 - 工作线程从
sem_wait
醒来后,除了检查是否有真实工作,还要非阻塞地 检查一下管道(或 eventfd)里有没有数据。如果有,就是收到了退出通知,于是退出。
- 主线程和工作线程之间创建一个管道(
-
代码示例(关键部分):
#include <unistd.h> // for pipe, read, write #include <fcntl.h> // for fcntl, O_NONBLOCK // 在共享数据结构中加入管道符 typedef struct shared_data_s { sem_t work_sem; int notify_pipe[2]; // 管道:[0]读端, [1]写端 volatile sig_atomic_t quit_flag; // 也可以保留标志位作为辅助 // ... 其他数据 ... } shared_data_t; shared_data_t shared_data; void *WorkerThread(void *arg) { int my_pipe_read_fd = shared_data.notify_pipe[0]; pthread_t tid = pthread_self(); char dummy_buf; // 用于从管道读取 printf("工作线程 %p: 开始,监听管道读端 %d\n", tid, my_pipe_read_fd); while (1) { printf("工作线程 %p: 等待 sem_wait...\n", tid); int rc = sem_wait(&shared_data.work_sem); // 检查是否收到退出信号 (管道方式) // 使用非阻塞读尝试从管道读数据 errno = 0; // 清除 errno 以便正确判断 read 返回值 ssize_t bytes_read = read(my_pipe_read_fd, &dummy_buf, 1); if (bytes_read > 0) { printf("工作线程 %p: 从管道读到数据,判定为退出信号。\n", tid); // 可能需要把信号量还回去,如果这次唤醒只是为了退出 // sem_post(&shared_data.work_sem); break; // 退出循环 } else if (bytes_read == 0) { // 管道写端关闭,也认为是退出信号 printf("工作线程 %p: 管道写端关闭,退出。\n", tid); break; } else { // bytes_read == -1 if (errno == EAGAIN || errno == EWOULDBLOCK) { // 管道里没数据,正常情况,继续检查 sem_wait 结果 // (如果 errno 不是 EAGAIN/EWOULDBLOCK,那可能是真错误) } else { perror("工作线程: read 管道失败"); break; // 异常情况,退出 } } // 检查 sem_wait 的结果 (假设管道没收到退出信号) if (rc == -1) { if (errno == EINTR) { // 虽然可能不触发,但处理是好习惯 printf("工作线程 %p: sem_wait 被 EINTR 中断,继续...\n", tid); continue; // 回到循环顶部继续等待 } else { perror("工作线程: sem_wait 失败"); break; // 其他错误,退出 } } // sem_wait 正常返回,处理工作... printf("工作线程 %p: 获得工作信号量,处理任务...\n", tid); // ... 处理队列中的任务 ... sleep(1); // 模拟工作 printf("工作线程 %p: 任务完成。\n", tid); } printf("工作线程 %p: 退出。\n", tid); return NULL; } int main() { pthread_t worker; long t = 0; // 初始化信号量 sem_init(&shared_data.work_sem, 0, 0); // 创建管道 if (pipe(shared_data.notify_pipe) == -1) { perror("创建管道失败"); return 1; } // 将管道读端设为非阻塞,以便工作线程检查退出信号时不阻塞 int flags = fcntl(shared_data.notify_pipe[0], F_GETFL, 0); fcntl(shared_data.notify_pipe[0], F_SETFL, flags | O_NONBLOCK); printf("主线程: 创建工作线程...\n"); pthread_create(&worker, NULL, WorkerThread, (void *)t); sleep(1); // 等线程启动 // 模拟发任务 printf("主线程: 发送任务 1\n"); sem_post(&shared_data.work_sem); sleep(2); // ---- 请求退出 ---- printf("主线程: 请求工作线程退出...\n"); // 1. 往管道写入一个字节,发出通知 printf("主线程: 向管道写端 %d 写入数据...\n", shared_data.notify_pipe[1]); char exit_signal = 'q'; if (write(shared_data.notify_pipe[1], &exit_signal, 1) != 1) { perror("主线程: 写入管道失败"); // 即使写入失败,也尝试 post 唤醒,也许能基于其他逻辑退出 } // 2. 关闭管道写端,让读端在没有数据时也能感知到 (read 返回 0) close(shared_data.notify_pipe[1]); // 很重要! // 3. 调用 sem_post 确保线程从 sem_wait 唤醒 printf("主线程: 调用 sem_post 唤醒线程检查管道...\n"); sem_post(&shared_data.work_sem); printf("主线程: 等待工作线程退出...\n"); pthread_join(worker, NULL); // 清理 close(shared_data.notify_pipe[0]); // 关闭读端 sem_destroy(&shared_data.work_sem); printf("主线程: 退出。\n"); return 0; }
-
安全与优点:
- 管道是标准的进程间/线程间通信方式,比信号更清晰可控。
- 避免了信号处理函数的诸多限制和陷阱。
eventfd
(Linux-specific) 比管道更轻量,开销更小,专门用于事件通知。
-
进阶使用技巧:
- 如果工作线程本身就需要用
select/poll/epoll
等待多个事件源(比如网络套接字),那么把这个通知管道(或 eventfd)的读端加进去一起等就非常自然了。主线程只需写管道/eventfd,工作线程的事件循环就能自动感知到。
- 如果工作线程本身就需要用
方案三:“毒丸”(Poison Pill)/ 哨兵值
如果你的工作队列是传递数据的,可以用一个特殊的数据项来表示结束信号。
-
原理:
- 定义一个特殊的任务值(比如 NULL 指针,一个包含特殊标记的结构体等),作为“结束”信号。
- 主线程想结束工作线程时,往工作队列里放入这个“毒丸”,然后像正常发布任务一样调用
sem_post
。 - 工作线程从
sem_wait
醒来,像往常一样去队列取任务。如果取到的是“毒丸”,就知道该退出了。
-
代码示例(概念性):
// 假设 work_item_t 是你的任务类型 #define POISON_PILL NULL // 假设 NULL 表示结束 void *WorkerThread(void *arg) { while (1) { sem_wait(&queue.work); // 等待任务信号量 // 从队列中获取任务 (需要加锁保护队列访问) pthread_mutex_lock(&queue_mutex); work_item_t* task = get_task_from_queue(&queue); pthread_mutex_unlock(&queue_mutex); if (task == POISON_PILL) { // 收到了毒丸! printf("工作线程 %p: 收到毒丸,退出...\n", pthread_self()); // 把毒丸放回去,让其他等待的线程也能收到并退出 (如果是线程池) // add_task_to_queue(&queue, POISON_PILL); // 需要加锁 // sem_post(&queue.work); // 通知下一个线程 break; } // 处理正常任务... process_task(task); } return NULL; } int main() { // ... 初始化 ... // 工作一段时间 ... // ---- 请求退出 ---- printf("主线程: 放入毒丸到队列...\n"); pthread_mutex_lock(&queue_mutex); add_task_to_queue(&queue, POISON_PILL); // 放入毒丸 pthread_mutex_unlock(&queue_mutex); // 通知一个工作线程去取 (如果是线程池,可能要 post 多次) sem_post(&queue.work); // ... 等待线程结束 (pthread_join) ... // ... 清理 ... return 0; }
-
适用性:
- 非常适合生产者-消费者模型的工作队列。
- 实现相对简单直观。
-
注意事项:
- 需要确保队列操作是线程安全的(通常需要互斥锁)。
- 如果使用了线程池(多个工作线程),主线程需要放入足够多的“毒丸”并调用相应次数的
sem_post
,以确保所有线程都能收到退出信号并退出。或者,一个线程收到毒丸后,再把它放回队列并sem_post
,形成链式退出。
选哪种方案取决于你的具体场景和偏好。对于需要明确、可靠地中断 sem_wait
的场景,方案一(信号+标志位+sem_post唤醒) 和 方案二(管道/eventfd+sem_post唤醒) 通常是更健壮的选择。方案三(毒丸)则在队列逻辑比较简单直接时非常方便。