Skip to content

Commit

Permalink
Update docs and add BoundedSemaphore
Browse files Browse the repository at this point in the history
  • Loading branch information
Serious-senpai committed Apr 15, 2024
1 parent 715cc28 commit f31a107
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ jobs:

- name: Build documentation
run: dart doc .

- name: Upload documentation
uses: actions/upload-artifact@v4
with:
name: docs
path: doc/api
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Miscellaneous
coverage/
**/example.txt
**/*.test
analysis_options.yaml
Expand Down
25 changes: 21 additions & 4 deletions lib/src/lock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,30 @@ abstract class _Lock {

/// Mutex lock to guarantee exclusive access to a shared state.
///
/// A lock object can be in one of the two states: "locked" or "unlocked".
/// A [Lock] object can be in one of two states: "locked" or "unlocked".
///
/// If the lock is "locked", all futures which call [acquire] will be put in a waiting queue
/// If the lock is "locked", all futures that call [acquire] will be put in a waiting FIFO queue
/// and will proceed in order for each [release] call.
///
/// If the lock is "unlocked", calling [acquire] will set the lock to "locked" state and
/// If the lock is "unlocked", calling [acquire] will set the lock to the "locked" state and
/// return immediately.
///
/// Example usage:
/// ```dart
/// final lock = Lock();
///
/// // Acquire the lock
/// await lock.acquire();
///
/// try {
/// // Perform exclusive operations on the shared state
/// // ...
/// } finally {
/// // Release the lock
/// lock.release();
/// }
/// ```
///
/// See also: [Python documentation](https://docs.python.org/3/library/asyncio-sync.html#asyncio.Lock)
class Lock extends _Lock {
/// Create a new [Lock] object.
Expand All @@ -85,7 +101,8 @@ class Lock extends _Lock {
}

/// An [UnfairLock] object is identical to a [Lock] excepts that it wakes up the
/// last future that called [acquire] instead of the first
/// last future that called [acquire] instead of the first (waiting futures are
/// put in a LIFO queue).
class UnfairLock extends _Lock {
/// Create a new [UnfairLock] object.
UnfairLock();
Expand Down
64 changes: 57 additions & 7 deletions lib/src/semaphore.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,26 @@ abstract class _Semaphore {
}
}

/// Semaphore object which allows a number of futures to acquire it.
/// A semaphore object that allows a limited number of futures to acquire it.
///
/// A semaphore object keeps track of an internal counter. The internal counter is decremented
/// each time a future completes [acquire] and incremented for each [release] call.
/// A semaphore is a synchronization primitive that maintains a counter indicating the number of
/// available resources or permits. In this implementation, the semaphore keeps track of an internal
/// counter. The counter is decremented each time a future acquires the semaphore using the [acquire]
/// method and incremented each time the semaphore is released using the [release] method.
///
/// When multiple futures are waiting for the semaphore, they will be put in a queue and only
/// the first one will proceed when the semaphore is available.
/// When multiple futures are waiting for the semaphore, they will be put in a FIFO queue and only the
/// first one will proceed when the semaphore becomes available.
///
/// The [Semaphore] class is inspired by the Python `asyncio.Semaphore` class.
///
/// Example usage:
/// ```dart
/// final semaphore = Semaphore(2); // Create a semaphore with a limit of 2 permits
///
/// await semaphore.acquire(); // Acquire a permit
/// // Perform some asynchronous operation
/// semaphore.release(); // Release the permit
/// ```
///
/// See also: [Python documentation](https://docs.python.org/3/library/asyncio-sync.html#asyncio.Semaphore)
class Semaphore extends _Semaphore {
Expand All @@ -81,8 +94,45 @@ class Semaphore extends _Semaphore {
_FutureWaiter _getNextWaiter() => _waiters.removeFirst();
}

/// A [UnfairSemaphore] object is identical to a [Semaphore] excepts that it wakes up the
/// last future that called [acquire] instead of the first
/// A semaphore object that enforces an upper bound on the internal counter.
///
/// A bounded semaphore is a synchronization primitive that limits the number of
/// concurrent accesses to a shared resource. It maintains a counter that represents
/// the number of available resources. When a future wants to access the resource,
/// it must acquire a permit from the semaphore. If no permits are available, the
/// thread will be blocked until a permit becomes available.
///
/// This implementation extends the [Semaphore] class and adds additional logic to
/// enforce a limit on the number of permits. If the value of the semaphore exceeds
/// the initial value, a [BoundedSemaphoreLimitException] is thrown when releasing
/// a permit.
class BoundedSemaphore extends Semaphore {
final int _initial;

/// Construct a new [BoundedSemaphore] object with the initial internal counter set to [value].
/// This provided [value] is also the upper bound of the internal counter.
BoundedSemaphore(int value)
: _initial = value,
super(value);

/// Release a permit from the semaphore.
///
/// This method releases a permit from the semaphore, allowing other threads to
/// acquire it. If the value of the semaphore is greater than the initial value,
/// a [BoundedSemaphoreLimitException] is thrown.
@override
void release() {
super.release();
if (_value > _initial) {
throw BoundedSemaphoreLimitException();
}
}
}

/// A [UnfairSemaphore] is a synchronization primitive that limits the number of concurrent
/// accesses to a shared resource. It is similar to a [Semaphore], but it wakes up the last
/// future that called [acquire] instead of the first (i.e. waiting futures are put in a
/// LIFO queue).
class UnfairSemaphore extends _Semaphore {
/// Create a new [UnfairSemaphore] object with the initial internal counter set to [value].
UnfairSemaphore(int value) : super(value);
Expand Down
3 changes: 3 additions & 0 deletions lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ class LockAcquireFailureException extends AsyncLocksException {}

/// Exception thrown to futures cancelled by [Semaphore.cancelAll] or [UnfairSemaphore.cancelAll]
class SemaphoreAcquireFailureException extends AsyncLocksException {}

/// Exception that may be thrown in [BoundedSemaphore.release]
class BoundedSemaphoreLimitException extends AsyncLocksException {}
11 changes: 9 additions & 2 deletions test/semaphore_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ const futures_count = 20;
const concurrency = 4;

void main() {
var semaphores = [Semaphore(concurrency), UnfairSemaphore(concurrency)];

var semaphores = [Semaphore(concurrency), BoundedSemaphore(concurrency), UnfairSemaphore(concurrency)];
for (var semaphore in semaphores) {
test(
"Testing control flow: $semaphore",
Expand Down Expand Up @@ -49,4 +48,12 @@ void main() {
},
);
}

test(
"BoundedSemaphore release limit",
() async {
var boundedSemaphore = BoundedSemaphore(concurrency);
expect(boundedSemaphore.release, throwsA(isA<BoundedSemaphoreLimitException>()));
},
);
}

0 comments on commit f31a107

Please sign in to comment.