Комментарии 7
Ходят слухи, что бинарный семафор больше подходит для синхронизации между потоками, в то время как мьютекс как раз отлично подходит для организации критической секции, т. е. take ownership. Так ли это?
Все так, реализуя и используя семафор, мы ожидаем, что другой поток может перехватить управление и войти в критическую секцию раньше первого потока. Единственное, в чем мы точно уверены в этом случае - одновременно в критической секции будет только один поток. Мьютекс же полностью заблокирует доступ к ресурсу, и снять его можно будет только из текущего потока (владельца ресурса).
Должно быть верно, здесь логика следущая:
// Ждем пока освободится место для нашего потока, то есть счетчик станет больше 0
Atomics.wait(this.counter, 0, 0);
// Получаем значение счетчика
const n = Atomics.load(this.counter, 0);
// Еще раз проверям на то, что n > 0, так как другой поток мог вклиниться между wait и load
if (n > 0) {
// Пытаемся атомарно уменьшить счетчик, получив его предыдущее значение
const prev = Atomics.compareExchange(this.counter, 0, n, n - 1);
// Если prev === n, значит мы успешно уменьшили счетчик, все ок, можно выходить из цикла
if (prev === n) return;
// Eсли prev !== n, значит мы не изменили счетчик, надо пробовать снова - идем в начало цикла
}
Спасибо больше за сатью. Написано интересно!
Задам только тупой вопрос: подход применим только и именно для потоков? Я имею в виду, если я создам n промисов, которые будут читать/писать каку-то глоб. переменную, ситуация гонок ведь тоже будет? И Аtomics с семафором это дело порешают?
Спасибо за внимание!
В таком случае семафор не поможет, так как блокирует поток, а он у нас (упс) один. С другой стороны он и не нужен, так как последовательные манипуляции с глобальной переменной выполнятся в одном потоке атомарно, не важно сколько промисов с ней взаимодействуют.
Покажу на примере: есть два промиса, один увеличивает счетчик, если он в нуле, другой уменьшает, если он в единице - в итоге ожидаем увидеть 0.
let sharedCounter = 0;
const checkAndChange = () => {
const isZero = sharedCounter === 0;
// сюда не проберется никакой другой поток
if (isZero) {
sharedCounter += 1;
} else {
sharedCounter -= 1;
}
}
const incrementIfZero = new Promise((resolve) => {
resolve(checkAndChange());
});
const decrementIfNonZero = new Promise((resolve) => {
resolve(checkAndChange());
});
Promise.all([incrementIfZero, decrementIfNonZero]).then(() => {
console.log(`Result: ${sharedCounter}`);
});
Как написал выше, код в функции checkAndChange выполнится атомарно, никто не может поменять переменную sharedCounter в процессе его выполнения, если программа выполняется в одном потоке.
Другая ситуация, если проверка и манипуляции с переменной происходят в теле промиса и затем в then-колбэке.
let sharedCounter = 0;
const incrementIfZero = new Promise((resolve) => {
resolve(sharedCounter === 0 ? 1 : 0);
}).then((increment) => {
sharedCounter += increment;
});
const decrementIfNonZero = new Promise((resolve) => {
resolve(sharedCounter === 1 ? 1 : 0);
}).then((decrement) => {
sharedCounter -= decrement;
});
Promise.all([incrementIfZero, decrementIfNonZero]).then(() => {
console.log(`Result: ${sharedCounter}`);
});
В этом случае как раз не увидим в результате нуля, потому что сначала выполнятся код в теле промиса, а уже затем then-обработчики. Но здесь в первую очередь вопросики к такой реализации, а не повод применять семафор.
Примитивы синхронизации в JavaScript: cемафоры и хоккей