Предисловие:Реализация блокировки в Node.js
Для того, чтобы решить мою проблему, вы должны иметь знания в следующих областях: безопасность потоков, Promise, асинхронной-ОЖИДАНИЕ.
Для людей, которые не знакомы с TypeScript, это обычный JavaScript (ES6) с аннотациями типов.
У меня есть функция с именем excludeItems
, которая принимает список элементов (каждый элемент является строкой), и называет API (что исключает элемент) для каждого элемента. Важно не вызывать API дважды для одного и того же элемента, даже в разных исполнениях функции, поэтому я сохраняю в локальном БД элементы, которые уже исключены.
async function excludeItems(items: string[]) {
var excludedItems = await db.getExcludedItems();
for (var i in items) {
var item = items[i];
var isAlreadyExcluded = excludedItems.find(excludedItem => excludedItem == item);
if (isAlreadyExcluded) {
continue;
}
await someApi.excludeItem(item);
await db.addExcludedItem(item);
}
}
Эта функция вызывается асинхронно его клиентом несколько раз мгновенно, то есть клиент вызывает функцию, скажем, 5 раз до первого исполнения завершено.
Конкретный сценарий:
excludeItems([]);
excludeItems(['A','B','C']);
excludeItems([]);
excludeItems(['A','C']);
excludeItems([]);
В этом случае, хотя Node.js является однопоточным, Критической проблема Секции существующая здесь, и я получаю неправильные результаты. Это моя «excludedItems» коллекция в моей локальной БД после выполнения этого сценария:
[{item: 'A'},
{item: 'B'},
{item: 'C'},
{item: 'A'},
{item: 'C'}]
Как вы можете видеть, последний «A» и «C» является избыточным (это означает, что API также вызывается дважды для этих предметов).
Это связано с операциями await
в коде. Каждый раз, когда выполняется условие ожидания, создается новое Promise под капотом, поэтому, хотя Node.js является однопоточным, выполняется следующая функция async, ожидающая выполнения, и таким образом этот критический раздел выполняется параллельно ,
Чтобы решить эту проблему, я реализовал механизм блокировки:
var excludedItemsLocker = false;
async function safeExcludeItems(items: string[]) {
while (excludedItemsLocker) {
await sleep(100);
}
try {
excludedItemsLocker = true;
var excludedItems: string[] = await db.getExcludedItems();
for (var i in items) {
var item = items[i];
var isAlreadyExcluded = excludedItems.find(excludedItem => excludedItem == item);
if (isAlreadyExcluded) {
continue;
}
await someApi.excludeItem(item);
await db.addExcludedItem(item);
}
}
finally {
excludedItemsLocker = false;
}
}
async function sleep(duration: number): Promise<Object> {
return new Promise(function (resolve) {
setTimeout(resolve, duration);
});
}
Однако эта реализация не работает по какой-то причине. Я все еще получаю более одного (предполагаемого) «потока» в критическом разделе, что означает, что он все равно выполняется параллельно, а локальная БД заполняется теми же неправильными результатами. BTW, метод sleep
работает так, как ожидалось, его целью является просто дать процессорное время следующему вызову функции, который ожидает исполнения.
Кто-нибудь видит, что сломалось в моей реализации?
BTW Я знаю, что могу достичь той же цели без реализации блокировки, например, позвонив в db.getExcludedItems
внутри цикла, но я хочу знать, почему нарушена реализация блокировки.
Прошу прощения. Я забыл сказать, что db.getExcludedItems() возвращает массив строк. Однако спасибо за помощь. – Alon
В любом случае я видел в отладочном времени, что два (якобы) «потока» одновременно входят в критический раздел. – Alon