使用 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_SIGNALpthread_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 回调重用单个线程?的主要内容,如果未能解决你的问题,请参考以下文章

在 Slick 中,如何为 Table 提供默认列

如何为用户定义的类型专门化 std::hash<T>?

当其中一个是本地引用时,如何为类型约束中的引用编写生命周期?

Windows 如何为绿色软件运行时添加参数 如最小化,无窗口运行

为孩子提供属性时如何为 React.cloneElement 分配正确的类型?

带有SpriteKit的iOS通用设备应用程序,如何为所有视图缩放节点?