Limit or throttle the concurrent execution of asynchronous code in separate iterations of the event loop.
Using npm:
npm install @chriscdn/promise-semaphore
Using yarn:
yarn add @chriscdn/promise-semaphore
Version 3 introduces two main changes:
- A new
GroupSemaphore
class has been added. It allows multiple tasks within the same group (identified by a key) to run concurrently while ensuring that only one group's tasks are active at a time. See below for documentation. - The default export has been replaced with a named export.
Change:
import Semaphore from "@chriscdn/promise-semaphore";
to:
import { Semaphore } from "@chriscdn/promise-semaphore";
import { Semaphore } from "@chriscdn/promise-semaphore";
const semaphore = new Semaphore([maxConcurrent]);
The maxConcurrent
parameter is optional and defaults to 1
(making it an
exclusive lock or binary semaphore). An integer greater than 1
can be used
to allow multiple concurrent executions from separate iterations of the event
loop.
semaphore.acquire([key]);
This returns a Promise
that resolves once a lock is acquired. The key
parameter is optional and allows the same Semaphore
instance to manage locks
in different contexts. Additional details are provided in the second example.
semaphore.release([key]);
The release
method should be called within a finally
block (whether using
promises or a try/catch
block) to ensure the lock is released.
semaphore.canAcquire([key]);
This synchronous method returns true
if a lock can be immediately acquired,
and false
otherwise.
semaphore.count([key]);
This function is synchronous, and returns the current number of locks.
const results = await semaphore.request(fn [, key]);
This function reduces boilerplate when using acquire
and release
. It returns
a promise that resolves when fn
completes. It is functionally equivalent to:
try {
await semaphore.acquire([key]);
return await fn();
} finally {
semaphore.release([key]);
}
const results = await semaphore.requestIfAvailable(fn [, key]);
This is functionally equivalent to:
return semaphore.canAcquire([key]) ? await semaphore.request(fn, [key]) : null;
This is useful in scenarios where only one instance of a function block should run while discarding additional attempts. For example, handling repeated button clicks.
import { Semaphore } from "@chriscdn/promise-semaphore";
const semaphore = new Semaphore();
// Using promises
semaphore
.acquire()
.then(() => {
// This block executes once a lock is acquired.
// If already locked, it waits and executes after all preceding locks are released.
//
// Critical operations
})
.finally(() => {
// The lock is released, allowing the next queued block to proceed.
semaphore.release();
});
// Using async/await
try {
await semaphore.acquire();
// Critical operations
} finally {
semaphore.release();
}
// Using the request function
await semaphore.request(() => {
// Critical operations
});
Consider an asynchronous function that downloads a file and saves it to disk:
const downloadAndSave = async (url) => {
const filePath = urlToFilePath(url);
if (await pathExists(filePath)) {
// The file is already on disk, so no action is required.
return filePath;
}
await downloadAndSaveToFilepath(url, filePath);
return filePath;
};
This approach works as expected until downloadAndSave()
is called multiple
times in quick succession with the same url
. Without control, it could
initiate simultaneous downloads that attempt to write to the same file at the
same time.
This issue can be resolved by using a Semaphore
with the key
parameter:
import { Semaphore } from "@chriscdn/promise-semaphore";
const semaphore = new Semaphore();
const downloadAndSave = async (url) => {
try {
await semaphore.acquire(url);
// This block continues once a lock on url is acquired. This
// permits multiple simultaneous downloads for different urls.
const filePath = urlToFilePath(url);
if (await pathExists(filePath)) {
// the file is on disk, so no action is required
} else {
await downloadAndSaveToFilepath(url, filePath);
}
return filePath;
} finally {
semaphore.release(url);
}
};
The same outcome can be achieved by using the request
function:
const downloadAndSave = (url) => {
return semaphore.request(async () => {
const filePath = urlToFilePath(url);
if (await pathExists(filePath)) {
// The file is already on disk, so no action is required.
} else {
await downloadAndSaveToFilepath(url, filePath);
}
return filePath;
}, url);
};
The GroupSemaphore
class manages a semaphore for different groups of tasks.
A group is identified by a key, and the semaphore ensures that only one group
can run its tasks at a time. The tasks within a group can run concurrently.
The GroupSemaphore
class exposes acquire
and release
methods, which have
the same interface as Semaphore
. The only difference is that the key
parameter is required.
import { GroupSemaphore } from "@chriscdn/promise-semaphore";
const groupSemaphore = new GroupSemaphore();
const RunA = async () => {
try {
await groupSemaphore.acquire("GroupA");
// Perform asynchronous operations for group A
} finally {
groupSemaphore.release("GroupA");
}
};
const RunB = async () => {
try {
await groupSemaphore.acquire("GroupB");
// Perform asynchronous operations for group B
} finally {
groupSemaphore.release("GroupB");
}
};
This setup allows RunA
to be called multiple times, and will run concurrently.
However, calling RunB
will wait until all GroupA
tasks are completed before
acquiring the lock for GroupB
. As soon as GroupB
acquires the lock, any
subsequent calls to RunA
will wait until GroupB
releases the lock before it
executes.