What practically is the difference between Semaphore and CapacityLimiter?

The Trio docs are clear that a CapacityLimiter is for limiting the number of things that can acquire some shared resource. It has an internal token counter, and if a task tries to acquire more tokens than are available, the task has to wait.

However: that was always my mental model of a semaphore, but apparently a CapacityLimiter is not a semaphore, or at least not a “traditional” semaphore.

Meanwhile there is in fact a Semaphore, and the explanation of what it does seems identical to the explanation of what a CapacityLimiter does.

The docs for Semaphore tell you to use CapacityLimiter for the one “obvious” semaphore use case, and the CapacityLimiter docs hint that it is “specialized for one common use case, with additional error checking”, but do not explain what those specializations and additional error checks are!

I’m sure that the distinction is perfectly obvious to concurrency experts and systems programmers, but it’s not at all obvious to “regular Joe” programmers like myself.

So what really is the difference? I don’t mind if the explanation gets technical, I just want to know!

References:

They are indeed very similar. Differences include:

  • CapacityLimiter tracks which tasks are holding each of the tokens, so it can give an error if the same task tries to take two tokens at once, or a task tries to release a token without having acquired one first. It potentially tell you which tasks are holding tokens if you get a deadlock (though we don’t really have an API for that latter part yet). Semaphore lets any task acquire/release at any time, with the only restriction being that acquire will block if the counter is 0.

  • CapacityLimiter lets you adjust the total number of tokens on the fly. Adding more tokens is equivalent to calling release on a Semaphore. But removing tokens gives some semantics that can’t be done with a classic Semaphore: all tasks that are currently using the capacity get to keep going, but no new tasks are allowed until the tasks have dropped below the new limit.

  • CapacityLimiter always has a total_tokens. For Semaphore, max_value is optional (and not set by default).

  • CapacityLimiter's method naming leans into the “bag of tokens” metaphor for understandability. Semaphore is much more abstract, which is helpful if you’re using it for some exotic situation where the tokens metaphor doesn’t make sense, but is unhelpful if you just want a CapacityLimiter and aren’t familiar with semaphores.

I might be missing something, but I think that’s most of them.

Does that answer the question?

1 Like

It does, yes! It would be great if this info could make its way back into the docs, it’s clear and easy to understand.

Would you like to open a pull request to add this to the documentation?