Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Should our locks (and semaphores, queues, etc.) be fair? #54
while True: async with some_lock: ...
where they keep releasing the lock and then immediately reaquiring it. Currently, the two tasks will alternate round-robin. If we allow barging, then in the naive implementation the thread that just released the lock will deterministically succeed in re-acquiring it, so the lock will never change hands. Of course this isn't the most representative example, but it illustrates the general idea.
Nonetheless, the conventional wisdom is that fairness is actually bad (!!):
It's not clear to me whether the conventional arguments apply here, though. AFAICT the biggest reason why fairness is bad is that it prevents the very-common situation where locks are acquired and then released within a single scheduling quantum, without contention. In this case barging is great; in a uniprocessor system where a lock can always be acquired/release without pre-empting in between, then barging entirely eliminates contention. Wow! And these kinds of micro-critical-sections that just protect a few quick operations are very common in traditional preemptive code. But... for us, micro-critical-sections often don't need locks at all! So this case that may dominate the traditional analysis basically doesn't exist for us. Hmm. Also, our context switching has very different characteristics than theirs, in terms of relative costs of different operations. I don't feel like I have a very clear sense of how this plays out in practice, though -- pypy tracing might change things a lot, it might matter a lot whether the scheduler gets smart enough to start eliding
The answer here may also vary for different primitives. In particular, fairness for locks and fairness for queues seems rather different. I guess for queues fairness is useful if you're multiplexing a set of inputs with backpressure into
Noticed while experimenting with TLS support: an interesting use case for fair mutexes is when multiple tasks are sharing a single connection – code like
data = some_sansio_state_object.send(...) async with connection_lock: await connection.sendall(data)
is correct (sends the data in the correct order) iff
Of course, in this case we could just as well have written it as
async with connection_lock: data = some_sansio_state_object.send(...) await connection.sendall(data)
but if you have a protocol where you can unexpectedly find yourself with data to send (e.g. doing a receive on a protocol that has pings, like websockets or http/2, or TLS 1.2 and earlier with their renegotiation stuff), and don't want to hold the send mutex unnecessarily (e.g. b/c you're trying to handle the receive end of the connection!), then this seems like a plausible (albeit rather tricky!) approach:
some_sansio_state_object.do_something() if some_sansio_state_object.has_data(): # pull it out of the shared state before yielding data = some_sansio_state_object.get_data() async with connection_lock: await connection.sendall(data)
Note that the above requires strict FIFO fairness, which is incompatible with doing clever things with letting higher priority tasks skip ahead, as suggested in this comment (but some sort of priority inheritance thing could work I guess).
Though I guess for this particular use case and WFQ-style priorities, there's an argument for lack-of-priority inheritance, i.e. just making the slow task block the others that are sharing the same connection, as a kind of connection-wise group scheduling.
The SSL thing makes me nervous: if people start relying on
(And in particular, if we do implement WFQ then we'd probably want the implementations of