Synthetix
Synthetix

Reputation: 2135

How can I reuse a single thread for all timer_create callbacks when using SIGEV_THREAD?

I have a timer that runs at regular intervals. I create the timer using timer_create() using the SIGEV_THREAD option. This will fire a callback on a thread when the timer expires, rather than send a SIGALRM signal to the process. The problem is, every time my timer expires, a new thread is spawned. This means the program spawns potentially hundreds of threads, depending on the frequency of the timer. What would be better is to have one thread that handles the callbacks. I can do this when using timer_create() with signals (by using sigaction), but not threads only. Is there any way to not use signals, but still have the timer notify the process in a single existing thread? Or should I even worry about this from a performance perspective (threads vs signals)?

EDIT:

My solution was to use SIGEV_SIGNAL and pthread_sigmask(). So, I continue to rely on signals to know when my timer expires, but I can be 100% sure only a single thread (created by me) is being used to capture the signals and execute the appropriate action.

Upvotes: 1

Views: 1455

Answers (2)

stsp
stsp

Reputation: 400

Is there any way to not use signals, but still have the timer notify the process in a single existing thread?

There is: timerfd API (timerfd_create()) provides such means.

Overall, if you can use timerfd_create() (portability is not your concern), then you should. Because SIGEV_THREAD is completely broken: it can spawn the arbitrary amount of threads if your handler is not fast enough, and because of that you can't even use timer_getoverrun() reliably.

SIGEV_THREAD_ID is not a solution either because, while being non-portable, you also can't easily get the tid for it as not many libc'es implement pthread_getthreadid_np() to get tid from pthread_t.

So if you have to use timer_create() then SIGEV_SIGNAL + pthread_sigmask() is what "kinda" works. At least you can use timer_getoverrun() there, but its still very weird: timer_getoverrun() is specified to provide the amount of extra overruns that happened between signal queuing and dequeuing. But if you ask how about overruns that happened after the signal dequeue but before your prog called timer_getoverrun(), then you won't find any answer unless you look into the linux sources. From there it can be seen that linux actually measures an overruns between subsequent signal dequeuings, so it can be though of as adding the lost overruns to the next signal delivery. Plus, as you already mentioned, directing the timer signals to the particular thread is not always reliable: some unaware lib could create the thread and alter sigmask to use it with its own timer, but would instead steal your timer's signals. And so on.

Its good that linux provides the legacy-free APIs like timerfd_create(), but when you want to write the portable code, you quickly find yourself out of working solutions.

Upvotes: 0

Daniel Kleinstein
Daniel Kleinstein

Reputation: 5512

tl;dr: The basic premise that SIGEV_THREAD doesn't work based on signals is false - signals are the underlying mechanism through which new threads are spawned. glibc has no support for reutilizing the same thread for multiple callbacks.


timer_create doesn't behave exactly the way you think - its second parameter, struct sigevent *restrict sevp contains the field sigevent_notify which has following documentation:

SIGEV_THREAD

Notify the process by invoking sigev_notify_function "as if" it were the start function of a new thread. (Among the implementation possibilities here are that each timer notification could result in the creation of a new thread, or that a single thread is created to receive all notifications.) The function is invoked with sigev_value as its sole argument. If sigev_notify_attributes is not NULL, it should point to a pthread_attr_t structure that defines attributes for the new thread (see pthread_attr_init(3)).

And indeed, if we look at glibc's 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);

And we can see __start_helper_thread's implementation:

void
attribute_hidden
__start_helper_thread (void)
{
  ...
  int res = pthread_create (&th, &attr, timer_helper_thread, NULL);

And follow along to timer_helper_thread's 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);

So - at least at the glibc level - when using SIGEV_THREAD you are necessarily using signals to signal a thread to create the function anyways - and it seems like your primary motivation to begin with was avoiding the use of alarm signals.

At the Linux source code level, timers seems to work on signals alone - the posix_timer_event in kernel/time/posix_timers.c function (called by alarm_handle_timer in kernel/time/alarmtimer.c) goes straight to code in signal.c that necessarily sends a signal. So it doesn't seem possible to avoid signals when working with timer_create, and this statement from your question - "This will fire a callback on a thread when the timer expires, rather than send a SIGALRM signal to the process." - is false (though it's true that the signal doesn't have to be SIGALRM).

In other words - there seem to be no performance benefits to be gained from SIGEV_THREAD as opposed to signals. Signals will still be used to trigger the creation of threads, and you're adding the additional overhead of creating new threads.

Upvotes: 2

Related Questions