Reputation: 2135
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
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
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