pthreadpool: Fix starvation after fork
authorVolker Lendecke <vl@samba.org>
Wed, 29 Nov 2017 15:45:40 +0000 (16:45 +0100)
committerKarolin Seeger <kseeger@samba.org>
Thu, 14 Dec 2017 11:22:18 +0000 (12:22 +0100)
commitb418ab369876f6aff08ee564a7f07d9e5dc0bcc0
tree5228812927d930a4267ca748e7724f35de980959
parent253005363e36b3f30fb59f3f3b8dadc490891dfe
pthreadpool: Fix starvation after fork

After the race is before the race:

1) Create an idle thread
2) Add a job: This won't create a thread anymore
3) Immediately fork

The idle thread will be woken twice before it's actually woken up: Both
pthreadpool_add_job and pthreadpool_prepare_pool call cond_signal, for
different reasons. We must look at pool->prefork_cond first because otherwise
we will end up in a blocking job deep within a fork call, the helper thread
must take its fingers off the condvar as quickly as possible.  This means that
after the fork there's no idle thread around anymore that would pick up the job
submitted in 2). So we must keep the idle threads around across the fork.

The quick solution to re-create one helper thread in pthreadpool_parent has a
fatal flaw: What do we do if that pthread_create call fails? We're deep in an
application calling fork(), and doing fancy signalling from there is really
something we must avoid.

This has one potential performance issue: If we have hundreds of idle threads
(do we ever have that) during the fork, the call to pthread_mutex_lock on the
fork_mutex from pthreadpool_server (the helper thread) will probably cause a
thundering herd when the _parent call unlocks the fork_mutex. The solution for
this to just keep one idle thread around. But this adds code that is not
strictly required functionally for now.

More detailed explanation from Jeremy:

First, understanding the problem the test reproduces:

add a job (num_jobs = 1) -> creates thread to run it.
job finishes, thread sticks around (num_idle = 1).
num_jobs is now zero (initial job finished).

a) Idle thread is now waiting on pool->condvar inside
pthreadpool_server() in pthread_cond_timedwait().

Now, add another job ->

pthreadpool_add_job()
-> pthreadpool_put_job()
This adds the job to the queue.
Oh, there is an idle thread so don't
create one, do:

pthread_cond_signal(&pool->condvar);

and return.

Now call fork *before* idle thread in (a) wakes from
the signaling of pool->condvar.

In the parent (child is irrelevent):

Go into: pthreadpool_prepare() ->
pthreadpool_prepare_pool()

Set the variable to tell idle threads to exit:

pool->prefork_cond = &prefork_cond;

then wake them up with:

pthread_cond_signal(&pool->condvar);

This does nothing as the idle thread
is already awoken.

b) Idle thread wakes up and does:

Reduce idle thread count (num_idle = 0)

pool->num_idle -= 1;

Check if we're in the middle of a fork.

if (pool->prefork_cond != NULL) {

Yes we are, tell pthreadpool_prepare()
we are exiting.

pthread_cond_signal(pool->prefork_cond);

And exit.

pthreadpool_server_exit(pool);
return NULL;
}

So we come back from the fork in the parent with num_jobs = 1,
a job on the queue but no idle threads - and the code that
creates a new thread on job submission was skipped because
an idle thread existed at point (a).

OK, assuming that the previous explaination is correct, the
fix is to create a new pthreadpool context mutex:

pool->fork_mutex

and in pthreadpool_server(), when an idle thread wakes up and
notices we're in the prepare fork state, it puts itself to
sleep by waiting on the new pool->fork_mutex.

And in pthreadpool_prepare_pool(), instead of waiting for
the idle threads to exit, hold the pool->fork_mutex and
signal each idle thread in turn, and wait for the pool->num_idle
to go to zero - which means they're all blocked waiting on
pool->fork_mutex.

When the parent continues, pthreadpool_parent()
unlocks the pool->fork_mutex and all the previously
'idle' threads wake up (and you mention the thundering
herd problem, which is as you say vanishingly small :-)
and pick up any remaining job.

Bug: https://bugzilla.samba.org/show_bug.cgi?id=13179
Signed-off-by: Volker Lendecke <vl@samba.org>
Reviewed-by: Jeremy Allison <jra@samba.org>
(cherry picked from commit f6858505aec9f1004aeaffa83f21e58868749d65)
lib/pthreadpool/pthreadpool.c