在Linux上,您可以设置自定义信号处理程序(例如使用signal()),其中包含等待另一个信号(例如使用sigsuspend())。然后,您可以使用pthread_kill()或tgkill()发送信号。重要的是要使用所谓的“实时信号”,因为正常的信号如SIGUSR1和SIGUSR2不会排队,这意味着它们在高负载条件下可能会丢失。您可以多次发送信号,但只接收一次,因为在信号处理程序运行期间,相同类型的新信号将被忽略。因此,如果有并发线程执行PAUSE/RESUME操作,则可能会丢失RESUME事件并导致死锁。另一方面,挂起的实时信号(例如SIGRTMIN+1和SIGRTMIN+2)不会被去重,因此队列中可能存在多个相同的rt信号。
免责声明:我还没有尝试过这个方法。但理论上应该能够工作。
此外,请参阅man 7 signal-safety。其中有一份调用列表,您可以安全地在信号处理程序中调用。幸运的是,sigsuspend()似乎是其中之一。
更新:我在这里有正在工作的代码。
#define _GNU_SOURCE
#include <signal.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <errno.h>
#include <sys/resource.h>
#include <time.h>
#define PTHREAD_XSIG_STOP (SIGRTMIN+0)
#define PTHREAD_XSIG_CONT (SIGRTMIN+1)
#define PTHREAD_XSIGRTMIN (SIGRTMIN+2)
pthread_t main_thread;
sem_t pthread_pause_sem;
pthread_once_t pthread_pause_once_ctrl = PTHREAD_ONCE_INIT;
void pthread_pause_once(void) {
sem_init(&pthread_pause_sem, 0, 1);
}
#define pthread_pause_init() (pthread_once(&pthread_pause_once_ctrl, &pthread_pause_once))
#define NSEC_PER_SEC (1000*1000*1000)
struct timespec timespec_normalise(struct timespec ts)
{
while(ts.tv_nsec >= NSEC_PER_SEC) {
++(ts.tv_sec); ts.tv_nsec -= NSEC_PER_SEC;
}
while(ts.tv_nsec <= -NSEC_PER_SEC) {
--(ts.tv_sec); ts.tv_nsec += NSEC_PER_SEC;
}
if(ts.tv_nsec < 0) {
--(ts.tv_sec); ts.tv_nsec = (NSEC_PER_SEC + ts.tv_nsec);
}
return ts;
}
void pthread_nanosleep(struct timespec t) {
struct timespec wake;
clock_gettime(CLOCK_MONOTONIC, &wake);
t = timespec_normalise(t);
wake.tv_sec += t.tv_sec;
wake.tv_nsec += t.tv_nsec;
wake = timespec_normalise(wake);
while(clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &wake, NULL)) if(errno!=EINTR) break;
return;
}
void pthread_nsleep(time_t s, long ns) {
struct timespec t;
t.tv_sec = s;
t.tv_nsec = ns;
pthread_nanosleep(t);
}
void pthread_sleep(time_t s) {
pthread_nsleep(s, 0);
}
void pthread_pause_yield() {
sem_wait(&pthread_pause_sem);
sem_post(&pthread_pause_sem);
pthread_yield();
}
void pthread_pause_handler(int signal) {
sem_post(&pthread_pause_sem);
if(signal == PTHREAD_XSIG_STOP) {
sigset_t sigset;
sigfillset(&sigset);
sigdelset(&sigset, PTHREAD_XSIG_STOP);
sigdelset(&sigset, PTHREAD_XSIG_CONT);
sigsuspend(&sigset);
} else return;
}
void pthread_pause_enable() {
pthread_pause_init();
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, PTHREAD_XSIG_STOP);
sigaddset(&sigset, PTHREAD_XSIG_CONT);
const struct sigaction pause_sa = {
.sa_handler = pthread_pause_handler,
.sa_mask = sigset,
.sa_flags = SA_RESTART,
.sa_restorer = NULL
};
sigaction(PTHREAD_XSIG_STOP, &pause_sa, NULL);
sigaction(PTHREAD_XSIG_CONT, &pause_sa, NULL);
pthread_sigmask(SIG_UNBLOCK, &sigset, NULL);
}
void pthread_pause_disable() {
pthread_pause_init();
sem_wait(&pthread_pause_sem);
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset, PTHREAD_XSIG_STOP);
sigaddset(&sigset, PTHREAD_XSIG_CONT);
pthread_sigmask(SIG_BLOCK, &sigset, NULL);
sem_post(&pthread_pause_sem);
}
int pthread_pause(pthread_t thread) {
sem_wait(&pthread_pause_sem);
while(pthread_kill(thread, PTHREAD_XSIG_STOP) == EAGAIN) usleep(1000);
pthread_pause_yield();
return 0;
}
int pthread_unpause(pthread_t thread) {
sem_wait(&pthread_pause_sem);
while(pthread_kill(thread, PTHREAD_XSIG_CONT) == EAGAIN) usleep(1000);
pthread_pause_yield();
return 0;
}
void *thread_test() {
while(1) {
pthread_pause_disable();
printf("Running!\n");
pthread_pause_enable();
pthread_pause(main_thread);
pthread_unpause(main_thread);
pthread_unpause(main_thread);
}
}
int main() {
pthread_t t;
main_thread = pthread_self();
pthread_pause_enable();
pthread_create(&t, NULL, thread_test, NULL);
while(1) {
pthread_pause(t);
printf("PAUSED\n");
pthread_sleep(3);
printf("UNPAUSED\n");
pthread_unpause(t);
pthread_sleep(1);
pthread_pause(t);
pthread_unpause(t);
}
pthread_join(t, NULL);
printf("DIEDED!\n");
}
我正在开发一个名为“pthread_extra”的库,其中包括像这样的内容和更多的功能。很快会发布。
更新2:当快速调用pause/unpause时,仍然会导致死锁(已删除sleep()调用)。glibc中的Printf()实现有互斥锁,因此如果您挂起打印Printf()中间的线程,然后想要从您计划稍后取消暂停该线程的线程进行printf(),它将永远不会发生,因为printf()被锁定。不幸的是,我已经删除了Printf()并在线程中只运行了空的while循环,但在高暂停/取消暂停率下仍然出现死锁。我不知道为什么。也许(即使是实时)Linux信号不是100%安全的。有实时信号队列,也许它会溢出或类似的问题...
更新3:我认为我已经成功解决了死锁问题,但必须完全重写大部分代码。现在每个线程都有一个(sig_atomic_t)变量,用于保存该线程是否应该运行的状态。工作方式有点像条件变量。pthread_(un)pause()自动透明地记住每个线程的这个变量。我没有两个信号。现在我只有一个信号。该信号的处理程序查看该变量,并且仅在该变量表明该线程不应该运行时才在sigsuspend()上阻塞。否则它从信号处理程序返回。为了挂起/恢复线程,我现在设置sig_atomic_t变量以期望的状态并调用那个信号(这对于暂停和恢复是相同的)。重要的是使用实时信号来确保在修改状态变量后处理程序将实际运行。由于线程状态数据库,代码有点复杂。尽快简化后,我将在单独的解决方案中共享代码。但我想保留此处的两个信号版本,因为它可以工作,我喜欢它的简单性,也许人们会给我们更多优化的见解。
更新4:我已经修复了原始代码中的死锁(不需要辅助变量持有状态),通过使用两个信号的单个处理程序以及稍微优化信号队列。使用helgrind显示Printf()仍然存在一些问题,但它并非由我的信号引起,即使我根本不调用pause/unpause也会发生。总体而言,这仅在LINUX上进行了测试,不确定代码的可移植性如何,因为似乎存在一些未记录的信号处理程序行为最初导致死锁。
请注意,pause/unpause不能嵌套。如果您暂停了3次,并取消暂停1次,则线程将运行。如果需要这样的行为,则应创建某种包装器,该包装器将计算嵌套级别并相应地发出信号。
更新5:我通过以下更改提高了代码的鲁棒性:使用信号量确保暂停/恢复调用的正确序列化。这将解决所有剩余的死锁问题,现在您可以确信当暂停调用返回时,目标线程已经实际上被暂停了。这也解决了信号队列溢出的问题。我还添加了SA_RESTART标志,它可以防止内部信号中断IO等待。休眠/延迟仍然需要手动重新启动,但我提供了一个方便的封装函数pthread_nanosleep()来做到这一点。
更新6:我意识到简单地重启nanosleep()是不够的,因为这样超时时间不会在线程暂停时运行。因此,我修改了pthread_nanosleep()函数,将超时间隔转换为未来的绝对时间点,并休眠至该时间点。同时,我隐藏了信号量初始化,这样用户就不需要自己处理。