О последней задачке (что удалось увидеть сразу, может там еще что есть). Два консьюмера одновременно проходят safe_empty(). Первый успевает сделать safe_pop(). А вот второму достанется пустая очередь - safe_pop() и бум.
Немного errata в отношении пока не замеченных в комментариях ошибок для bounded MPMC queue / stack. (1) В случае multi-producer делать wake_and_done надо на всех продьюсеров один раз иначе бывает так что консьюмеры повыходили но не все продьюсеры ещё закончили. (2) Саму функцию wait_and_pop надо делать с сигнатурой bool wait_and_pop(T &Data) чтобы понимать есть там что консьюмить или нет (3) Внутри wait_and_pop условие должно выглядеть так: CondCons.wait(Lk, [this] { return !empty() || done(); }); if (empty()) return false; Даже если у нас сигнализирован done, задачи могут быть недоразобраны и поэтому критерий return false это только empty. (4) Внутри push признак done вообще проверять не надо т.к. это признак отработки всех продьюсеров. Там играет роль только full. (5) В интерфейсе необходим метод is_empty_and_done() чтобы по нему консьюмеры понимали когда начинать выходить. Полу-финальные обновлённые версии bounded stack и bounded queue с тестами на то что они не теряют задач я выложил сюда: github.com/tilir/cpp-masters/blob/master/queues/classic_queue.cc github.com/tilir/cpp-masters/blob/master/queues/classic_stack.cc Начну с этого следующую лекцию.
Выходит, что метод is_empty_and_done - вводящий в заблуждение, т.к. зависит от того, действительно ли пользователь закончил пользоваться push'ами. И еще должен быть соблюден строгий гайдлайн: 1) закончить push'ы и синхронизировать всех продюсеров 2) вызвать wake_and_done 3) только теперь сихронизировать консюмеров И даже после этого мы можем продолжать пользоваться и Push и Pop. Ибо флаг Done ограничивает только возможность засыпать у консьюмера) wake_and_done тоже нужно защитить мьютексом, саму переменную Done. И вот если из Push начать возвращать false, если Done уже стоит, то тогда is_empty_and_done уже не поменяется.
А если поток задач бесконечен, то как будто бы (4) done в push будет полезен. Или задача на данном этапе звучит как MPMC и через нее пропустить гарантированно N задач? зы: Хотя я бы может извне сделал байпас на пуше для бесконечного потока задач, чтобы очередь на себя много не брала. Либо push возвращал бул, чтобы пользователь-продюсер мог понять, что пушить нельзя, и что-то с этим сделал. Не нравится в такой схеме, что о подобных событиях продюсер узнает от очереди.
Я вообще думал, что остатки из очередей единственно правильно консъюмить после остановки всех потоков перед непосредственным удалением очереди в месте владения очереди.
Спасибо за наблюдательность. Я действительно взял не тот пример. Впрочем bounded очередь не особо сложнее, но там нужно две позиции и разбирать с хвоста. Начну с этого следующую лекцию. Интересно что та же ошибка в курсе 2019-20 года осталась незамеченной на много лет (в т.ч. мной). Сильно растёт уровень аудитории...
Здраствуйте, Константин. Хотел задать вопрос - будут ли выложены на ютуб лекции по языку С с первого курса? Очень хотелось бы увидеть на ютубе лекции по этому языку в вашем исполнении, уж слишком у вас шикарная манера подачи учебного материала)
@@tilir Спасибо за вашу работу, в одном из прошлых видео в комментариях еще спрашивали про курс ассемблера. Лично я очень жду и С, и ассемблер, надеюсь у вас все получится в следующем учебном году :)
@@tilir да. Уже немало времени учу с++ самостоятельно (может года полтора, с перерывами) но чувствуются пробелы в знаниях, связанные с С и ассемблером. Надо, наверное, было начинать изучение с них. Но так уж сложилось) Поэтому очень-очень жду записи лекций первого курса. Ваши лекции лучшие!
По unbounded_queue с 33-ей минуты есть вопрос. void wait_and_pop(T& Data) { ... Cond.wait(Lk, [this]{ return !Q.empty(); }); T Task = std::move(Q.front()); // а это корректно в случае лимитера? у нас не будет лимитер moved-from после того // как первый из проснувшихся консюмеров его обработает? ... } Не правильнее ли будет сделать мув после проверки? void wait_and_pop(T& Data) { ... Cond.wait(Lk, [this]{ return !Q.empty(); }); T& Task = Q.front(); if (Task == Limiter) { ... } ... Data = std::move(Task); Q.pop(); }
32:09 Почему мы делаем Q.wake_and_done() в consume? Мы точно знаем, что в produce он выполнится первым, и зачем-то пушим лимитеры второй раз. Или я что-то не так понял?
Константин говорит о неком TLA который позволяет отследить, что многопоточный код работает верно. Речь об этом на 28 минуте. Можете кто нибудь поделится референсами на этот инструментарий ?
О последней задачке (что удалось увидеть сразу, может там еще что есть).
Два консьюмера одновременно проходят safe_empty().
Первый успевает сделать safe_pop().
А вот второму достанется пустая очередь - safe_pop() и бум.
Да тривиальный API race. Мои поздравления, отличная скорость решения ))
Немного errata в отношении пока не замеченных в комментариях ошибок для bounded MPMC queue / stack.
(1) В случае multi-producer делать wake_and_done надо на всех продьюсеров один раз иначе бывает так что консьюмеры повыходили но не все продьюсеры ещё закончили.
(2) Саму функцию wait_and_pop надо делать с сигнатурой bool wait_and_pop(T &Data) чтобы понимать есть там что консьюмить или нет
(3) Внутри wait_and_pop условие должно выглядеть так:
CondCons.wait(Lk, [this] { return !empty() || done(); });
if (empty())
return false;
Даже если у нас сигнализирован done, задачи могут быть недоразобраны и поэтому критерий return false это только empty.
(4) Внутри push признак done вообще проверять не надо т.к. это признак отработки всех продьюсеров. Там играет роль только full.
(5) В интерфейсе необходим метод is_empty_and_done() чтобы по нему консьюмеры понимали когда начинать выходить.
Полу-финальные обновлённые версии bounded stack и bounded queue с тестами на то что они не теряют задач я выложил сюда:
github.com/tilir/cpp-masters/blob/master/queues/classic_queue.cc
github.com/tilir/cpp-masters/blob/master/queues/classic_stack.cc
Начну с этого следующую лекцию.
Выходит, что метод is_empty_and_done - вводящий в заблуждение, т.к. зависит от того, действительно ли пользователь закончил пользоваться push'ами.
И еще должен быть соблюден строгий гайдлайн:
1) закончить push'ы и синхронизировать всех продюсеров
2) вызвать wake_and_done
3) только теперь сихронизировать консюмеров
И даже после этого мы можем продолжать пользоваться и Push и Pop. Ибо флаг Done ограничивает только возможность засыпать у консьюмера)
wake_and_done тоже нужно защитить мьютексом, саму переменную Done.
И вот если из Push начать возвращать false, если Done уже стоит, то тогда is_empty_and_done уже не поменяется.
А если поток задач бесконечен, то как будто бы (4) done в push будет полезен. Или задача на данном этапе звучит как MPMC и через нее пропустить гарантированно N задач?
зы: Хотя я бы может извне сделал байпас на пуше для бесконечного потока задач, чтобы очередь на себя много не брала. Либо push возвращал бул, чтобы пользователь-продюсер мог понять, что пушить нельзя, и что-то с этим сделал. Не нравится в такой схеме, что о подобных событиях продюсер узнает от очереди.
Я вообще думал, что остатки из очередей единственно правильно консъюмить после остановки всех потоков перед непосредственным удалением очереди в месте владения очереди.
Мне кажется очередь с 15:00 является на самом деле стеком
Спасибо за наблюдательность. Я действительно взял не тот пример. Впрочем bounded очередь не особо сложнее, но там нужно две позиции и разбирать с хвоста. Начну с этого следующую лекцию.
Интересно что та же ошибка в курсе 2019-20 года осталась незамеченной на много лет (в т.ч. мной). Сильно растёт уровень аудитории...
Здравствуйте, Константин. Спасибо за интересную лекцию!
На слайде 42 (1:13:33) не должно ли быть std::forward_as_tuple вместо std::make_tuple?
Здраствуйте, Константин. Хотел задать вопрос - будут ли выложены на ютуб лекции по языку С с первого курса? Очень хотелось бы увидеть на ютубе лекции по этому языку в вашем исполнении, уж слишком у вас шикарная манера подачи учебного материала)
Я очень хочу их в каком то виде записать и выложить. Возможно следующий учебный год как раз на этом сконцентрируюсь.
@@tilir Спасибо за вашу работу, в одном из прошлых видео в комментариях еще спрашивали про курс ассемблера. Лично я очень жду и С, и ассемблер, надеюсь у вас все получится в следующем учебном году :)
Это один и тот же курс. Странно учить одно без другого.
@@tilir да. Уже немало времени учу с++ самостоятельно (может года полтора, с перерывами) но чувствуются пробелы в знаниях, связанные с С и ассемблером. Надо, наверное, было начинать изучение с них. Но так уж сложилось) Поэтому очень-очень жду записи лекций первого курса. Ваши лекции лучшие!
По unbounded_queue с 33-ей минуты есть вопрос.
void wait_and_pop(T& Data) {
...
Cond.wait(Lk, [this]{ return !Q.empty(); });
T Task = std::move(Q.front()); // а это корректно в случае лимитера? у нас не будет лимитер moved-from после того
// как первый из проснувшихся консюмеров его обработает?
...
}
Не правильнее ли будет сделать мув после проверки?
void wait_and_pop(T& Data) {
...
Cond.wait(Lk, [this]{ return !Q.empty(); });
T& Task = Q.front();
if (Task == Limiter) {
...
}
...
Data = std::move(Task);
Q.pop();
}
Да интересное и тонкое замечание, спасибо. В этом примере роли не играет но в общем случае вы конечно правы.
Только хотел написать, как увидел что уже заметили)
32:09
Почему мы делаем Q.wake_and_done() в consume? Мы точно знаем, что в produce он выполнится первым, и зачем-то пушим лимитеры второй раз. Или я что-то не так понял?
Честно не знаю насколько хорошо плавают котята, но надеюсь фраза "всплывают 3 белых котенка" не несет зловещего смысла)
Константин говорит о неком TLA который позволяет отследить, что многопоточный код работает верно. Речь об этом на 28 минуте. Можете кто нибудь поделится референсами на этот инструментарий ?
th-cam.com/video/_9B__0S21y8/w-d-xo.html
Мне кажется, что std::reference_wrapper можно принимать в функции просто by value
Ну всё-таки там ссылочная семантика и хочется сохранить указание на это в параметрах.