Здравствуйте, Videoman, Вы писали:
V>у Linux все ожидания, всех объектов в ядре так устроены — происходит пробуждение по сигналу со специальным кодом. Т.к. в случаях не связанных с CV у нас состояние объекта присутствует в ядре, мы можем проверить код возврата, перепроверить само состояние и вернуться обратно в ожидание, если оно было ложным. В случае в CV у нас состояние в user mod-е и мы вынуждены вернуться и проверять его самостоятельно.
у Linux cond_wait по любому сигналу, поступающему процессу, отмораживается имхо. Тут должно совпасть следующее:
1.кто-то посигналил процессу
2.обработчик сигнала вызывается в контексте этой нитки
Когда нитка уходит из ожидания в userspace, ей могут посигналить (cond_signal) и сигнал будет пропущен, если окончательно не вывалиться из библиотечного кода и руками не проверить состояние.
V>Из всего вышесказанного делаем вывод — futex оказался слишком низкоуровневым объектом что бы его выносить в стандартную библиотеку C++. Ведь никто, слава богу, не догадался сделать spurious_read, spurious_join или spurious_lock. И все-равно остается куча вопросов:
futex — один из вариантов реализации мьютекса, разве нет?
V>1. Что c Windows ? V>2. Если на Windows обошлись без ложных пробуждений, то почему особенности реализации POSIX на Linux "втащили" в стандартную библиотеку ?
А так, надо в реализацию pthreads для винды глянуть, на чем там мьютексы сделаны. Думаю, на CRITRICAL_SECTION, скорее всего.
V>3. Почему, например, не сделали флаг для futex-а — NO_SPURIOUS_WAKEUP для использования в высокоуровневом API ?
Потому что код обработчика сигнала надо в userspace выполнять всё равно.
V>4. Что с вариантами wait_until и wait_for, ведь если wait_until еще как-то можно в цикле крутить, то wait_for становится весьма нетривиальным ?
Да черт его знает, я с позиксом хоть как-то ковырялся, а на стандартных c++ примитивах кондвары не использовал
Здравствуйте, rsdn_179b, Вы писали:
_>Зачем крутиться в цикле и проверять какие-то условия
Futex, как некоторые коллеги пишут тут, ни при чём. Правило про возможность ложных пробуждений возникло задолго до Linux и futex. Оно было в BSD в select(). Оно было в изначальных версиях многониточности на Unix (которые появились на SysV, как Solaris), и перенесено 1:1 в Java. Оно было в Bell Unix в ядерном sleep/wakeup (там вместо мьютексов были уровни приоритета ядра, но суть та же). Более того, этот механизм в таком виде существовал ещё до Unix, и там тоже было это правило.
Настоящая и исконная причина: в multi-producer-multi-customer построении невозможно гарантировать, что когда потребителю A "свистнули", что появился ресурс, не придёт потребитель B, который захватит мьютекс раньше и потребит ресурс. Когда же B отпустит мьютекс и A получит такую возможность, ему уже может не достаться ресурса, он увидит другое состояние, чем предполагалось на момент отдачи нотификации.
Частным "ракурсом" этой проблемы является ABA problem, но ситуация к ней сводится только в простых случаях (перед спячкой увидели какое-то состояние, вышли из спячки — а там вернулись к нему же; в сложных случаях состояние будет другое, хотя тоже неинтересное).
Если кто-то спросит, почему не делать такую синхронизацию, при которой нет такой проблемы... можно сделать. Например, при notifyOne() указывать, кого именно notify. Но тогда другая проблема — а что, если этот решил вообще выйти из игры? Ему надо переслать на кого-то другого? Увы, за >50 лет существования темы такой синхронизации — этот подход самый простой и надёжный.
А уже последствием описанной проблемы является возможность разрешения ложных пробуждений за счёт коллизий реализации (про futex — скорее сказка, а вот select collision в старых BSD была известным явлением).
Здравствуйте, uzhas, Вы писали:
U>2) из-за того, что в void update не лочит мьютекс мы имеем race, из-за которого cv.wait(l) зависнет навечно, хотя ready_flag будет равен true. очень важно понимать, насколько мощен wait: U>этот метод одновременно уходит в ожидание и разлочивает мьютекс (транзакционно). это гарантирует, что при правильном использовании (а не как в этом примере) cv, метод notify не уйдет в пустоту. гарантирует, что условие в while не может измениться, пока мы не провалимся в wait. поэтому менять данные, которые могут изменять condition, надо строго под мьютексом.
Нет. И такое изменение допустимо (см. ниже), и notify за пределами владения мьютексом разрешён во многих подобных реализациях (включая pthreads), и часто даже рекомендуется — за счёт того, что он не вызывает дополнительных переключений (когда задача, получившая нотификацию, пробуждается и тут же засыпает снова на ожидании мьютекса).
Пример Кодта плох не тем, что он мог бы не работать — он работает. Пример плох таки тем, что смешиваются два разных подхода — защита произвольных данных (в общем случае не пригодных к атомарному изменению) мьютексом — как полагается для этого механизма, и игр с interlocked exchange, которые тут требуют дополнительного объяснения — в частности, почему они вообще работают, а тут уже надо углубляться, что InterlockedExchange() содержит в себе полный барьер памяти, который в случае мьютекса реализуется собственно суммой входа в мьютекс и выхода из него.
По-нормальному надо было бы нарисовать что-то вроде кольцевого буфера с head, tail и length — это банальный избитый пример, но он показал бы основы проблемы. А уже после этого пытаться подключать внелоковые атомики.
The God is real, unless declared integer.
Re[6]: C++11: Синхронизация - Условные переменные и ложные проб
Здравствуйте, Videoman, Вы писали:
A>>Чем тебя не устраивает объяснение, что cond_wait вылетает при получении процессом сигнала ?
V>Не совсем устраивает, т.к. не хватает информации в общем. Я так понял: V>у Linux все ожидания, всех объектов в ядре так устроены — происходит пробуждение по сигналу со специальным кодом. Т.к. в случаях не связанных с CV у нас состояние объекта присутствует в ядре, мы можем проверить код возврата, перепроверить само состояние и вернуться обратно в ожидание, если оно было ложным. В случае в CV у нас состояние в user mod-е и мы вынуждены вернуться и проверять его самостоятельно.
Эта логика не подтверждается. У сигнала в схеме Unix есть настройка перезапуска (по каждому сигналу отдельно, см. SA_RESTART в sigaction(). И есть список вызовов, которые не перезапускаются даже при SA_RESTART — тут — и в него входят все вызовы ожидания события (select, poll, *sleep и так далее).
Тем не менее, согласно последней ссылке, futex wait перезапускается. То есть сигналы (по крайней мере с 2.6.22, то есть, по состоянию на последние ~10 лет) ни при чём.
V> Из всего вышесказанного делаем вывод — futex оказался слишком низкоуровневым объектом что бы его выносить в стандартную библиотеку C++. Ведь никто, слава богу, не догадался сделать spurious_read, spurious_join или spurious_lock.
Если имеется в виду, что read может получить отказ по сигналу — то догадался и делает. Это не относится только к uninterruptible waits, которые в основном относятся к FS, ну и исторически к non-direct raw disk I/O.
V>3. Почему, например, не сделали флаг для futex-а — NO_SPURIOUS_WAKEUP для использования в высокоуровневом API ?
Я написал в комментарии к исходному сообщению, почему это не имеет смысла.
V>4. Что с вариантами wait_until и wait_for, ведь если wait_until еще как-то можно в цикле крутить, то wait_for становится весьма нетривиальным ?
Сейчас wait_for тривиально везде (или в Windows не так?) можно переделать в wait_until по монотонному времени.
The God is real, unless declared integer.
Re[2]: C++11: Синхронизация - Условные переменные и ложные пробужде
Здравствуйте, se_sss, Вы писали:
_>Возник вопрос не по совсем теме, но всё же связанный с ней. _>Сейчас нашёл разъяснение для Linux, состоящее в том, что если идёт блокирующией вызов и приходит сигнал, то вызов прерывается: _>http://blog.vladimirprus.com/2005/07/spurious-wakeups.html
2005 год? Повторюсь: futex wait сейчас может перезапускаться, так что то обоснование уже неадекватно.
С другой стороны, 1) не линуксом единым, 2) конкретный сигнал может не иметь SA_RESTART, 3) причина может быть не только в сигналах (хотя, если потребитель один, я с ходу не знаю другого источника диверсии).
_>В связи с этой страничкой возник вопрос. А что если у нас несколько потоков? В каком из них ошибка произошла? _>errno ведь глобальная переменная?
Про это уже ответили — errno она типичная thread local.
The God is real, unless declared integer.
Re[3]: C++11: Синхронизация - Условные переменные и ложные п
Вот в ядре linux есть функция match_futex, которая используется во всех операциях обхода бакета как раз для того, чтобы отфильтровать фьютексы попавшие в него из-за коллизий
Да, есть такая функция, которая используется во всех операциях обхода бакета. Можно ссылку на код, который используется именно для отфильтровывания "коллизийных" фьютексов, а не для поиска нужного бакета по ключу ?
Здравствуйте, kotalex, Вы писали:
K>Да, есть такая функция, которая используется во всех операциях обхода бакета. Можно ссылку на код, который используется именно для отфильтровывания "коллизийных" фьютексов, а не для поиска нужного бакета по ключу ?
В ядре linux работа с futex не размазана по куче исходников, а вполне компактно содержится почти целиком в единственном файле. На который уже привёл ссылку в предыдущем сообщении. Пропустил? Ну вот ещё раз: match_futex
А вот чуть ниже в этом же файле пример её целевого использования: https://github.com/torvalds/linux/blob/9e98c678c2d6ae3a17cb2de55d17f69dddaa231b/kernel/futex.c#L1602-L1616
Там неоднократно встречается шаблон из последовательности вызовов:
// получение бакета по фьютексу:
hb = hash_futex(&key);
// обход всех записей в бакете:
plist_for_each_entry_safe(this, next, &hb->chain, list)
// проверка на коллизииif (match_futex (&this->key, &key))
Везде результаты plist_for_each_entry* отфильтровываются через match_futex.
Re[2]: C++11: Синхронизация - Условные переменные и ложные проб
Здравствуйте, netch80, Вы писали:
N>Настоящая и исконная причина: в multi-producer-multi-customer построении невозможно гарантировать, что когда потребителю A "свистнули", что появился ресурс, не придёт потребитель B, который захватит мьютекс раньше и потребит ресурс. Когда же B отпустит мьютекс и A получит такую возможность, ему уже может не достаться ресурса, он увидит другое состояние, чем предполагалось на момент отдачи нотификации.
О каком ресурсе (состоянии) идёт речь? У condition_variable нет никакого состояния, кроме одного мьютекса на всех.
N>Если кто-то спросит, почему не делать такую синхронизацию, при которой нет такой проблемы... можно сделать. Например, при notifyOne() указывать, кого именно notify.
Т.е. указывать нить?
N>Но тогда другая проблема — а что, если этот решил вообще выйти из игры?
Это как? У нити нет способа "выйти из игры", она же заблокирована.
И каждый день — без права на ошибку...
Re[3]: C++11: Синхронизация - Условные переменные и ложные п
Здравствуйте, B0FEE664, Вы писали:
N>>Настоящая и исконная причина: в multi-producer-multi-customer построении невозможно гарантировать, что когда потребителю A "свистнули", что появился ресурс, не придёт потребитель B, который захватит мьютекс раньше и потребит ресурс. Когда же B отпустит мьютекс и A получит такую возможность, ему уже может не достаться ресурса, он увидит другое состояние, чем предполагалось на момент отдачи нотификации.
BFE>О каком ресурсе (состоянии) идёт речь?
О том, который защищается данным конкретным мьютексом.
N>>Если кто-то спросит, почему не делать такую синхронизацию, при которой нет такой проблемы... можно сделать. Например, при notifyOne() указывать, кого именно notify. BFE>Т.е. указывать нить?
Да.
N>>Но тогда другая проблема — а что, если этот решил вообще выйти из игры? BFE>Это как? У нити нет способа "выйти из игры", она же заблокирована.
С чего вдруг? В данный момент она может, например, быть занята обработкой предыдущего задания
Здравствуйте, netch80, Вы писали:
N>>>Настоящая и исконная причина: в multi-producer-multi-customer построении невозможно гарантировать, что когда потребителю A "свистнули", что появился ресурс, не придёт потребитель B, который захватит мьютекс раньше и потребит ресурс. Когда же B отпустит мьютекс и A получит такую возможность, ему уже может не достаться ресурса, он увидит другое состояние, чем предполагалось на момент отдачи нотификации. BFE>>О каком ресурсе (состоянии) идёт речь? N>О том, который защищается данной конкретной парой mutex + CV.
Речь идёт о notifyOne()?
N>>>Если кто-то спросит, почему не делать такую синхронизацию, при которой нет такой проблемы... можно сделать. Например, при notifyOne() указывать, кого именно notify. N>>>Но тогда другая проблема — а что, если этот решил вообще выйти из игры? BFE>>Это как? У нити нет способа "выйти из игры", она же заблокирована. N>С чего вдруг? В данный момент она может, например, быть занята обработкой предыдущего задания.
Нить, которая занята обработкой предыдущего задания, пропустит notifyOne() в соответствии с текущей спецификацией.
И каждый день — без права на ошибку...
Re[5]: C++11: Синхронизация - Условные переменные и ложные проб
Здравствуйте, B0FEE664, Вы писали:
BFE>Здравствуйте, netch80, Вы писали:
N>>>>Настоящая и исконная причина: в multi-producer-multi-customer построении невозможно гарантировать, что когда потребителю A "свистнули", что появился ресурс, не придёт потребитель B, который захватит мьютекс раньше и потребит ресурс. Когда же B отпустит мьютекс и A получит такую возможность, ему уже может не достаться ресурса, он увидит другое состояние, чем предполагалось на момент отдачи нотификации. BFE>>>О каком ресурсе (состоянии) идёт речь? N>>О том, который защищается данной конкретной парой mutex + CV. BFE>Речь идёт о notifyOne()?
Нет, здесь — в общем случае.
N>>>>Если кто-то спросит, почему не делать такую синхронизацию, при которой нет такой проблемы... можно сделать. Например, при notifyOne() указывать, кого именно notify. N>>>>Но тогда другая проблема — а что, если этот решил вообще выйти из игры? BFE>>>Это как? У нити нет способа "выйти из игры", она же заблокирована. N>>С чего вдруг? В данный момент она может, например, быть занята обработкой предыдущего задания. BFE>Нить, которая занята обработкой предыдущего задания, пропустит notifyOne() в соответствии с текущей спецификацией.
И поэтому этот гипотетический метод нужно ещё больше дорабатывать.
The God is real, unless declared integer.
Re[6]: C++11: Синхронизация - Условные переменные и ложные проб
Здравствуйте, netch80, Вы писали:
BFE>>>>О каком ресурсе (состоянии) идёт речь? N>>>О том, который защищается данной конкретной парой mutex + CV. BFE>>Речь идёт о notifyOne()? N>Нет, здесь — в общем случае.
А причём тут общий случай, если речь идёт о condition_variable ? condition_variable вообще не обязана быть связанной с какими-то разделяемыми данными.
N>>>>>Если кто-то спросит, почему не делать такую синхронизацию, при которой нет такой проблемы... можно сделать. Например, при notifyOne() указывать, кого именно notify. N>>>>>Но тогда другая проблема — а что, если этот решил вообще выйти из игры? BFE>>>>Это как? У нити нет способа "выйти из игры", она же заблокирована. N>>>С чего вдруг? В данный момент она может, например, быть занята обработкой предыдущего задания. BFE>>Нить, которая занята обработкой предыдущего задания, пропустит notifyOne() в соответствии с текущей спецификацией. N>И поэтому этот гипотетический метод нужно ещё больше дорабатывать.
В каком смысле дорабатывать? Должна проснутся одна из тех ниток, которые заблокированы, а те, которые работают должны продолжать работать. Это в соответствии с текущей спецификацией.
И каждый день — без права на ошибку...
Re[7]: C++11: Синхронизация - Условные переменные и ложные п
Здравствуйте, B0FEE664, Вы писали:
BFE>>>>>О каком ресурсе (состоянии) идёт речь? N>>>>О том, который защищается данной конкретной парой mutex + CV. BFE>>>Речь идёт о notifyOne()? N>>Нет, здесь — в общем случае. BFE>А причём тут общий случай, если речь идёт о condition_variable ?
Общий случай для использования condition variable. Я полагал это очевидным по контексту.
BFE> condition_variable вообще не обязана быть связанной с какими-то разделяемыми данными.
Тогда она вообще нахрен никому не нужна. CV имеет смысл только тогда, когда логически привязана к каким-то реальным разделяемым данным, доступ к которым сериализуется мьютексом, с которым связано ожидание над ней.
N>>И поэтому этот гипотетический метод нужно ещё больше дорабатывать. BFE>В каком смысле дорабатывать? Должна проснутся одна из тех ниток, которые заблокированы, а те, которые работают должны продолжать работать. Это в соответствии с текущей спецификацией.
В первом абзаце исходного сообщения я сказал про текущий режим, а во втором слегка затронул возможные альтернативы (и снова, полагал это отличие очевидным). Но больше я эти альтернативы обсуждать не хочу, потому что обсуждение мгновенно потеряло целостность.
// проверка на коллизии
if (match_futex (&this->key, &key))
Вы не внимательно читаете заданный вопрос — это не проверка на коллизии, а вполне конкретный код для нахождения заданного бакета, чтобы, например, пробудить его !
Здравствуйте, kotalex, Вы писали:
K>Здравствуйте, watchmaker, Вы писали: K>
K>// проверка на коллизии
K>if (match_futex (&this->key, &key))
K>Вы не внимательно читаете заданный вопрос — это не проверка на коллизии, а вполне конкретный код для нахождения заданного бакета, чтобы, например, пробудить его !
А вы не читаете код и не разбираетесь в терминологии.
"bucket" в хэш-таблицах — это место для нахождения всех элементов, у которых результат применения хэш-функции показывает индекс этого бакета (для closed addressing, как в данном случае).
Не одного, а всех. В hash_futex() последней строкой:
В каждом бакете находится голова списка структур ожидания.
А уже найдя конкретный бакет, начинается итерирование того, что в него попало — и вот тут уже через match_futex() проверяется конкретный элемент, описывающий ожидающий futex.
The God is real, unless declared integer.
Re[7]: C++11: Синхронизация - Условные переменные и ложные п
А вы не читаете код и не разбираетесь в терминологии.
"bucket" в хэш-таблицах — это место для нахождения всех элементов, у которых результат применения хэш-функции показывает индекс этого бакета (для closed addressing, как в данном случае).
Не одного, а всех. В hash_futex() последней строкой: return &futex_queues[hash & (futex_hashsize — 1)];
У Вас талант отвечать на вопрос, который не задавался
Здравствуйте, netch80, Вы писали:
N>В первом абзаце исходного сообщения я сказал про текущий режим, а во втором слегка затронул возможные альтернативы (и снова, полагал это отличие очевидным). Но больше я эти альтернативы обсуждать не хочу, потому что обсуждение мгновенно потеряло целостность.
Мне с самого начала изучения condition_variable ничего очевидным с использованием этого конструкта не кажется. Начиная с совершенно непонятного и вводящего в заблуждение названия и кончая ничем не обоснованным непредсказуемым поведением.
Во-первых CV — это не переменная, так как у неё нет состояния.
Во-вторых CV — не может называться условной, так как она может срабатывать без всяких видимых условий.
В-третьих CV — ни имеет никакого смысла сама по себе т.к. "CV имеет смысл только тогда, когда логически привязана к каким-то реальным разделяемым данным."
Поэтому я не вижу никакого прямого способа использования этого примитива, кроме как превратив CV в что-то более логичное, например в объект Event (Событие).
Я понимаю трудности изложенные здесь: N>Настоящая и исконная причина: в multi-producer-multi-customer построении невозможно гарантировать, что когда потребителю A "свистнули", что появился ресурс, не придёт потребитель B, который захватит мьютекс раньше и потребит ресурс. Когда же B отпустит мьютекс и A получит такую возможность, ему уже может не достаться ресурса, он увидит другое состояние, чем предполагалось на момент отдачи нотификации.
но я так и не понял, как эти трудности ведут к ложным пробуждениям CV.
И каждый день — без права на ошибку...
Re[9]: C++11: Синхронизация - Условные переменные и ложные п
Здравствуйте, B0FEE664, Вы писали:
BFE>Мне с самого начала изучения condition_variable ничего очевидным с использованием этого конструкта не кажется. Начиная с совершенно непонятного и вводящего в заблуждение названия и кончая ничем не обоснованным непредсказуемым поведением.
Понятно.
Ну а я считаю, что название — это единственное, что в ней нелогично. Исторически даже понятно, но всё равно нелогично, потому что чтобы объяснить название, надо подымать историю, и в достаточно глубинных деталях. Поэтому лучше говорить "тут так принято, просто запомните".
Если же обсуждать альтернативные названия, я бы начал с обсуждения чего-то вроде ghost pipe
BFE>Поэтому я не вижу никакого прямого способа использования этого примитива, кроме как превратив CV в что-то более логичное, например в объект Event (Событие).
Именно что напрямую и используется. Сделать Event на её основе, конечно, можно, но не подходит в заметной части случаев, и излишне в большинстве остальных.
BFE>Я понимаю трудности изложенные здесь: N>>Настоящая и исконная причина: в multi-producer-multi-customer построении невозможно гарантировать, что когда потребителю A "свистнули", что появился ресурс, не придёт потребитель B, который захватит мьютекс раньше и потребит ресурс. Когда же B отпустит мьютекс и A получит такую возможность, ему уже может не достаться ресурса, он увидит другое состояние, чем предполагалось на момент отдачи нотификации.
BFE>но я так и не понял, как эти трудности ведут к ложным пробуждениям CV.
notifyOne() дёрнул согласно списку ожидающих задачу TA (была она при этом первой в очереди, последней или случайно выбранной — не важно). Когда шедулер дойдёт до TA, первым делом сработает залочивание мьютекса.
Но если почему-то оказалось, что задача TB раньше захватила этот мьютекс, то она может изменить состояние так, что активация задачи TA окажется бессмысленной.
Вот пример. Типовая реализация пула нитей для исполнения очереди заданий выглядит так: основной цикл псевдокода исполнителя:
Пусть задача TB закончила выполнение очередной job и стала в очередь на мьютекс. Задача TC поставила новый job в пустую очередь и отпустила мьютекс. TC делает notify_one(), шедулер дёргает проснуться TA (TB сейчас не в ожидании, на неё не поставят). TA становится в очередь на мьютекс. TC отпускает мьютекс (до notify_one() или после — почти без разницы), TB захватывает мьютекс, начинает выполняться, забирает задание, очередь становится пустой. TA приходит за заданием, а его нет — TB забрала раньше. TA идёт спать на новый круг.
Оптимизировав логику шедулера, можно сократить частоту таких левых пробуждений, но свести до нуля — не получается. А ещё есть случаи старта новых задач в пул, и с ними надо тоже синхронизироваться.
Здравствуйте, netch80, Вы писали:
BFE>>Поэтому я не вижу никакого прямого способа использования этого примитива, кроме как превратив CV в что-то более логичное, например в объект Event (Событие). N>Именно что напрямую и используется. Сделать Event на её основе, конечно, можно, но не подходит в заметной части случаев, и излишне в большинстве остальных.
Разве есть что-то такое, что можно сделать на CV и нельзя на Event?
Что же касается излишеств, то экономить на спичках в большинстве случаев не следует.
N>Пусть задача TB закончила выполнение очередной job и стала в очередь на мьютекс. Задача TC поставила новый job в пустую очередь и отпустила мьютекс. TC делает notify_one(), шедулер дёргает проснуться TA (TB сейчас не в ожидании, на неё не поставят). TA становится в очередь на мьютекс. TC отпускает мьютекс (до notify_one() или после — почти без разницы), TB захватывает мьютекс, начинает выполняться, забирает задание, очередь становится пустой. TA приходит за заданием, а его нет — TB забрала раньше. TA идёт спать на новый круг.
Это описание выглядит как описание типичной ошибки race condition: два независимых события 'пробудить очередь' и 'отдать-захватить мьютекс' не синхронизированы.
N>Оптимизировав логику шедулера, можно сократить частоту таких левых пробуждений, но свести до нуля — не получается. А ещё есть случаи старта новых задач в пул, и с ними надо тоже синхронизироваться.
Я не верю, что задача является неразрешимой.
И каждый день — без права на ошибку...
Re[11]: C++11: Синхронизация - Условные переменные и ложные п
Здравствуйте, B0FEE664, Вы писали:
BFE>Здравствуйте, netch80, Вы писали:
N>>Пусть задача TB закончила выполнение очередной job и стала в очередь на мьютекс. Задача TC поставила новый job в пустую очередь и отпустила мьютекс. TC делает notify_one(), шедулер дёргает проснуться TA (TB сейчас не в ожидании, на неё не поставят). TA становится в очередь на мьютекс. TC отпускает мьютекс (до notify_one() или после — почти без разницы), TB захватывает мьютекс, начинает выполняться, забирает задание, очередь становится пустой. TA приходит за заданием, а его нет — TB забрала раньше. TA идёт спать на новый круг.
BFE>Это описание выглядит как описание типичной ошибки race condition: два независимых события 'пробудить очередь' и 'отдать-захватить мьютекс' не синхронизированы.
Конкретно в этом примере нет гонки с пробуждением. Он же позвал notify_one внутри захвата мьютекса. Ядро в ответ на вызов notify_one тут же будит поток (который попадает в планировщик), и лишь потом отпускает мьютекс. Что это тогда, если не самая лучшая синхронизация? :)
Если другая проблема: потоки могут тупить по сотне разных причин. То им процессора не достанется, или достанется, но их другой поток тут же вытеснит, то page fault случится. В результате задание из очереди получает более расторопный поток (и это хорошо). И по большому счёту эта ситуация эквивалентна другой ситуации, в которой нет никаких условных переменных и сигналов пробуждения, а просто есть два потока-исполнителя стартующих одновременно и единственное задание в очереди. Никто же не называет гонкой ситуацию, что задание может достаться одному исполнителю или другому:
std::vector<int> v = {{1}};
std::for_each(std::execution::parallel_policy, begin(v), end(v), fn);
— не гонка, что fn будет вызван в случайном потоке из пула.