[Ecls-list] Locking strategy (best so far)
Juan Jose Garcia-Ripoll
juanjose.garciaripoll at googlemail.com
Thu Mar 29 15:57:09 UTC 2012
This seems to be a good compromise, using the underlying operating system
for waiting and signaling and using a fast atomic path for detecting the
lock-free case. First the simple mutex
cl_object
mp_get_lock_wait(cl_object lock)
{
if (ecl_atomic_queue_list(lock) != Cnil ||
mp_get_lock_nowait(lock) == Cnil) {
ecl_wait_on(get_lock_inner, lock);
}
@(return Ct)
}
Note that the first part of the conditional is what makes this a fair
implementation: if there are others waiting, we respect their precedence.
The nowait locking is very simple as well
static cl_object
get_lock_inner(cl_env_ptr env, cl_object lock)
{
cl_object output;
cl_object own_process = env->own_process;
ecl_disable_interrupts_env(env);
if (AO_compare_and_swap_full((AO_t*)&(lock->lock.owner),
(AO_t)Cnil, (AO_t)own_process)) {
lock->lock.counter = 1;
output = Ct;
print_lock("acquiring\t", lock, lock);
} else [...]
ecl_enable_interrupts_env(env);
return output;
}
Now comes the hard part, which is waiting for the lock (or condition
variable or semaphore) to change. Apart from the polling version which is
in CVS, I came up with this other version which relies on POSIX signals and
seems to work reasonably fast -- at least as fast as the POSIX mutexes on
the same platform.
void
ecl_wait_on(cl_object (*condition)(cl_env_ptr, cl_object), cl_object o)
{
const cl_env_ptr the_env = ecl_process_env();
volatile cl_object own_process = the_env->own_process;
volatile cl_object record;
volatile sigset_t original;
/* 0) We reserve a record for the queue. In order to a void
* using the garbage collector, we reuse records */
record = own_process->process.queue_record;
unlikely_if (record == Cnil) {
record = ecl_list1(own_process);
} else {
own_process->process.queue_record = Cnil;
}
/* 1) First we block all signals. */
{
sigset_t empty;
sigemptyset(&empty);
pthread_sigmask(SIG_SETMASK, &original, &empty);
}
/* 2) Now we add ourselves to the queue. In order to avoid a
* call to the GC, we try to reuse records. */
ecl_atomic_queue_nconc(the_env, o->lock.queue_list, record);
own_process->process.waiting_for = o;
CL_UNWIND_PROTECT_BEGIN(the_env) {
/* 3) At this point we may receive signals, but we
* might have missed a wakeup event if that happened
* between 0) and 2), which is why we start with the
* check*/
cl_object queue = ECL_CONS_CDR(o->lock.queue_list);
if (ECL_CONS_CAR(queue) != own_process ||
condition(the_env, o) == Cnil)
{
do {
/* This will wait until we get a signal that
* demands some code being executed. Note that
* this includes our communication signals and
* the signals used by the GC. Note also that
* as a consequence we might throw / return
* which is why need to protect it all with
* UNWIND-PROTECT. */
sigsuspend(&original);
} while (condition(the_env, o) == Cnil);
}
} CL_UNWIND_PROTECT_EXIT {
/* 4) At this point we wrap up. We remove ourselves
from the queue and restore signals, which were */
own_process->process.waiting_for = Cnil;
ecl_atomic_queue_delete(the_env, o->lock.queue_list, own_process);
own_process->process.queue_record = record;
ECL_RPLACD(record, Cnil);
pthread_sigmask(SIG_SETMASK, NULL, &original);
} CL_UNWIND_PROTECT_END;
}
I am now working on small optimizations of this code and further testing. I
believe the same code can be used for Windows, because there we use a type
of signals that are not delivered until the code enters some specific
functions. Thus we could get rid of pthread_sigmask() (which do not exist
in Windows) and just replace sigsuspend() with a simple SleepEx().
Juanjo
--
Instituto de Física Fundamental, CSIC
c/ Serrano, 113b, Madrid 28006 (Spain)
http://juanjose.garciaripoll.googlepages.com
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mailman.common-lisp.net/pipermail/ecl-devel/attachments/20120329/49601f1d/attachment.html>
More information about the ecl-devel
mailing list