使用 SIGEV_THREAD 时,如何为所有 timer_create 回调重用单个线程?
Posted
技术标签:
【中文标题】使用 SIGEV_THREAD 时,如何为所有 timer_create 回调重用单个线程?【英文标题】:How can I reuse a single thread for all timer_create callbacks when using SIGEV_THREAD? 【发布时间】:2021-10-03 15:11:05 【问题描述】:我有一个定时运行的计时器。我使用 SIGEV_THREAD 选项使用 timer_create() 创建计时器。当计时器到期时,这将在线程上触发回调,而不是向进程发送 SIGALRM 信号。问题是,每次我的计时器到期时,都会产生一个新线程。这意味着程序可能产生数百个线程,具体取决于计时器的频率。 最好有一个处理回调的一个线程。我可以在使用带有信号的 timer_create() 时执行此操作(通过使用 sigaction),但不能仅使用线程。 有没有办法不使用信号,但仍然让计时器在单个现有线程中通知进程? 或者我什至应该从性能角度(线程与信号)担心这一点?
编辑:
我的解决方案是使用 SIGEV_SIGNAL 和 pthread_sigmask()。因此,我继续依靠信号来了解我的计时器何时到期,但我可以 100% 确定只有一个线程(由我创建)用于捕获信号并执行适当的操作。
【问题讨论】:
大概新生成的线程会在回调完成后终止,但如果定时器频繁触发,这听起来仍然是一个非常昂贵的选择。 @500-InternalServerError 这正是我的想法。为每个计时器到期创建和销毁一个线程似乎是很多低效的工作。但据我所知,这就是 API 的工作方式。从我所见,除了使用信号之外,没有其他选择可以将计时器触发回调保持在单个线程上。 @Synthetix +1 因为有趣的问题(从快速扫描相关的 Linux 代码来看,它似乎不可能开箱即用,但也许有人会有一个聪明的解决方案) - 但为什么厌恶信号? 考虑重新设计您的程序以改用timerfd_create
。
@Shawn 我最终做了后者,使用 SIGEV_SIGNAL 和 pthread_sigmask()。
【参考方案1】:
tl;dr:SIGEV_THREAD
不能基于信号工作的基本前提是错误的——信号是产生新线程的底层机制。 glibc 不支持为多个回调重用同一个线程。
timer_create
的行为与您想象的不完全一样 - 它的第二个参数 struct sigevent *restrict sevp
包含字段 sigevent_notify
,该字段具有以下 documentation:
SIGEV_THREAD
通过调用 sigev_notify_function "通知进程" if" 它是一个新线程的启动函数。(在 这里的实现可能性是每个定时器通知 可能会导致创建一个新线程,或者单个线程 被创建用于接收所有通知。) 该函数被调用 以 sigev_value 作为其唯一参数。如果 sigev_notify_attributes 是 不为 NULL,它应该指向一个 pthread_attr_t 结构,该结构定义 新线程的属性(参见 pthread_attr_init(3))。
事实上,如果我们查看 glibc 的 implementation:
else
/* Create the helper thread. */
pthread_once (&__helper_once, __start_helper_thread);
...
struct sigevent sev =
.sigev_value.sival_ptr = newp,
.sigev_signo = SIGTIMER,
.sigev_notify = SIGEV_SIGNAL | SIGEV_THREAD_ID,
._sigev_un = ._pad = [0] = __helper_tid ;
/* Create the timer. */
INTERNAL_SYSCALL_DECL (err);
int res;
res = INTERNAL_SYSCALL (timer_create, err, 3,
syscall_clockid, &sev, &newp->ktimerid);
我们可以看到__start_helper_thread
的implementation:
void
attribute_hidden
__start_helper_thread (void)
...
int res = pthread_create (&th, &attr, timer_helper_thread, NULL);
然后关注timer_helper_thread
的implementation:
static void *
timer_helper_thread (void *arg)
...
/* Endless loop of waiting for signals. The loop is only ended when
the thread is canceled. */
while (1)
...
int result = SYSCALL_CANCEL (rt_sigtimedwait, &ss, &si, NULL, _NSIG / 8);
if (result > 0)
if (si.si_code == SI_TIMER)
struct timer *tk = (struct timer *) si.si_ptr;
...
(void) pthread_create (&th, &tk->attr,
timer_sigev_thread, td);
所以 - 至少在 glibc 级别 - 当使用 SIGEV_THREAD
时,您必然使用信号来向线程发出信号以创建函数 - 而且您的主要动机似乎是避免使用警报信号。
在 Linux 源代码级别,计时器似乎只处理信号 - kernel/time/posix_timers.c
中的 posix_timer_event
函数(由 kernel/time/alarmtimer.c
中的 alarm_handle_timer
调用)直接进入 signal.c
中的代码,这必然会发送一个信号。因此,在使用timer_create
时似乎无法避免信号,并且您的问题中的这个语句 - “当计时器到期时,这将在线程上触发回调,而不是向进程发送 SIGALRM 信号。” - 是假的(虽然信号不一定是SIGALRM
)。
换句话说 - 与信号相比,SIGEV_THREAD
似乎没有性能优势。信号仍将用于触发线程的创建,并且您正在增加创建新线程的额外开销。
【讨论】:
感谢您的出色解释。现在这是有道理的。我将继续依靠 SIGALRM 来告诉我何时在计时器到期时触发操作,并使用我自己的线程作为信号处理程序。这样,我可以确定信号处理程序线程只创建一次并在程序期间运行。这似乎是最有效的方式。以上是关于使用 SIGEV_THREAD 时,如何为所有 timer_create 回调重用单个线程?的主要内容,如果未能解决你的问题,请参考以下文章
当其中一个是本地引用时,如何为类型约束中的引用编写生命周期?
Windows 如何为绿色软件运行时添加参数 如最小化,无窗口运行