A crate for generic semaphore performing asynchronous permit acquisition.
A semaphore maintains a set of permits. Permits are used to synchronize access to a shared resource. A semaphore differs from a mutex in that it can allow more than one concurrent caller to access the shared resource at a time.
When acquire is called and the semaphore has remaining permits, the
function immediately returns a permit. However, if no remaining permits are
available, acquire (asynchronously) waits until an outstanding permit is
dropped. At this point, the freed permit is assigned to the caller.
This Semaphore is fair, and supports both FIFO and LIFO modes.
- In FIFO mode, this fairness means that permits are given out in the order they were requested.
- In LIFO mode, this fairness means that permits are given out in the reverse order they were requested.
This fairness is also applied when acquire with high 'parameters' gets
involved, so if a call to acquire at the end of the queue requests
more permits than currently available, this can prevent another call to acquire
from completing, even if the semaphore has enough permits to complete it.
This semaphore is generic, which means you can customise the state. Examples:
- Using two counters, you can immediately remove permits, while there are some still in flight. This might be useful if you want to remove concurrency if failures are detected.
- There might be multiple quantities you want to limit over. Stacking multiple semaphores can be awkward and risk deadlocks. Instead, making the state contain all those quantities combined can simplify the queueing.
Performance
My performance testing has shown that flag_bearer's [Semaphore] is competitive
with tokio::sync::Semaphore.
The only exception is in the case when try_acquire is called with no permits available,
tokio only needs a single atomic read for this case, whereas flag_bearer still needs to acquire a lock.
This is measurable, but mostly in the contended usecase.
Examples
A Semaphore like tokio::sync::Semaphore
;
# block_on
A more complex usecase with the ability to update limits on demand
tokio's Semaphore allows adding permits on demand, but it doesn't support removing permits.
You have to forget the permits that are already acquired, and if you don't have those on hand,
you will have to spawn a task to acquire them.
With the following construction, we can define a state that allows removing permits at will.
# block_on
A LIFO semaphore which tracks multiple values at once
Last-in, first-out is an unfair strategy of queueing, where the latest queued task will be the first one to get the permit. This might seem like a bad strategy, but if high latency is considered a failure in your applications, this can reduce the failure rate. In a FIFO setting, you might have a P50 = 50ms, P99 = 100ms. The same in a LIFO setting could be P50 = 10ms, P99 = 500ms. If 50ms half the time is too slow for your application, then switching to LIFO could help. https://encore.dev/blog/queueing#lifo
Additionally to LIFO, Our [SemaphoreState] allows us to track multiple fields at once.
Let's imagine we want to limit in-flight requests, as well as in-flight request body allocations.
We can put the two counters in a single state object.
# block_on
A connection pool
A connection pool can work quite like a semaphore sometimes. There's a limited number of connections
and you don't want too many at one time. Our [SemaphoreState::Permit]s don't need to be Plain-Old-Data,
so we can use them to hold connection objects too.
This example still allows creating new connections on demand, if they are needed in high load cases, as well as re-creating connections if they fail.
# async
async
# block_on