**Синхронизация между процессами - необработанная версия — Transcript & Summary | SozAI**
Source: https://sozai.app/transcript/inter-process-synchronization-raw-version/

Лекция о синхронизации между процессами: барьеры std::barrier в C++20, их отличие от мьютексов, дедлоки, shared memory и именованные доменные сокеты.

## Key Takeaways

- std::barrier синхронизирует фазы параллельных вычислений и может использоваться многократно
- Мьютекс защищает операции от одновременного выполнения, барьер согласует параллельные этапы работы
- Для межпроцессного взаимодействия нужны отдельные механизмы: shared memory, сокеты, блокировки файлов
- Аварийное завершение участника требует явной обработки (arrive\_and\_drop), иначе остальные зависнут

## What the video covers

- Введение: переход от синхронизации потоков внутри процесса к синхронизации между процессами
- Барьер (std::barrier, C++20): потоки ждут друг друга в конце фазы вычислений, а не защищают критическую секцию как мьютекс
- Методы arrive\_and\_wait и arrive\_and\_drop; функция завершения (completion function) для обновления общего состояния
- Барьер гарантирует видимость изменений памяти между фазами, но не решает проблему аварийного завершения участников
- std::latch как одноразовая синхронизация в отличие от многократно используемого std::barrier
- Дедлоки: четыре условия Коффмана, стратегии избежания, использование std::lock для захвата нескольких мьютексов
- Инверсия приоритетов в планировании задач и её влияние на высокопроизводительные приложения
- Общая разделяемая память (shared memory, mmap) и проблема одновременного инкремента счётчика между процессами
- Доменные сокеты: режимы SOCK\_STREAM и SOCK\_DGRAM, именованные доменные сокеты для независимых процессов
- Блокировка файлов: shared lock (LOCK\_SH) и exclusive lock (LOCK\_EX)

## Chapters

1. 00:00 Введение и повторение синхронизации потоков
2. 05:00 Барьер синхронизации и std::barrier
3. 22:45 Дедлоки и инверсия приоритетов
4. 42:00 Разделяемая память и проблема синхронизации
5. 71:00 Доменные сокеты
6. 90:00 Блокировка файлов и заключение

Answers

## Questions about this video

Чем барьер отличается от мьютекса?

Мьютекс не позволяет потокам одновременно выполнять определённые операции. Барьер же заставляет потоки дождаться друг друга в конце фазы, чтобы согласованно перейти к следующему этапу параллельных вычислений.

Что делать, если один из потоков аварийно завершился при использовании барьера?

Барьер сам не решает эту проблему: остальные потоки будут ждать. Поток, который выходит из работы, должен вызвать arrive\_and\_drop, чтобы его больше не учитывали в последующих фазах.

Какие механизмы межпроцессного взаимодействия рассматриваются в лекции?

Рассматриваются общая разделяемая память (shared memory с mmap), доменные сокеты (включая именованные для независимых процессов) и блокировки файлов (shared и exclusive lock).

## Full Transcript — Download SRT & Markdown

00:00

Speaker A

Всем привет. На прошлой неделе мы с вами разбирали синхронизацию внутри одного процесса, и сегодня планировали начать синхронизацию между процессами.

00:15

Speaker A

Но там осталось ещё несколько тем, которые внутри по внутрипроцессной коммуникации, а точнее, синхронизации. Давайте поговорим про несколько важных, значит, примитивов синхронизации, которые внутри одного процесса позволяют вам синхронизировать работу потоков.

00:32

Speaker A

Но там осталось ещё несколько тем, которые внутри по внутрипроцессной коммуникации, а точнее, синхронизации. Давайте поговорим про несколько важных примитивов синхронизации, которые внутри одного процесса позволяют вам синхронизировать работу потоков.

00:41

Speaker A

Представим, что у нас есть три потока. Вот они тут показаны на слайде, которые параллельно выполняют некоторую вычислительную задачу. Каждый поток обрабатывает свою часть данных и [вздыхает] э работа состоит из нескольких этапов и перед началом очередного этапа нужно убедиться, что

01:07

Speaker A

Мы с вами познакомились с мьютексами, семафорами и условными переменными. Теперь рассмотрим ещё один примитив под названием барьер.

01:30

Speaker A

э некорректный некорректный результат. Вот. И здесь нам как пригодится барьер. То есть каждый поток выполняет свою часть работы и достигнув барьера, ожидает остальных.

01:45

Speaker A

Представим, что у нас есть три потока. Вот они тут показаны на слайде, которые параллельно выполняют некоторую вычислительную задачу. Каждый поток обрабатывает свою часть данных, и работа состоит из нескольких этапов. Перед началом очередного этапа нужно убедиться, что все участники завершили предыдущий этап.

01:57

Speaker A

И вот они тут все приостановились. И когда последний из участников достигает барьера, текущая фаза завершается. Все три потока могут продолжить работу. Кроме того, некоторые из потоков могут взять и выполнить ещё какую-нибудь дополнительную операцию, так называемый завершающий шаг. Ну,

02:15

Speaker A

Например, мы моделируем некий физический процесс: на каждой итерации нужно вычислить новое состояние системы на основе предыдущего. Но если один из потоков начнёт просчитывать новое состояние до того, как все его собратья закончили работу, мы получим некорректный результат. Вот здесь нам и пригодится барьер. То есть каждый поток выполняет свою часть работы и, достигнув барьера, ожидает остальных.

02:20

Speaker A

[вздыхает][фыркает] Обратите внимание, значит, барьер не означает, что потоки после этого барьера продолжат работу строго одновременно. То есть планировщик по-прежнему самостоятельно решает, кому и когда выделять процессорное время, но гарантируется, что ни один из них, вот из этих потоков, не сможет пройти точку

02:38

Speaker A

Вот, например, поток А завершил свои вычисления раньше, он приостанавливается. Потом завершил работу поток С, он тоже приостанавливается, и последним завершил работу поток Б.

02:53

Speaker A

Э отличие э от барьера от мьютекса заключается в назначении. Мьютекс нужен для того, чтобы не позволить потокам одновременно выполнять определённые операции, а барьер, наоборот, помогает нескольким потокам согласованно выполнять параллельные вычисления.

03:11

Speaker A

И вот они тут все приостановились. Когда последний из участников достигает барьера, текущая фаза завершается, и все три потока могут продолжить работу. Кроме того, некоторые из потоков могут выполнить ещё какую-нибудь дополнительную операцию, так называемый завершающий шаг. Ну, допустим, одному из потоков нужно что-то такое сделать. Они сами договорились, кто что будет делать.

03:30

Speaker A

ворота откроются только тогда, когда все добегут. Вот что такое барьер. Э, начиная с C++ 20, в стандартной библиотеке есть готовая реализация барьера кроссплатформенная std::barrier.

03:42

Speaker A

Обратите внимание: барьер не означает, что потоки после него продолжат работу строго одновременно. Планировщик по-прежнему самостоятельно решает, кому и когда выделять процессорное время, но гарантируется, что ни один из этих потоков не сможет пройти точку синхронизации до завершения очередной фазы всеми участниками. То есть тут у нас две фазы: сперва все выполнили первую фазу, потом приступили ко второй.

03:50

Speaker A

Каждый из потоков сначала вызывает функцию prepare data, которая выполняет первую фазу работы. Затем э-э, каждый из потоков вызывает функцию метод arrive arrive and wait, есть э-э, он говорит, что я до дошёл до барьера, и я буду ждать всех остальных участников.

04:12

Speaker A

Отличие барьера от мьютекса заключается в назначении. Мьютекс нужен для того, чтобы не позволить потокам одновременно выполнять определённые операции, а барьер, наоборот, помогает нескольким потокам согласованно выполнять параллельные вычисления.

04:26

Speaker A

А важная особенность std::barrier - это возможность многократного использования. Например, есть стандартной библиотеке ещё класс std::latch, э-э, latch можно перевести как защёлка или шпингалет, вот э-э который предназначен для однократной синхронизации.

04:42

Speaker A

Можно представить барьер как лошадей на ипподроме. Перед всеми ними стоят ворота, и они все должны как-то стартовать вовремя. Они стартовали и добежали до следующих ворот, и те, кто прибежал первым, будут ждать, и ворота откроются только тогда, когда все добегут. Вот что такое барьер.

05:05

Speaker A

обновления общего состояния, например, переключить буферы или увеличить номер итерации. Также, кроме arrive and wait, есть ещё операция arrive and drop. Она нужна, если поток завершает участие в вычислениях и больше не должен учитывать в последующих фазах барьера. Просто если

05:21

Speaker A

Начиная с C++20, в стандартной библиотеке есть готовая кроссплатформенная реализация барьера: std::barrier.

05:37

Speaker A

кто-то упал, он должен хотя бы сказать arrive and drop, да, и в следующий раз тогда уже будут ждать двоих.

05:45

Speaker A

То есть при создании барьера мы указываем количество участников, в нашем случае это три потока. Каждый из потоков сначала вызывает функцию prepare\_data, которая выполняет первую фазу работы. Затем каждый из потоков вызывает метод arrive\_and\_wait: он говорит, что дошёл до барьера и будет ждать всех остальных участников.

05:57

Speaker A

Э, изменения, сделанные участниками до барьера, каждый в своём потоке, гарантированно станут видны э всем другим участникам после барьера.

06:09

Speaker A

Здесь у нас запущено три потока, и смотрите: в тот момент, когда все три потока вызовут arrive\_and\_wait, барьер откроется, и дальше они будут обрабатывать результаты.

06:27

Speaker A

поток А записал что-то в переменную Х, то все другие потоки после барьера увидят эту запись. То есть до барьера не гарантируется, что они эту запись увидят.

06:39

Speaker A

Важная особенность std::barrier — возможность многократного использования. Для сравнения, в стандартной библиотеке есть ещё класс std::latch. Latch можно перевести как защёлка или шпингалет; он предназначен для однократной синхронизации.

06:47

Speaker A

Угу. Однако [откашливается] барьер не решает автоматически проблемы аварийного завершения участников. Если один из потоков перестанет работать и не достигнет барьера, остальные могут остаться в ожидании.

07:00

Speaker A

Ещё одна полезная возможность std::barrier — функция завершения, completion function. Это функция, которая выполняется один раз после прибытия всех участников, но до того, как ожидающие потоки продолжат выполнение. Её можно использовать для обновления общего состояния, например, чтобы переключить буферы или увеличить номер итерации.

07:17

Speaker A

То есть если вы в однопоточной программе где-то выбросили исключение и может быть вам с большей вероятностью повезёт, то в многопоточной программе если [фыркает] Что такое?

07:27

Speaker A

Также, кроме arrive\_and\_wait, есть ещё операция arrive\_and\_drop. Она нужна, если поток завершает участие в вычислениях и больше не должен учитываться в последующих фазах барьера. Если какой-то из потоков до точки синхронизации не вызовет arrive\_and\_wait, все остальные будут ждать. Представьте, что бегут три всадника на лошадях, и одна из лошадей упала: барьер не откроется, пока все три не добегут. Так вот, если кто-то упал, он должен хотя бы сказать arrive\_and\_drop, и в следующий раз уже будут ждать двоих.

07:41

Speaker A

То есть если [откашливается] у нас какой-то во время вызова при prepare\_data выбросится исключение, соответственно, arrive\_and\_wait мы не вызовем.

07:50

Speaker A

Также барьер обеспечивает необходимые гарантии синхронизации памяти между фазами. Это не просто счётчик потоков. Изменения, сделанные участниками до барьера, каждым в своём потоке, гарантированно станут видны всем другим участникам после барьера.

08:00

Speaker A

Понятно? Тоесть, допустим, достаточно было бы написать обработчик catch и в catch вызвать arrive\_and\_drop. Ну, или ещё как-то сделать, в зависимости от вашей программы.

08:15

Speaker A

Если это непонятно, попробую объяснить. В разных ядрах процессора вычисления выполняются несколько асинхронно. Если один из потоков что-то записал в переменную, другой поток не сразу увидит изменения этой переменной. Так вот, после барьера гарантируется, что если поток А записал что-то в переменную X, то все другие потоки после барьера увидят эту запись. До барьера не гарантируется, что они эту запись увидят, а после барьера увидят. То есть после барьера потоки синхронизируют своё видение памяти. Понятно?

08:36

Speaker A

текущего состояния, вот клеточки были так, на следующей итерации они станут таким образом. И мы хотим написать многопоточную версию игры жизнь.

08:50

Speaker A

Угу. Однако барьер не решает автоматически проблему аварийного завершения участников. Если один из потоков перестанет работать и не достигнет барьера, остальные могут остаться в ожидании.

09:00

Speaker A

Вот, мы Как мы можем сделать игру жизнь? Мы можем распараллелить работу между n-ным количеством потоков. Каждый из потоков выполняет своё количество каждый поток выполняет своё обсчитывает своё количество строк.

09:22

Speaker A

Поэтому надо гарантированно вызвать либо arrive\_and\_wait, либо arrive\_and\_drop. Поэтому ещё более важным становится написание надёжного с точки зрения исключений кода.

09:31

Speaker A

Ну, некоторое количество строк изображения, которое он обсчитывает. И смотрите, функция NextCell, она просто вычисляет по правилам игры жизнь следующее состояние клетки.

09:45

Speaker A

Если в однопоточной программе где-то выбросили исключение, вам, возможно, с большей вероятностью повезёт, а в многопоточной программе...

09:59

Speaker A

Вот, э, но рассчитывать следующий шаг, да, то есть использовать данные, которые есть в next, чтобы туда вот в current положить можно э, можно когда?

10:13

Speaker A

Что такое? У меня как будто бы компьютер завис. А нет, компьютер не завис, это у меня просто мышка, видимо, разрядилась. Дайте-ка я попробую выключить и включить. О.

10:38

Speaker A

дальше продолжат работать. И мы тут вызываем swap э, next и current, то есть меняем следующее и текущее состояние. То есть, если в первый раз мы слева-направо перекидывали данные, да, то на следующей итерации мы будем справа-налево перекидывать, потом снова слева-направо,

10:53

Speaker A

То есть если во время вызова prepare\_data выбросится исключение, соответственно, arrive\_and\_wait мы не вызовем. Поэтому код надо написать таким образом, чтобы каким бы образом ни произошёл выход из функции, либо arrive\_and\_wait, либо arrive\_and\_drop был вызван.

11:14

Speaker A

Ну, может быть, поменьше, но не больше, чем 10. Понятно? Теперь поговорим про завершение потоков и отмену ожидания.

11:29

Speaker A

Понятно? То есть, допустим, достаточно было бы написать обработчик catch и в catch вызвать arrive\_and\_drop. Ну, или сделать иначе, в зависимости от вашей программы.

11:46

Speaker A

программу? Ну, например, нажимает Ctrl C или подправляет к программе сигнал. В нормальной программе потоки должны корректно завершить свою работу, потому что с каждым из потоков могут быть какие-то системные ресурсы связаны.

12:00

Speaker A

Рассмотрим конкретный пример, в котором барьеры особенно полезны: игру «Жизнь». Знаете, да, игру «Жизнь»? То есть у нас есть клеточное поле, на котором значение каждой клетки на новой итерации зависит от состояния клетки и её соседей. Правила повторять не буду; вот пример текущего состояния, вот клеточки были так, на следующей итерации они станут таким образом. И мы хотим написать многопоточную версию игры «Жизнь».

12:13

Speaker A

Ну, даже если нам э-э даже если мы хотели бы э-э ну, забить на утечки памяти, которые связаны.

12:24

Speaker A

Почему? Потому что каждая клетка, в общем-то, рассчитывает своё новое состояние независимо от других.

12:31

Speaker A

А мы, если возьмём и этот поток прибьём, то он этот мьютекс уже не освободит.

12:35

Speaker A

Как мы можем это сделать? Мы можем распараллелить работу между n потоками. Каждый поток обсчитывает своё количество строк. То есть каждый поток начинается со строки, соответствующей его ID, и у каждого потока есть некоторая высота: некоторое количество строк изображения, которое он обсчитывает. Функция NextCell просто вычисляет по правилам игры следующее состояние клетки.

12:44

Speaker A

Соответственно, принудительная остановка потока этого нам добиться не смо- не может она поскольку может оставить программу в некорректном состоянии.

12:53

Speaker A

Но теперь смотрите. Точнее, у нас есть буфер current, и в буфере next каждый поток рассчитывает новое состояние,

13:11

Speaker A

это сделать с помощью стоп-токена, то есть запросить остановку. И тот поток, который поддерживает свою остановку, он должен проверять значение стоп-токена, и если он увидел, что его просят завершиться, он берёт и м-м как красиво завершает свою работу, то есть

13:29

Speaker A

без утечек памяти, освободив все необходимые ресурсы. Пример, как это можно сделать, мм, смотрите, если вы используете JThread, то у вас это всё работает из коробки. Мм, если функцию потока вы первым аргументом передадите стоп-токен, то туда передастся стоп-токен, который

13:47

Speaker A

хранится прямо внутри JThread'а. То есть внутри JThread'а есть стоп-токен, точнее, даже не стоп-токен. А, внутри JThread'а хранится стоп-сорс.

13:58

Speaker A

Стоп-сорс - это объект, с помощью которого можно запросить э, остановку, а стоп-токен - это фактически объект, у которого можно узнать, запросили ли он остановку или не запросили.

14:12

Speaker A

Как неудобно без без мышки. Сейчас я, погодите, мышку подключу к своему компьютеру. Оп. [вздыхает] Так, на чём я остановился?

14:42

Speaker A

А, вот поток выполняется, э, выполняет работу свою и если мы его, у, у потока Worker вызовем э, RequestStop, то после очередной итерации он увидит, что у него запросили остановку и цикл while завершит.

14:59

Speaker A

Автоматически стоп запрашивается в деструкторе э, JThread'а. То есть даже если вы RequestStop не сделали, через стоп-токен вы можете узнать.

15:14

Speaker A

Теперь рассмотрим более интересную ситуацию. Представим рабочий поток, который ждёт появления данных. Для ожидания он использует условную переменную condition variable.

15:26

Speaker A

Так сейчас секундочку. Пока данные недоступны, поток заблокирован и не расходует процессорное время. Но как его остановить? И И просто вызвать request stop не получится, потому что обычная э, условная переменная не обязана автоматически пробудить такой поток. То есть он внутри wait'а будет сидеть и

15:47

Speaker A

ждать. Но C++ 20 дополнительно появился ещё condition variable any, который, э, которая поддерживает ещё стоп-токены. То есть мы функцию wait э, помимо unique lock'а можем передать ещё стоп-токен.

16:03

Speaker A

И тогда вот эта вот функция, которая проверяет готовность, она будет пробуждена когда э, точнее, э, не пробуждена, а функция wait прекратит ожидание, когда через э, к- когда будет запрошена остановка.

16:19

Speaker A

То есть мы сюда ещё стоп-токен передали. И вот смотрите, если мы рабо- э, рабочему потоку говорим request stop, а он в это время ждал условную переменную, то метод wait завершит свою работу, то есть мы не будем про- попадать сюда, мы просто

16:38

Speaker A

завершим свою работу и здесь уже проверим, есть у нас данные или нету данных. Если данных у нас нету, [кашель] [откашливается] где у нас там has data это? А, вот has data, э, пе- переменная wait э, возвращает нам true, если мы

16:56

Speaker A

дождались, и false, если сработал стоп-токен. Э, то есть если м- данных не пришло, то есть мы вышли не потому, что ready, а потому, что здесь, э, то значит, э, у нас запросили остановку, и мы раньше времени заканчиваем свою работу. В противном

17:17

Speaker A

случае, ну, каким-то образом эти данные обрабатываем. Понятно? Упс. Теперь вернёмся к задаче производителей и потребителя, которую мы уже на прошлой лекции разбирали.

17:38

Speaker A

Предположим, что программа использует у нас ограниченную очередь, то есть очередь, в которой е ограниченная вместимость, туда больше некоторого количества элементов положить нельзя.

17:48

Speaker A

Когда очередь пустая, потребители блокируются на условной переменной not empty. Но представим себе, что приложение должно завершить свою работу. То есть производители остановились и новых элементов больше не будет. А что произойдёт с потребителями?

18:05

Speaker A

Если они просто будут ждать условия выполнения, то есть пока ждут, пока размер не станет больше нуля, то они могут остаться заблокированными навсегда.

18:19

Speaker A

И чтобы решить эту проблему, необходимо предусмотреть явный протокол закрытия очереди. Поэтому мы можем в наш класс добавить дополнительную переменную closed.

18:31

Speaker A

Так? И ожидание будет завершаться не только тогда, когда, ну, метод pop потребитель будет завершать ожидание не только тогда, когда появится новый элемент, но и когда ещё был была запрошена остановка.

18:52

Speaker A

И дальше мы из метода pop должны что-то вернуть да? Так, что-то у меня тут size равно the optional. Тут надо return, конечно же, сделать. Я, подождите, забыл написать return.

19:20

Speaker A

Ох, как-то так. Э-э, дальше смотрите, нам надо предусмотреть метод close. И когда программа хочет завершить свою работу, она у этой очереди вызывает метод close.

19:39

Speaker A

Сперва мы должны завершить мьютекс, ой, захватить мьютекс и в переменную close положить true. После этого мы освобождаем мьютекс, ну, чтобы дать возможность другим потокам э-э, иметь доступ к переменным класса. И должны все потоки и которые ожидают то, что очередь не

20:00

Speaker A

пустая и то, что она э-э, не заполненная, пробудиться. То есть и метод pop, и метод push должны э-э, пробудиться, то есть точнее, ну, точнее и потоки, ожидающие на заполненности данных, и потоки, ожидающие их появления, э-э, должны пробудиться, увидеть

20:23

Speaker A

переменную closed и как-то скорректировать свою работу. Не совсем понятно, зачем мы э-э, not full уведомляем?

20:36

Speaker A

А чтобы потоки, которые ждут, пока в очереди освободится, э-э, освободятся, освободиться место, они чтобы могли тоже пробудиться и свою ну, и допустим, в push больше не добавлять.

20:51

Speaker A

Типа, они пробудились и э-э ну в общем, чтобы они дальше просто обработали, что ли, что очередь пустая и больше туда писать нельзя.

21:00

Speaker A

Вот смотрите, у нас есть состояние open, когда очередь открыта, и можно и push, и pop выполнять. Если мы очередь закрыли, то только можно с помощью поп данные извлекать, а пушить уже в неё нельзя.

21:13

Speaker A

Поэтому мы и пробуждаем и тех, и других. Мы должны пробудить потоки, которые пушат, чтобы они там не смогли сделать пуш, ну, типа, всё, данные больше закидывать нельзя. И они могут, допустим, выбросить исключение или там фолс вернуть, сказать, что всё,

21:32

Speaker A

очередь не принимает данные. А те, кто делает поп, ну, чтобы они успели обработать те данные, которые туда попали.

21:40

Speaker A

Вот. Теперь давайте поговорим ещё про несколько проблемных ситуаций, с которыми вы можете столкнуться, когда будете писать многопоточные приложения, использовать синхронизацию.

21:53

Speaker A

[тяжело вздыхает] Вот, есть такая штука, как взаимная блокировка или дедлок. Э, то есть, это проблема, которая возникает даже тогда, когда каждый мьютекс используется правильно.

22:06

Speaker A

Представим себе два потока и два общих ресурса. Для защиты первого ресурса используется мьютекс один, для э ресурса для второго ресурса используется мьютекс два.

22:17

Speaker A

Вот, поток номер один сначала захватывает мьютекс М1, потом захватывает мьютекс М2, то есть, он дождался э разрешения доступа к обоим ресурсам и начинает выполнять работу, связанную с с ресурсами, защищёнными этими мьютексами, так?

22:32

Speaker A

А поток два сначала захватывает М М2, а потом захватывает М1. И тоже пытается делать что-то с этими ресурсами. И оказывается, вот в этой ситуации может произойти взаимная блокировка.

22:47

Speaker A

Э, рассмотрим следующий порядок выполнения. Поток номер один захватил первый мьютекс, после этого планировщик переключает выполнение на поток Б, ну, на на поток два, который захватывает второй мьютекс.

23:02

Speaker A

Вот, теперь поток номер один пытается захватить э-э, мьютексом два, а тот захваче- захвачен мьютексом М1, и поэтому он ждёт блокируется.

23:13

Speaker A

[тяжело вздыхает] Э, поток два пытается захватить мьютексом один, но он захвачен вот этим. И каждый из них ждёт друг друга, и ждать они будут бесконечно.

23:27

Speaker A

Ну, точнее как, вполне возможно, что они будут ждать друг друга бесконечно, если реализация мьютексов не способна обнаруживать такие ситуации. Ну, в стандартной библиотеке, э-э, это считается дедлок, и, в общем-то, там что-то неопределённое поведение или, там, зависание у вас будет.

23:44

Speaker A

Представьте себе двух детей, да? Первый хочет допустим погремушку второй хочет, э-э, там, дудочку. Первый хватает А, нет, даже не так.

23:55

Speaker A

Была сказка Дудочка и Кувшинчик. Не слышали? Ну, ладно, прочитайте сказку Дудочка и Кувшинчик, это это как раз дедлок.

24:04

Speaker A

Девочке нужна была дудочка, чтобы она задудела и ягодки появились, а, э, там, какому-то грибу нужен был кувшинчик, чтобы, ну, не знаю, зачем-то.

24:14

Speaker A

И пока не И если девочке нужно э-э девочка дудела в дудочку, ягодки появлялись, но ей нужен был кувшинчик, чтобы эти ягоды собрать. Она бежала за кувшинчиком, а ягодки прятались. Ну, в общем, был дедлок.

24:27

Speaker A

Ладно, может быть, ситуация не такая, но всё равно сказка интересная, но жаль, что вы её в школе или в детском саду не читали.

24:35

Speaker A

И вот у меня есть лекция про дедлок целая, вы можете подробнее почитать э-э, на канале. [откашливается] Ну, точнее послушать.

24:44

Speaker A

Значит, почему, в какой ситуации возникает дедлок? В классической теории операционных систем выделяют четыре необходимых условия возникновения взаимной блокировки, их ещё называют условиями Кофмана.

24:57

Speaker A

Первое условие - это взаимное исключение. То есть это означает, что некоторый ресурс в каждый момент времени может принадлежать только одному участнику.

25:05

Speaker A

Второе условие это hold and wait. То есть поток уже удерживает один ресурс, но при этом пытается получить другой и блокируется, не освобождая первый.

25:15

Speaker A

Именно это с нами и произошло в нашей ситуации с двумя мьютексами. Третье условие это no preemption.

25:22

Speaker A

То есть нельзя просто отобрать захваченный ресурс у другого потока. Владелец должен освободить его добровольно.

25:30

Speaker A

Четвёртое условие это циклическое ожидание. То есть существует замкнутая цепочка, первый поток ожидает ресурс второго, второй третьего и так далее, пока последний не начинает ожидать ресурс первого.

25:41

Speaker A

В нашем примере цикл состоял всего из двух потоков, но в каких-то более сложных ситуациях, там цикл может состоять из нескольких потоков, у вас всё работает, работает, там, не знаю, 1 час программа работает, через полтора часа программа зависла.

25:58

Speaker A

При этом взаимная блокировка не обязательно связана с несколькими мьютексами. Например, поток А может вызвать join у потока Б, удерживая мьютекс, необходимый потоку Б для завершения работы.

26:09

Speaker A

И получается аналогичная проблема. То есть поток А ожидает завершения Б, а Б ожидает завершения освобождения мьютекса, который удерживается потоком А.

26:19

Speaker A

Также важно отличать дедлок от starvation. Starvation это голодание потоков, которое мы встречали в задаче readers-writers.

26:28

Speaker A

То есть при голодании одни потоки продолжают работу, а другой поток долго не получает ресурс. Вот при взаимной блокировке замкнутого цикла вообще ни один из потоков не может продвинуться без внешнего вмешательства.

26:46

Speaker A

То есть все четыре условия должны одновременно выполняться, пока для того, чтобы возник дедлок. Если хотя бы один из них, одно из этих условий не выполняется, у вас дедлока не будет.

26:58

Speaker A

Это уже хорошо, да? Гораздо хуже было бы, если бы хотя бы одно из этих условий выполнялось, да?

27:06

Speaker A

Нет. Всё-таки не всё так плохо. И мы можем писать надёжные программы. Хорошая новость заключается в том, что большинство взаимных блокировок можно предупредить ещё на этапе проектирования программы.

27:22

Speaker A

Самый простой подход - это единый порядок захвата ресурсов. То есть, если у нас есть два мьютекса m1 и m2, то можно договориться, что любой поток, которому нужны оба ресурса, должен их сперва захватить m1, а потом m2.

27:35

Speaker A

Это позволяет устранить циклическое ожидание между этими блокировками. Вот, но программисту необходимо следить за соблюдением такого соглашения во всех участках программы.

27:46

Speaker A

Казалось бы. Так, не то чтобы, то есть, если это два потока, это хорошо, ой, два мьютекса, то хорошо, да? Легко захватить. А если нам надо захватить 10 мьютексов?

28:02

Speaker A

Ну, самое простое, что может прийти в голову, это взять и отсортировать все эти мьютексы в одном порядке, так?

28:11

Speaker A

Мм. И в одном порядке захватить. Если все потоки один и тот же набор мьютексов захватывают, это хорошо. Но что если наборы мьютексов у потоков частично пересекаются? Не будет ли такого?

28:25

Speaker A

Подумайте сами об этом. И, короче, чтобы вам не заниматься вот этой всей ерундой, есть э более безопасный совместный захват. В C++17 появился [фыркает] std::scoped\_lock.

28:39

Speaker A

Он берёт, вы ему передаёте просто энное количество мьютексов, и он уже э захватит эти мьютексы в правильном порядке.

28:51

Speaker A

[откашливается] То есть, когда вам эти мьютексы нужно все захватить для того, чтобы работать с ресурсами, вы через scoped\_lock захватываете.

28:59

Speaker A

После успешного захвата программа может безопасно работать с, э-э, со всеми общими защищаемыми ресурсами. Не обязательно два мьютекса туда передать, вы можете передать и три, и четыре. Ну, короче, произвольное количество мьютексов.

29:13

Speaker A

Однако, важно понимать границы гарантий. std::scoped\_lock предотвращает э-э, взаимную блокировку при совместном захвате переданных ему мьютексов.

29:24

Speaker A

Но, он не может гарантировать отсутствие всех взаимных блокировок в программе. То есть, если вы в каком-то потоке делаете ещё join, да, и от этого зависит от мьютекса, то scoped\_lock вам не поможет.

29:38

Speaker A

[фыркает] Вот. Поэтому важными остаются э-э, общие рекомендации. Не следует, э-э, удерживать мьютекс во время длительных или блокирующих операций. Не следует удерживать мьютекс, когда, э-э, ваш класс передаёт управление какому-нибудь другому, ну, внешнему коду. Потому что, если этот внешний код вас снова вызовет,

30:05

Speaker A

может ещё раз одна блокировка произойти и будет не очень хорошо. Вот, то есть, чем вообще меньше время удержания ресурса и чем проще протокол захвата тем легче анализировать программу и тем меньше вероятность взаимной блокировки.

30:23

Speaker A

Вот, для нескольких мьютексов удобно использовать именно scoped\_lock, но общий порядок работы с ресурсами всё равно должен быть продуман.

30:31

Speaker A

То есть, если однопоточную программу вы могли писать и о дедлоках не думать, то в многопоточной вот у вас ещё такая проблема появилась.

30:41

Speaker A

Ещё одна проблема, с которой вы можете столкнуться, это инверсия приоритетов. [вздыхает][тяжело вздыхает] Мы рассмотрели ситуацию, когда несколько потоков не могут продолжить работу из-за циклического ожидания, но бывает и другая проблема. Программа не зависает навсегда, однако один из потоков получает процессор слишком поздно.

31:07

Speaker A

Особенно актуальна такая проблема для операционных систем реального времени. Это такие операционные системы, в которых критически важно э-э ограничивать время ожидания определённых событий. Ну, например, если ваша программа управляет каким-нибудь станком или каким-нибудь жизненно важным оборудованием, не знаю, в самолёте.

31:27

Speaker A

Вот, то нельзя сделать так, чтобы какой-нибудь из потоков слишком долго ожидал. Иначе вы либо запорите деталь, либо, например, не подадите в нужный момент сигнал управляющему э-э там элементу самолёта, и самолёт у вас упадёт. Не очень хорошо.

31:45

Speaker A

[откашливается] Представим себе такую ситуацию. У нас есть три потока с разными приоритетами. Поток H имеет самый высокий приоритет, он отвечает за некоторую критически важную задачу например управление оборудованием. В операционных системах можно задавать приоритеты потоков, и операционная система прежде всего отдаёт

32:04

Speaker A

процессорное время вот таким потокам с высоким приоритетом. Поток M имеет средний приоритет, поток L самый низкий.

32:12

Speaker A

И допустим, что сейчас у нас выполняется поток L. Он захватывает мьютекс, чтобы изменить общие данные.

32:21

Speaker A

Затем становится к выполнению поток H. Планировщик ему передаёт процессорное время, поскольку его приоритет выше.

32:30

Speaker A

Однако потоку H тоже требуется мьютекс, который пока удерживается потоком L. Поэтому H будет заблокирован.

32:40

Speaker A

H б- э-э блокируется, планировщик передаёт время потоку M, потому что M имеет более высокий приоритет, чем L.

32:51

Speaker A

И, казалось бы, теперь планировщик должен дать возможность L завершить работу и освободить ресурс, но в этот момент у нас поток M выполняется и смотрите поток H не может получить процессорное время, не смо- И вы то есть в с- с- в данный момент

33:13

Speaker A

выполняется поток M, который не даёт потоку L получить процессорное время, а процесс а поток L задерживает поток H.

33:25

Speaker A

Понятно? То есть выполняется не тот не та задача, которая должна получать процессорное время. Это называется инверсией приоритетов.

33:40

Speaker A

Э-э инверсия приоритетов - это не обязательно дедлок, то есть как только поток M, средний поток, перестанет занимать процессор, L сможет по- продолжить выполнение, освободить мьютекс и H возобновит работу. Но штука в том, что это уже может быть поздно,

33:56

Speaker A

э-э нужный момент времени пропущен и у вас испорчена деталь или испорчен самолёт вместе с пассажирами.

34:05

Speaker A

Как бороться с инверсией приоритетов? Один из наиболее известных механизмов называется наследование приоритета. Вот и вернёмся к нашему примеру с тремя потоками.

34:17

Speaker A

Низкоприоритетный поток L удерживает мьютекс, который необходим высокоприоритетному потоку H. Когда H пытается захватить мьютекс и блокируется, операционная система обнаруживает эту зависимость и тогда приоритет потока L временно повышается до уровня приоритета H. В результате средний приоритетный поток M э, не может вытеснять высокоприоритет,

34:42

Speaker A

ой, низкоприоритетный, но сейчас временно приоритетный поток L. L получает процессорное время, завершает критическую секцию, освобождает мьютекс.

34:52

Speaker A

После освобождения ресурса его приоритет возвращается к обычному значению, если больше нет причин для повышения.

35:00

Speaker A

И тогда H теперь получит процессорное время, ну, точнее, получит блокировку и продолжит выполнение. Таким образом, мы устранили проблему.

35:09

Speaker A

Среднеприоритетные задачи больше не могут произвольно задерживать освобождение ресурса, необходимого высокоприоритетному потоку. Ещё есть один приоритет, ой, подход, это priority ceiling, э, или потолок приоритета.

35:25

Speaker A

В этом случае для мьютекса заранее определяется э, определённый потолок приоритета. Когда поток захватывает такой мьютекс, его эффективный приоритет повышается в соответствии с выбранным протоколом.

35:37

Speaker A

Этот поток удобен в системах реального времени, где набор задач и приоритеты известны заранее. Э, в POSIX существуют специальные атрибуты мьютексов, которые позволяют за, задавать протоколы.

35:49

Speaker A

Например pthread\_prio\_inherit и pthread\_prio\_protect. И есть один известный случай, э э реальный. В девяносто седьмом году космический аппарат, э, Mars Pathfinder приземлился на Марсе, но вскоре после посадки начал периодически перезапускаться.

36:12

Speaker A

Расследование показало, ну, путём в э обмена данными с этим с марсоходом, что причиной была инверсия приоритетов в бортовой системе.

36:23

Speaker A

Э, там использовалась система реального времени VxWorks и низкоприоритетная задача удерживала ресурс, который требовался высокоприоритетной задаче.

36:31

Speaker A

При этом задача со средним приоритетом могли задерживать освобождение ресурса. Вот как раз та ситуация, которую мы рассмотрели.

36:38

Speaker A

В результате высокоприоритетная задача не успевала завершить необходимые операции, срабатывал сторожевой таймер, который перезапускал систему.

36:46

Speaker A

Инженерам удалось воспроизвести проблему на земле, а затем они изменили настройки синхронизации чтобы э-э вот применив механизм наследования приоритета для соответствующего мьютекса.

37:00

Speaker A

И проблема с э-э, вот эти инверсии приоритетов с перезапусками была устранена. Вот, э-э, а если бы у них не было возможности, там, не знаю, менять прошивку то проект на, там, миллионы, наверное, долларов э-э, улетел бы просто в трубу,

37:23

Speaker A

завершился бы неудачей. Вот, то есть, смотрите, мьютекс корректно может защищать данные от гонок, но без соответствующего протокола приоритетов он не всегда обеспечивает предсказуемое время ожидания.

37:34

Speaker A

Я вас обрадую, то есть, в в большинстве задач, над которыми вы будете работать, э-э, в продакшене, скорее всего, это не будет основной проблемой.

37:43

Speaker A

Но, возможно, вам придётся, ну, если вы будете писать какие-нибудь высокопроизводительные приложения, где э-э, важно максимально утилизировать процессорное время и чтобы все задачи получали ну, процессор тогда, когда нужно, э-э, возможно, вот, инверсия приоритетов будет оказывать влияние.

38:08

Speaker A

Вот, в системах реального времени не только важно выбирать правильные блокировки, но и учитывать их взаимодействие с планировщиком.

38:15

Speaker A

[тяжело вздыхает] Это то, что касалось, э-э, про синхронизации внутри одного процесса. Теперь поговорим про межпроцессное взаимодействие и синхронизацию.

38:27

Speaker A

Мы частично с вами уже про обмен данными между процессами проходили. Это были FIFO, сигналы и пайпы. Вот, но разберём ещё некоторые механизмы, которые есть в операционных системах.

38:43

Speaker A

[вздыхает] Смотрите, потоки одного процесса имеют общее адресное пространство, поэтому могут обращаться к одним и тем же объектам.

38:52

Speaker A

И для защиты этих объектов мы используем мьютексы, семафоры, условные переменные. Но когда у нас есть два независимых процесса, каждый один процесс принимает запросы от пользователя, а другой выполняет обработку данных. И мы решили действительно разместить их в разных процессах, потому что так безопасно. Ну,

39:09

Speaker A

например, если у нас по какой-то причине процесс, выполняющий обработку данных, упадёт, чтобы он мог быть перезапущен, не влияя на там процесс, который принимает запросы от пользователя.

39:24

Speaker A

По умолчанию у каждого процесса своё виртуальное адресное пространство. То есть каждый процесс один процесс не может просто взять, получить указатель на данные в памяти другого процесса и начать там что-то изменять или что-то прочитать.

39:40

Speaker A

Как же им взаимодействовать? И в операционных системах есть два основных подхода, это использовать разделяемую память.

39:51

Speaker A

В этом случае операционная система позволяет как бы прорезать дырочку в стенах, в стенах, ограничивающих один память одного процесса от другого, и с какой-то общ- с каким-то общим участком памяти работать совместно.

40:07

Speaker A

Э-э. Процессы могут работать с одними и теми же областями памяти практически так же, как и обычные потоки, но возникает необходимость межпроцессной синхронизации.

40:20

Speaker A

Второй подход - это обмен сообщениями. Вместо непосредственного доступа к общей структуре данных, э, один процесс отправляет другому сообщение через ядро.

40:29

Speaker A

То есть ядро выступает в роли почтальона, который из одного до из домика, в котором живёт один процесс, доставляет сообщение в домик, в котором живёт другой процесс.

40:40

Speaker A

С некоторыми примерами мы уже знакомы. То есть мы рассматривали пайпы и FIFO и сигналы. Теперь посмотрим э, на другие средства Линукса по и поймём, в каких ситуациях лучше использовать общую память, а в каких обмен сообщениями. И сначала поговорим

40:57

Speaker A

про [откашливается] общую память. Кратко расскажу, как устроена виртуальная память. У нас будут ещё этому посвящена, э, посвящены отдельные лекции.

41:09

Speaker A

То есть смотрите, каждый процесс работает с виртуальными адресами. Процессор и операционная система с помощью таблиц страниц преобразовывают эти адреса в физические.

41:19

Speaker A

Физические адреса - это фактически, э, указывают о том, где внутри микросхемы памяти эти адреса располагаются.

41:26

Speaker A

Вот, но при этом адресные пространства процессов изолированы. Это не означает, что каждый процесс обязательно использует совершенно отдельные физические страницы.

41:39

Speaker A

Операционная система может создать так называемое отображение, при которых виртуальные адреса разных процессов отображаются на одну и ту же физическую память.

41:50

Speaker A

Даже если вы ничего специально для этого не делаете, это может, ну, в своей программе вообще вы ничего не знаете про shared memory, уже ваша программа может использовать совместную память с другими процессами.

42:03

Speaker A

Например, и ваш, э, одна программа и другая программа используют одну и ту же, э, системную библиотеку.

42:12

Speaker A

Зачем хранить в физической памяти разные копии физи- и не- несколько копий одних и тех же физи- физических библиотек? Поэтому Ой э-э не- копии одной и той же системной библиотеки. Поэтому системная библиотека в физической памяти загружается в одном единственном экземпляре

42:30

Speaker A

и отображается в адресное пространство разных процессов. Ну, допустим, какая-нибудь библиотека, отвечающая за графический ввод-вывод, может использоваться и веб-браузером, и какой-нибудь там и текстовым редактором.

42:48

Speaker A

И она се- С кодом это вообще всё прекрасно происходит. Почему? Потому что код - это память, доступная только для чтения.

42:57

Speaker A

есть изменить о- область кода просто так нельзя, и можно одну и ту же один и тот же код в адресное пространство разных процессов отобразить.

43:08

Speaker A

Вот. Но кроме того, процессы могут договориться и определённый регион физич- виртуальной памяти отобразить на одну и ту же общую физическую память. То есть, грубо говоря, мы берём и в адресном пространстве ви- виртуальной адресной виртуальном адресном пространстве каждого процесса

43:28

Speaker A

э-э соединяем определённые участки. Это позволяет обоим процессам работать с одной и той же физической памятью.

43:39

Speaker A

При этом они не обязаны одним и тем же адресам располагаться. То есть, вот shared region в процессе А может по адресу, допустим, 10 000, условно говоря, располагаться, а э-э в процессе Б по адресу 20 000. Но физически это одна и та же память.

43:58

Speaker A

Поэтому, если процесс А изменит данные вот в этой области памяти, процесс Б может наблюдать эти изменения через своё отображение.

44:10

Speaker A

Преимущество заключается в том, что не требуется каждый раз передавать данные через системный вызов. После того, как программы настроили общую область памяти, оба процесса могут к ней непосредственно обращаться.

44:23

Speaker A

Это полезно при обмене большими объёмами данных. Например, я могу предположить, что браузер Chromium использует отображаемую память для того, чтобы э-э перекидывать данные из буфера кадра э-э там одного рабочего процесса в буфер кадра, который отображается непосредственно пользователю.

44:55

Speaker A

Ну, то есть буфер кадра это довольно большие большой объём данных, там несколько мегабайт. И э-э допустим мы э-э мы хотим, чтобы веб-страница работающая ну точнее чтобы веб-страница обрабатываемая в одном процессе Chromium, могла нарисовать это что-то туда, в этот буфер

45:20

Speaker A

кадра, а потом браузерный процесс, который всё на экран отображает, мог бы нам эту память отобразить. И вполне Shared Memory для этих целей может быть использована.

45:32

Speaker A

Э-э в Linux э-э для создания общего доступа к памяти часто создаётся с помощью функции mmap, memory map, с флагом MAP\_SHARED.

45:45

Speaker A

Вот, смотрите, Shared Memory обеспечивает доступ к данным, но не безопасность одновременных изменений. То есть, если два процесса одновременно выполнят операцию, допустим, увеличения счётчика, тут, допустим, у нас счётчик хранится в общей физической памяти. Если два процесса одновременно начнут увеличивать счётчик, то получится та же

46:03

Speaker A

самая проблема, которую мы выполняли, ой, изучали при работе с потоками. И если с одной какой-нибудь переменной мы могли ещё какой-то атомарной операции использовать да и это было бы нормально, то для каких-то более сложных данных нам необходим механизм более

46:24

Speaker A

э-э как бы более сложной синхронизации. Вот, поэтому разделяемая память и межпроцессная синхронизация - это два отдельных механизма, которые приходится использовать совместно.

46:37

Speaker A

Как создаётся сначала shared memory? В POSIX-системах есть shared memory API. В процессе А мы вызываем функцию э-э shm\_open и передаём имя объекта, например, OS\_demo.

47:01

Speaker A

Флаги O\_CREAT, O\_EXCLUSIVE и O\_RDWR, точнее, флаги O\_CREAT и O\_EXCLUSIVE означают, что процесс хочет создать новый объект shared памяти и получить ошибку, если объект с таким именем уже существует.

47:14

Speaker A

Флаг RDWR позва- позволяет открыть объект для чтения и записи. То есть вот этому процессу разрешается и писать в эту область памяти, и читать.

47:24

Speaker A

И последний аргумент 0600, как вы думаете, что означает? Право доступа. Да. Права доступа чтения и записи владельцу с учётом правила создания объекта и маски umask.

47:37

Speaker A

То есть э-э другим м-м процессам э-э и другим группам ничего не разрешается. То есть чтобы никто к нам, чтобы другие процессы под этим именем не могли туда залезть.

47:50

Speaker A

Вот, после успешного Что? А вот тут RDWR. А другим, нет, точнее, процессам от запущенных от имени других пользователей.

48:04

Speaker A

Ага, ну, тогда да. Так. Дальше. После успешного вызова мы получили файловый дескриптор. Опана, ещё и здесь файлы, да, вылезли.

48:26

Speaker A

Смотрите, как интересно. Но сам объект пока имеет нулевой размер, поэтому следующим шагом мы вызываем Ftruncate и задаём размер. В нашем случае достаточно памяти для одного целого числа.

48:40

Speaker A

Затем мы вызываем функцию mmap. Мы просим операционную систему отобразить объект вот этот вот файлового дескриптора в адресное пространство нашего процесса с возможностью э, чтения и записи.

49:04

Speaker A

Ключевой флажок здесь, ой, параметр здесь MAP\_SHARED. Именно он позволяет другим процессам отображать тот же самый объект и видеть его содержимое.

49:14

Speaker A

После успешного вызова mmap мы получаем адрес области памяти, то есть void указатель обычный. И через этот адрес мы можем записать э, туда число 42.

49:26

Speaker A

Теперь мы запускаем процесс B. Он уже вы- вызывает э, shm\_open с тем же именем, что и os\_demo, но уже без флага создания. То есть вот тут флаг O\_CREATE ему не передаётся.

49:43

Speaker A

Мы открываем в режиме read-only. Флаги мы сюда не передаём, ну, потому что мы не владельцы.

49:52

Speaker A

И здесь мы получили тоже э, указатель с помощью mmap. Но мы говорим, что вот эта область памяти нам только для чтения доступна.

50:04

Speaker A

И тут мы можем прочитать значение этой переменной. И смотрите, в первом процессе, в процессе А, указатель может быть один, в процессе Б указатель может быть другой.

50:18

Speaker A

То есть виртуальные адреса у них различаются. Но они работают с одним и тем же объектом. Ну это всё равно, что вот в одном процессе, в одном в домике одного процесса вы прорезали э-э, дырку в стене в левой, а в другом процессе вы прорезали дырку в

50:37

Speaker A

стене правой. Но в пространстве они расположены так, что вы видите одну и ту же область, вот, не знаю, там одну и ту же область, там, не знаю, огорода да?

50:51

Speaker A

То есть адреса у вас разные, одному нужно через левую стену в этот огород смотреть, другому через правую область.

50:59

Speaker A

Причём один может только смотреть, а второй может ещё, не знаю, там, в этот огород что-нибудь кидать. Ну, такая вот аналогия.

51:11

Speaker A

Вот в нашем примере нет синхронизации, то есть мы предполагаем, что процесс Б начинает чтение после завершения э-э, записи процессом А.

51:20

Speaker A

Если оба процесса должны одновременно работать с общей памятью, то нам понадобится дополнительный механизм синхронизации.

51:27

Speaker A

Ещё, значит, какие две практические детали. После того, как мы успешно создали э-э, вы выполнили mmap, файловый дискриптор можно закрыть, не уничтожая отображение.

51:47

Speaker A

Э-э, Вот в настоящей программе, конечно же, необходимо проверять результаты всех этих функций, потому что системные вызовы могут завершиться ошибкой. Здесь мы ничего не проверяем.

52:00

Speaker A

Ну и потом mmap надо будет эту вот освободить память. Какие есть функции, да? Вот мы создали общую память, но возникает вопрос, как долго существует этот объект и когда он уничтожается?

52:17

Speaker A

Надо различать три сущности. Первая сущность - это сам объект разделяемой памяти. Второе - это файловые дескрипторы, через которые процессы открывают объект.

52:27

Speaker A

Третье - это отображение, созданное с помощью mmap. Когда процесс вызывает э-э shm\_open, он получает дескриптор существующего или нового объекта.

52:41

Speaker A

После вызова mmap у него появляется отображение в виртуальном в его виртуальном адресном пространстве. После этого файловый дескриптор можно уже закрыть.

52:52

Speaker A

При этом само отображение от этого не исчезнет. Чтобы от- удалить отображение, надо вызвать munmap или завершить процесс.

53:03

Speaker A

Вот, ну и это не обязательно уничтожит сам объект shared памяти, потому что другие процессы могут продолжать его использовать.

53:10

Speaker A

И для удаления имени объекта предназначена функция shm\_unlink, да? Shared memory unlink. После её вызова новые процессы больше не смогут открыть то же тот же объект по старому имени.

53:23

Speaker A

Однако процессы, у которых существуют уже отображения или открытые дескрипторы, могут продолжать работу. Фактически освобождение объекта произойдёт тогда, когда последние ссылки отображения исчезнут.

53:37

Speaker A

Чем-то похоже на shared pointer, да? Ну или похоже на поведение обычных файлов в Линуксе при вызове unlink.

53:45

Speaker A

Мы тоже можем открыть э-э открыть объект, потом сделать unlink, но продолжать в этот файл писать.

53:52

Speaker A

До тех пор, пока не закроем. И, значит, важная особенность, связанная с у с указателями.

54:00

Speaker A

Если может быть, вы сами догадаетесь, почему обычный абсолютный указатель не подходит для передачи данных между процессами?

54:10

Speaker A

Потому что адрес э-э в виртуальном адресном пространстве одного процесса может не совпадать с виртуальным адресом в другом процессе.

54:20

Speaker A

Если вы в этой в области памяти начни начнёте указатели обычные использовать, то есть в shared памяти записали адрес какой-нибудь переменной, а другой процесс захотел по э-э поэтому указать использовать этот указатель, чтобы прочитать значение переменной, но он там полезет в своём

54:42

Speaker A

адресном пространстве искать, и ничего не получится. В этом случае что вы должны использовать вместо указателей?

54:50

Speaker A

Например, можно использовать просто смещение относительно начала выделяемой памяти. То есть, смотрите, вот у нас два процесса. У одна один процесс там у одного процесса вот в этой области памяти в ше- shared область памяти видна, так?

55:15

Speaker A

А есть другой процесс, у него по более низкому адресу смещена э-э располагаются данные. И мы хотим вот в этой shared памяти поместить а- адрес какого-то объекта, находящегося где-то тут.

55:34

Speaker A

Вот процесс э-э [вздыхает] вот этот вот процесс не может поместить в shared указатель, но он может поместить вот сюда смещение относительно начала этой области памяти. И тогда, то есть мы фактически используем относительное смещение, а не абсолютные адреса понятно?

55:58

Speaker A

Так, что ещё есть? Э-э, в Линуксе есть ещё функция memfd\_create. Она позволяет создать анонимный объект памяти, представленный файловым дескриптором.

56:10

Speaker A

Этот дескриптор можно передать другому процессу, например, через Unix domain socket, про него тоже поговорим, после чего второй процесс может отобразить те же самые данные.

56:22

Speaker A

Но разбирать подробно мы этот механизм не будем. Теперь вернёмся к проблеме синхронизации. Мы создали shared memory, в которой находится у нас общий счётчик, и два процесса одновременно хотят выполнять инкремент этого счётчика.

56:44

Speaker A

Э-э. Эта операция не обязана выполняться как одна атомарная операция, и мы можем словить состояние гонки, только уже между процессами.

56:55

Speaker A

Э-э. Мы уже знакомы с мьютексами, но теперь мьютекс должен работать не между потоками одного процесса, а между независимыми процессами.

57:03

Speaker A

И для этого в POSIX предоставляется специальный атрибут PTHREAD\_PROCESS\_SHARED. Посмотрим на структуру shared data. В ней хранится у нас мьютекс, позволяющий синхронизировать данные, и счётчик.

57:20

Speaker A

Важно, что мьютекс тоже должен находиться в разделяемой памяти. Недостаточно создать по одному мьютексу в каждом процессе с одинаковым именем переменной. Наверное, понимаете почему, потому что с точки зрения ядра это будут разные объекты.

57:35

Speaker A

И не смогут синхронизировать доступ к одному ресурсу. Поэтому мы размещаем всю структуру э-э, внутри shared data.

57:44

Speaker A

При инициализации мьютекса мы создаём объект атрибутов и задаём атрибут pthread\_process\_shared. После этого мы инициализируем мьютекс.

58:03

Speaker A

Объект атрибутов можно уже удалить. И теперь смотрите, да, мы проинициализировали ещё наш счётчик. И теперь, когда каждый процесс хочет выполнить инкремент счётчика, он просто pthread\_mutex\_lock захватывает этот мьютекс, инкрементит счётчик pthread\_mutex\_unlock э-э, снимает блокировку с мьютекса.

58:25

Speaker A

Сейчас секундочку. Алло. Ал- алло. Олег, привет. Андрей это. Э-э, я пока занят, у меня лекция. Давай потом перезвоню.

58:48

Speaker A

Вот, в результате вот всего этого два процесса не могут одновременно войти вот в защищённую критическую секцию.

58:58

Speaker A

И при этом будут использовать разные виртуальные адреса. Но поскольку эти разные виртуальные адреса отображаются на одну и ту же физическую память, то механизм синхронизации будет работать с одним и тем же общим объектом.

59:13

Speaker A

Значит, по умолчанию POSIX мьютексы имеют режим pthread\_process\_private. И поэтому простого размещения мьютекса в shared memory недостаточно, необходимо явно задать для него нужный атрибут.

59:28

Speaker A

[фыркает] Можно Давайте ещё вопрос. А Можно ещё раз повторить, почему мы именно структуры используем, как бы, соединяем мьютекс вместе с каунтом?

59:42

Speaker A

Почему мы сделали, э, используем структуру, а не просто две отдельные переменные? Да. Ну, во-первых, так удобнее.

59:53

Speaker A

М-м. То есть, мы в одной и той же shared memory разм- Смотрите, с помощью shared memory как мы выделяем память? С помощью ftruncate мы берём и выделяем э, размер shared памяти между ними, так? И потом делаем mmap. Так вот, мы должны,

60:16

Speaker A

здесь, вот в, ну, в этом примере, выделить с помощью ftruncate объём памяти, чтобы там и мьютекс лежал, и вот этот счётчик.

60:27

Speaker A

Угу. Угу. Ну, мы должны одно, од- одно окошко прорезать, чтобы два объекта могли там работать.

60:34

Speaker A

Если вы э, с помощью разных необязательно их помещать в одну и ту же структуру. Важно, чтобы вы в одном memory мэпе их разместили.

60:47

Speaker A

Если вы в разных memory мэпах разместите, ну, может быть, у вас что-то получится, но вот, то есть, мьютекс должен, во-первых, он должен жить в shared памяти, э, во-вторых, он должен быть создан вот с этим sprocess create.

61:00

Speaker A

М-м, я не знаю, получится ли у вас сделать такое, что вы, э, мьютекс в одной shared памяти разместите, а счётчик в другой shared памяти. Будет ли это корректно работать?

61:12

Speaker A

Не знаю, попробуйте. Мне кажется, это как-то, даже если это будет работать, то код будет так, очень подозрительным, а лучше не писать подозрительный код.

61:22

Speaker A

Когда вы лезете в многопоточность или многопроцессность, у вас и так сюрпризов хватает. Зачем себя ещё сильнее удивлять?

61:32

Speaker A

Давайте дальше. [кашель] Итак, мы научились использовать мьютексы между процессами, и мьютекс решает задачу взаимного исключения. А что, если процессу нужно дождаться определённого события?

61:46

Speaker A

Например, один процесс готовит данные, а другой должен приступить к обработке только после того, как они будут готовы.

61:52

Speaker A

И мы можем использовать для этого семафоры. Семафоры у нас уже знакомы по задаче продюсер-консьюмер.

62:00

Speaker A

Но теперь посмотрим, как применять семафоры между процессами. В POSIX есть два варианта семафоров. Первый - это именованные семафоры.

62:09

Speaker A

Процесс вызывает функцию sem\_open, получает доступ к семафору по его имени. Другой независимый процесс может открыть тот же самый объект, используя такое же имя.

62:18

Speaker A

Второй вариант - это неименованные семафоры. Для него используется функция sem\_init. Тут мы исп- Тут мы используем функцию sem\_init.

62:41

Speaker A

Первый Первый аргумент говорит нам, где, ну, адрес, где должен размещаться семафор. Второй аргумент называется pshared. Если он равен нулю, то семафор предназначен для синхронизации потоков одного процесса. Если же он не нулевой, то семафор может использоваться между процессами при условии, что сам объект

63:06

Speaker A

находится в shared памяти. Вот, мы здесь создали структуру shared\_data, содержащую целое число и семафор.

63:15

Speaker A

Процесс под, э, потребитель э, ожидает, пока перемен, э ожидает э, появления данных, а процесс, э продюсер э, как бы постит данные в этом семафоре.

63:38

Speaker A

Соответственно, потребитель получает разрешение, просыпается и читает подготовленное значение. Ой, куда это у меня всё улетело?

63:51

Speaker A

Вот. В отличие от примера, [откашливается] то есть, смотрите, что здесь делает семафор. Семафор обеспечивает нам нужную синхронизацию памяти между публикацией значения и его чтением. То есть, смотрите, когда мы сделали семпост, а второй с помощью семвейта дождался, вот этот, э,

64:24

Speaker A

второй процесс может быть уверен, что он, э, он увидит значение 42 в шаред-памяти. [откашливается] Смотрите, если производитель вызовет семпост до того, как потребитель выполнит семвейт, разрешение сохранится и уведомление не потеряется.

64:53

Speaker A

То есть, не обязательно вот этот, э, семвейт должен заблокироваться и только после этого этот должен сделать семпост.

65:00

Speaker A

Нет э продюсер может с помощью семпоста опубликовать данные, и когда консьюмер вызовет семвейт, он уже увидит, что данные готовы и, э, сможет данные читать.

65:14

Speaker A

Конечно же, в настоящем приложении нужно проверять результаты системных вызовов, потому что semwait может быть прерван сигналом и завершится ошибкой EINTER, которую нужно корректно обрабатывать.

65:28

Speaker A

Здесь на слайдах примеры, которые для простоты чтоб на экран поместиться. Мы предполагали, что процессы выполняются нормально и обязательно освобождают все захваченные ресурсы.

65:43

Speaker A

Но в реальности процессы могут аварийно завершиться. И не обязательно из-за того, что в них содержатся ошибки.

65:57

Speaker A

Представим, что два процесса используют одну одну общую память. То есть первый процесс захватывает мьютекс и начинает изменять общую структуру данных.

66:08

Speaker A

Например, обновляет два взаимосвязанных поля, которые должны всегда удовлетворять некоему инварианту. После изменения первого поля процесс аварийно завершается.

66:19

Speaker A

Ну, не знаю, в каком-нибудь там параллельном потоке произошла там, не знаю, ошибка, процесс не обработал там полученный указатель, допустим, нулевой указатель разыменовал, весь процесс грохнулся. А что со вторым процессом произойдёт?

66:35

Speaker A

Если используется обычный мьютекс без механизма восстановления, то второй процесс может остаться заблокированным, ожидая освобождения мьютекса.

66:45

Speaker A

Для решения этой проблемы в POSIX предоставляется robust мьютекс. Robust как переводится? Надёжный мьютекс. То есть по умолчанию мьютексы ненадёжные, ну, [фыркает] потому что это дешевле.

67:00

Speaker A

Зачем платить за то, что вы не используете? Если вы создаёте мьютекс с атрибутом PTHREAD\_MUTEX\_ROBUST, то тогда, если под владелец завершится, не освободив блокировку, то следующий вызов mutex lock может вернуть специальное значение EOWNERDEAD, то есть ошибка владелец умер.

67:27

Speaker A

Это не означает, что захват не состоялся. Наоборот, новый процесс теперь владеет мьютексом, то есть если мы захватили этот мьютекс, но получили значение ownerdead, операционная система нас предупреждает, что предыдущий владелец завершился во время работы критической секции, то есть пока мьютекс был захвачен, он

67:45

Speaker A

завершился. Следовательно, защищаемые этим мьютексом данные могут находиться в некорректном состоянии. И теперь уже ваша задача э, либо восстановить согласованность данных, либо увидеть, что всё плохо и выдать какую-нибудь ошибку.

68:03

Speaker A

Может быть, тоже самим завершить работу, но не пытаться использовать некорректно проинициализированные данные. Например, вы можете перепроверить структуру, откатить незавершённую операцию или восстановить значение из, э, там какого-нибудь журнала, если там какое-нибудь журналирование вы использовали. Это уже зависит от конкретного приложения.

68:26

Speaker A

После этого вы должны вызвать pthread mutex consistent. Тем самым процесс, который стал владельцем, подтверждает, что данные снова находятся в корректном состоянии.

68:40

Speaker A

Ну, потому что могут же быть другие процессы, которые тоже могут этот мьютекс использовать. Но если процесс не может восстановить состояние и освобождает мьютекс без вызова pthread mutex consistent, то блокировка перейдёт в невосстановимое состояние.

68:54

Speaker A

И последующие ошибки будут вам возвращать result, э, результат э ENOTRECOVERABLE. Енот. Не- не невосстановимый енот.

69:10

Speaker A

Ну ладно э-э ладно. [вздыхает][тяжело вздыхает] Короче, м-м, ошибка мьютекс невосстановим. То есть главная э-э главная фишка Robust мьютекса, вы можете обнаружить аварийное завершение процесса, который этим мьютексом владел, но не исправляет повреждённые данные автоматически. То есть приложение с- должно самостоятельно

69:29

Speaker A

определить э-э такую ситуацию и восстановить свои инварианты. Есть вопрос. Вопрос. А что ну А как мы можем гарантировать, э-э, а что наш процесс не завершится, что что его не кильнуло, например?

69:45

Speaker A

Как ваш процесс может гарантировать? Никак. Что енот не возникнет у нас. Вы никак не можете гарантировать, что у вас будет енот. Вот ваш процесс тоже мо- могут тут до вызова pthread\_mutex\_con- con- consistent, вы тоже можете помереть.

70:01

Speaker A

И тогда всё, э-э, вашему еноту пришла смерть. И надо завершиться. То есть, знаете, когда [фыркает] вы пишете надёжные приложения, это не те, которые не падают, а те, которые, у- упав, восстанавливаются. И тут тоже картинка с Джейсоном Стэйтемом, да?

70:23

Speaker A

А вы думали, в сказку попали, что ли? Это вам не веб-странички клепать. Давайте дальше.

70:32

Speaker A

Поговорим ещё про э-э механизмы взаимодействия между процессами, э-э и Unix domain sockets. Это обмен сообщениями.

70:45

Speaker A

В лекции о процессах мы уже рассматривали пайпы и FIFO, но в Линуксе есть ещё э-э один механизм э-э локального взаимодействия.

70:53

Speaker A

Локального это значит в рамках одного компьютера. Unix domain sockets. Сокеты часто ассоциируются с сетевым программированием TCP UDP IP-адресами и тому подобное. Однако, сокетный интерфейс можно использовать и для связи процессов на одном компьютере.

71:10

Speaker A

Э, для этого предназначено э, семейство сокетов AF\_UNIX, также называемое AF\_LOCAL. При работе с такими сокетами нет необходимости обращаться к удалённому компьютеру. Оба процесса располагаются операционной системы.

71:30

Speaker A

У доменных сокетов есть э, несколько режимов. SOCK\_STREAM э, это режим, когда мы обеспечиваем точнее, при обмен происходит непрерывный поток байтов, похожий по семантике на TCP-соединение.

71:48

Speaker A

Второе это э, SOCK\_DGRAM, datagram, фактически. В этом режиме данные передаются отдельными сообщениями, и при этом границы сообщений сохраняются.

72:00

Speaker A

И есть ещё третий режим, это м, sequential packet, он сочетает установку соединения с сохранением границ сообщений и порядка доставки.

72:11

Speaker A

Э, это удобно, когда приложение работает с отдельными запросами или командами. Преимущество Unix-domain сокетов это возможность организовать локальный клиент-серверный интерфейс.

72:24

Speaker A

Например, вот фоновая служба может создать сокет с определённым именем файла в системе, а другие процессы могут подключаться к ней, отправлять команды.

72:32

Speaker A

Ну, примерно то же самое, что и вы делали через FIFO, только теперь не через FIFO.

72:38

Speaker A

Э, с помощью э, доменных сокетов можно передавать файловые дескрипторы. есть, один процесс может открыть файл или создать объект через э-э memfd\_create затем передать доступ к нему другому процессу через Unix domain socket.

73:03

Speaker A

Таким образом, доменные сокеты могут служить не только каналом передачи данных, но и способом безопасно передавать доступ к ресурсам ядра между процессами.

73:19

Speaker A

Давайте рассмотрим э-э сценарии организации связи через Unix domain socket. Это взаимодействие родственных процессов, то есть когда родитель и потомок.

73:30

Speaker A

Один процесс создаёт дочерний с помощью форка. И э-э, мы чтобы создать вот доменные сокеты, создаём их через socketpair.

73:44

Speaker A

Она создаёт сразу два связанных сокета. После вызова fork оба процесса наследуют файловые дескрипторы этой пары.

73:53

Speaker A

Родитель, где вот родитель? Вот. Родитель оставляет один конец, ну, то есть родитель закрывает второй конец а ребёнок закрывает первый.

74:05

Speaker A

И теперь они могут обмениваться данными в обоих направлениях. Дальше в случае мы создали socketpair с флагом SOCK\_SEQPACKET. Что это значит?

74:21

Speaker A

Это позволяет нам работать не с непрерывным пакетом байтов, а с отдельными сообщениями, то есть сохраняющим порядок.

74:29

Speaker A

Э-э, родитель отправляет строчку Hello. Вот он. То есть, э-э, через fd0 он отправляет пять байтиков строчку Hello.

74:41

Speaker A

Дочерний прини- паке- процесс принимает э-э, сообщение и отправляет ответ о'кей. То есть через каждое сокеты это двунаправленные данные, мы можем в один тот же сокет записывать данные и читать.

75:01

Speaker A

[кашель] Теперь родитель, смотрите, родитель он принимает строчку Hello и закрывает тоже вторую э-э родитель закрывает, соответственно, тот сокет, по которому он отправлял данные и с помощью waitpid дожидается завершения работы клиента, э-э завершения работы дочернего процесса.

75:31

Speaker A

А дочерний процесс, после того как о'кей отправил, тоже закрывает и второй файловый дескриптор и вызывает exit.

75:39

Speaker A

Ну, в результате они обменялись данными. Здесь у нас нет необходимости создавать объект файловой системе, придумывать ему какое-то уникальное имя, канал связи существует через уже созданные файловые дескрипторы, вот через этот сокет socketpair.

76:00

Speaker A

Но такой способ удобен прежде всего для родственных процессов, либо для ситуации, когда файловые дескрипторы можно передать другим способом.

76:08

Speaker A

А что делать, если у нас два независимых процесса, которые были запущены отдельно? И тогда мы можем использовать именованный доменный сокет.

76:25

Speaker A

А по тут уже сложнее идёт взаимодействие, сервер должен создать сокет, с- сбайндить его потом начать слушать и э-э с помощью акцепта дождаться подключения клиентов. А клиент должен создать сокет именованный и [вздыхает] э-э установить соединение.

76:49

Speaker A

Вот пример. Ну, если успею, я вам напишу до завтра. Если не успею, значит, э-э разберётесь сами.

77:01

Speaker A

Дальше. Э-э доменные сокеты поддерживают разные режимы передачи, то есть поток байтов, сообщения или сообщения.

77:13

Speaker A

Вот, э-э в случае с потоком байтов есть какая особенность? Если отправитель дважды вызвал send, то есть первым вызовом отправляет строчку "Hello", вторым вызовом отправляет строчку "World".

77:26

Speaker A

Если мы используем режим sock\_stream, то получатель видит непрерывную последовательность байтов "Hello World". Это означает, что один вызов receive не обязан вернуть ровно столько данных, сколько было передано одним вызовом send.

77:40

Speaker A

Так? То есть п- первый вызов у получателя может вернуть "Hel" второй "lo Wo", а соответственно, "rld". То есть тут количество сендов не обязательно равно количеству успешных ресивов.

77:57

Speaker A

Или же один вызов может сразу вернуть все 10 байтов. Оба сценария допустимы, поэтому при использовании потокового интерфейса приложение должно самостоятельно определять границы сообщений.

78:08

Speaker A

И с такой проблемой вы, наверное, сталкивались, когда работали с FIFO. Одно из решений перед каждым сообщением отправлять его размер.

78:17

Speaker A

Другой вариант использовать какой-нибудь специальный символ-разделитель. А можно выбрать э-э механизм механизм межпроцессного взаимодействия, который уже сохраняет границы сообщений.

78:27

Speaker A

Например, Unix domain socket, э-э используя режим sock\_dgram или sequential э-э packet или POSIX MQ ещё есть.

78:37

Speaker A

В этом случае система будет работать с отдельными сообщениями, правда, приложение всё равно должно предоставлять достаточно большой буфер для приёма сообщений и учитывать ограничения на его размер.

78:51

Speaker A

Дальше смотрите возможен блокирующий обмен, да, то есть, если получатель вызывает receive, а данных пока нет, поток переходит в состояние ожидания.

79:03

Speaker A

Если отправитель пытается записать данные, но буфер отправки переполнен, он тоже может заблокироваться. Что это нам даёт?

79:11

Speaker A

Это даёт нам естественный механизм ограничения скорости производителя при медленном потребителе, то есть поток производитель, тот тот, кто шлёт данные, будет приостанавливаться.

79:22

Speaker A

Такое такую штуку называют back pressure, то есть как обратное давление. Э, можно работать в неблокирующем режиме, тогда вызов не будет ожидать, а при невозможности вам вернётся результат EAGAIN, ошибка.

79:41

Speaker A

И что ещё означает? Если вы вызвали send, это не означает, что данные были доставлены потребителю.

79:49

Speaker A

Это просто гарантируется, что операция была принята. Ну, фактически, то, что вы бросили письмо в почтовый ящик, не означает, что это письмо будет прочитано.

80:02

Speaker A

Прочитано оно будет ещё тогда, когда получатель его извлечёт. Если вам нужно подтвердить обработку, то должно быть предусмотрено протоколом, то есть вы должны какое-то ответное сообщение посылать.

80:16

Speaker A

Давайте ещё один механизм рассмо рассмотрим, это POSIX message queues. Э, это то тоже он реализует механизм обмена сообщениями, даже это из имени звучит.

80:35

Speaker A

В отличие от общей памяти, программист не создаёт собственный общий буфер. Вместо этого, он обращается к именованной очереди, которую обслуживает ядро операционной системы.

80:46

Speaker A

Для этого используется функция mq\_open. А. Как и в случае с shared memory open, объект имеет имя, начинающееся с косой черты например os\_tasks.

81:00

Speaker A

Один процесс создаёт очередь, а остальные могут открыть её по тому же имени при наличии соответствующих прав доступа.

81:10

Speaker A

А mq\_max\_msg этот параметр задаёт максимальное количество сообщений, которые могут находиться в очереди одновременно, в данном случае 10.

81:19

Speaker A

А mq\_msgsize это максимальный размер одного сообщения. То есть, у нас очередь может содержать до 10 сообщений размером не больше 128 байт.

81:30

Speaker A

А, эти ограничения помогают нам контролировать использование ресурсов. Если производитель попытается отправить сообщение, а очередь заполнена, то mq\_send, функция, она заблокируется.

81:44

Speaker A

Ну, вот как обратное давление реализуется. Если потребитель вызывает mq\_receive, а очередь пуста, он тоже заблокируется.

81:54

Speaker A

Таким образом, вот этот механизм сам обеспечивает ожидание доступности сообщений или свободного места. Ещё есть в POSIX message queues поддержка приоритетов, то есть, при отправке сообщения можно указать его приоритет, и получатель будет сперва получать сообщения с наибольшим приоритетом, а среди сообщений при

82:17

Speaker A

а среди сообщений с одинаковым приоритетом, будут извлекаться, м, ну, будет сохраняться порядок поступления. Таким образом, мы можем более важные задачи обрабатывать раньше остальных.

82:34

Speaker A

И в отличие от FIFO, POSIX Message Queue сохраняет границы сообщений. Поэтому потребителю не требуется самостоятельно восстанавливать их из потока байтов.

82:42

Speaker A

Но при получении сообщения буфер должен быть достаточно большим, то есть не меньше, чем тот вот MSG size, который был установлен.

82:54

Speaker A

Ну и правильная очистка ресурсов важна. [вздыхает] MQ\_Close закрывает дескриптор очереди в конкретном процессе, а MQ\_Unlink удаляет её имя.

83:06

Speaker A

Сам объект при этом продолжает существовать, пока остаются открытые ссылки. То есть объект очереди даже после MQ\_Unlink удаляет имя.

83:17

Speaker A

Так? Но объект удалится, когда не останется ни имён, ни этих ни ссылок на него в других процессах.

83:29

Speaker A

[фыркает] Ну давайте рассмотрим, как продюсер-консьюмер, то есть поставщик-потребитель, можно решить с помощью POSIX Message Queues.

83:42

Speaker A

Раньше мы решали её с помощью ограниченной очереди, которая находится в памяти одного процесса. Для защиты очереди мы использовали мьютексы, для синхронизации условные переменные или семафоры.

83:55

Speaker A

Теперь мы увидели, как можно организовать общую память, использовать межпроцессные механизмы синхронизации. Ну а теперь попробуем э-э другим способом решить. Вместо создания общей памяти воспользуемся POSIX POSIX Message Queues.

84:10

Speaker A

Ну то есть очевидно, что если вы хотите обмениваться сообщениями с каким-нибудь другом на другом конце города, вам не обязательно делать телепорт между вашими э-э домами. Вы можете воспользоваться обычной почтой, да?

84:23

Speaker A

[тяжело вздыхает] Пусть процесс производитель создаёт задание для обработки. Каждое задание представлено отдельным сообщением. Для отправки потребитель открывает очередь, вызывает MQ send, чтобы послать туда э-э э-э сообщение.

84:48

Speaker A

Например, он послал сообщение там job 42. Если очередь не заполнена, то сообщение помещается э-э в неё, а если же очередь заполнена, что произойдёт?

85:01

Speaker A

Да, про- процесс заблокируется по умолчанию. Но вы можете флажок передать, [кашель] чтобы не блокироваться.

85:09

Speaker A

Процесс потребитель открывает ту же очередь для чтения и с помощью MQ receive ожидает. N нам вернёт количество сообщений, по-моему. Или количество байтиков, посмотрите.

85:32

Speaker A

По-моему э-э количество байтиков должен вернуть. Что возвращает MQ receive? Количество байт. Ну, логично, тут мы передали э-э размер сообщения, так?

85:53

Speaker A

То есть размер отдельного сообщения может быть меньше, чем размер максимального. И дальше мы это сообщение обрабатываем.

86:02

Speaker A

Таким образом, нам не нужно самостоятельно управлять какими-нибудь индексами кольцевого буфера, счётчиком элементов, мьютексом, которые э-э тогда использовали. Мы берём с помощью POSIX message queues э-э два независимых процессора работают с одной очередью.

86:23

Speaker A

Есть ограничения, какие? Передача сообщения предполагает копирование данных в очередь ядра, а затем буфер получателя.

86:31

Speaker A

То есть это медленнее немножко работает. Кроме того, размеры сообщений и их количество ограничено. Поэтому POSIX Message Q's хорошо подходит для событий, команд и сравнительно больших заданий.

86:45

Speaker A

Если же вам надо из обмениваться большими областями, большими массивами данных, ну, не знаю, буферами, буфер кадра, например, туда-сюда перекидывать, то Shared Memory, Shared Memory будет более эффективно.

87:03

Speaker A

Ну, и также смотрите, э, так. MQ Receive возвращает реальное количество полученных байтиков. Э, и полученные данные не обязательно являются сишной строкой. Вот, например, здесь у нас sizeof task, оно совпадает, с размером строки плюс завершающий нулевой символ.

87:30

Speaker A

Вот но мы могли бы отправить сообщение и без вот завершающего нулевого символа, то есть не sizeof, а strlen здесь использовать.

87:39

Speaker A

И тогда нам вот здесь в Process Message надо вот учитывать количество байт, и если не завершается нулевым символом, э, ну, за пределами той за предел этой строки не читать. То есть надо внимательно, то есть если вы текстовые данные туда-сюда гоняете,

87:58

Speaker A

внимательно следите за концом строки, иначе если выйдете за пределы массива, вот у вас э, приведёт в лучшем случае к падению вашего приложения, а в худшем к какой-нибудь э, ошибке, точнее, не ошибки, а уязвимости У, как это, вы- выполнение произвольного

88:19

Speaker A

кода внутри вашего процесса. есть, если ваша программа может э-э, принимать данные откуда-то из внешнего недоверенного источника и записывать их в память, и вы не проверяете, сколько байтов вы куда записываете или как вы интерпретируете эту память, вы можете столкнуться с тем, что перезапишете

88:37

Speaker A

какие-нибудь важные для вашего приложения участки памяти, и после этого м-м там у вас выполнится не тот код, который вы предус- ну, который вы планировали выполнить.

88:51

Speaker A

И ваше приложение м-м, может начать творить дичь под именем того пользователя, от которого вы запущены.

89:01

Speaker A

[откашливается] И давайте рассмотрим ещё межпроцессную синхронизацию через файлы. Это так называемые файловые блокировки. Представим себе, что у нас есть два экземпляра программы, пытаются одновременно изменить один и тот же файл конфигурации.

89:17

Speaker A

Если оба процесса сначала прочитают старое содержимое, затем независимо внесут изменения и запишут результат, то часть обновлений может потеряться.

89:26

Speaker A

То есть, эта ситуация похожа на состояние гонки при доступе в общий к общей памяти, но только мы тут файл перезаписываем. И вновь вспоминаем мультфильм Двое из Простоквашино, когда на Трое из Простоквашино, когда э-э, дядя Фёдор, кот Матроскин и Шарик

89:41

Speaker A

начинают одно и то же письмо родителям писать. Только в нашем случае общий ресурс это не область памяти, а файл.

89:51

Speaker A

В Линуксе есть механизмы, позволяющие процессам договориться об исключительном доступе к файлу. Один из самых простых это F\_LOCK, file lock.

90:01

Speaker A

Для него есть два режима блокировки. Первый это э-э, LOCK\_SH, это shared lock, несколько процессов могут одновременно удерживать совместную блокировку. Второй это exclusive lock. Это исключительная блокировка, такая блокировка несовместима с другими блокировками этого файла.

90:18

Speaker A

Если ресурс уже заблокирован другим процессом, то вызов и flock по умолчанию ожидает освобождения блокировки.

90:24

Speaker A

Вы можете использовать неблокирующий режим, передав флаг lock nb. Вот в нашем примере сначала открывается специальный lock файл tmpapp.log.

90:41

Speaker A

Давайте расколдуем эту строчку. O\_CREATE мы создаём эту блокировку. Разрешаем говорим, что мы будем в этот файл и читать, и писать. И устанавливаем доступ только для чтения и записи текущему пользователю.

90:56

Speaker A

общем-то логично да? Мы получили файловый дескриптор, но теперь мы хотим этот файловый получить блокировку. И с помощью flock мы получаем эксклюзивный доступ к этому файлу.

91:10

Speaker A

Если кто-то другой у нас в этот момент с этим файлом работает, мы заблокируемся, пока он не разблокирует нас.

91:17

Speaker A

Если мы зашли сюда, это значит, что мы получили эксклюзивный доступ [откашливается] к этому файлу и можем выполнить запись в него, ну, допустим, используя этот файловый дескриптор.

91:31

Speaker A

А затем мы снимаем блокировку и дальше закрываем файловый дескриптор. Файловые блокировки в Linux обычно имеют рекомендательный характер.

91:46

Speaker A

Это означает, что ядро не запрещает произвольной программе открыть и изменить файл только потому, что программа вызвала lock flock.

91:56

Speaker A

Чтобы механизм был полезен, все участники должны договориться использовать блокировки. Ещё одна особенность в Линуксе блокировки F лог связана с открытым файловым описанием.

92:12

Speaker A

Э-э, точнее с открытым файлом этим дескриптором. Дескрипторы, полученные через функцию дуб или унаследованные при форке, могут ссылаться на ту же самую блокировку, и она освобождается явно через лог а-а через лог UN.

92:28

Speaker A

Либо после закрытия последующей э-э по- последней ссылки. Есть ещё вот функция F cntl. С её помощью можно, допустим, заблокировать не весь файл целиком, а какой-то диа- диапазон байтов.

92:47

Speaker A

Но это сложнее, пока вот F лог. И где, например, применяется F лог? М-м. Вы когда-нибудь гитом пользовались и не сталкивались с тем, что там лог файл какой-то там оказывается занятым при работе с гитом?

93:04

Speaker A

Лог файл? Лог. Ну, а у у Блюфиш такая ситуация была. Ну, или у какой-то другой. То есть, в Линуксе часто используются файловые блокировки, например, для того, чтобы м-м если вы несколько процессов гит запустили внутри одного репозитория, важно, чтобы они не запороли вам

93:29

Speaker A

ваш репозиторий, правильно? И поэтому, когда какой-то гит ну, гит про- гит процесс пытается там модифицировать файлы репозитория, он захватывает файлы в блокировку. И если кто-то другой параллельно пытается сделать, это не даёт одновременно получить доступ э-э к файлу и начать вот как раз всё

93:52

Speaker A

портить. Если бы в гите не было этих такого механизма синхронизации, то репозиторий ваш превратился бы в тыкву и никто бы гитом не пользовался.

94:08

Speaker A

Так, ну и давайте посмотрим, что использовать: shared memory или обмен сообщениями? И правильный ответ: смотря, что вам надо.

94:19

Speaker A

[кашель] Сейчас секундочку. То есть, если у вас данные находятся Если вам нужны большие массивы данных, то используйте общую память. Если в рамках одного процесса вы хотите клиент-серверное взаимодействие, то, возможно, Unix domain socket будет вам э-э, одним из наиболее удобных

94:51

Speaker A

э-э, механизмов обмена. Если у вас есть какие-то независимые задания и события, то используйте message POSIX message queue.

94:59

Speaker A

А если вам нужно просто согласовать доступ к файлу, то используйте блокировку файлов. Э-э, shared memory располагается в общей памяти процессов.

95:10

Speaker A

М-м м-м м-м то есть, данные в общей памяти процессов находятся. И это обеспечивает быстрый доступ э-э, к большим объёмам данных.

95:20

Speaker A

В случае с обменом сообщений, они передаются через межпроцессное взаимодействие, данные через ядро копируются. Э-э, синхронизация в случае с общей памятью организуется приложением.

95:34

Speaker A

А-а, при обмене сообщениям это ядро на на себя берёт часть ответственности. Производительность message passing для команд и со- событий, то есть, какие-то небольшие блоки данных передаются.

95:51

Speaker A

Сложность в случае shared memory, мне кажется, более высокая, то есть вам надо блокировки межпроцессные создавать, управлять общей памятью, управлять временем жизни.

96:05

Speaker A

А в случае с message passing, вы должны задать определённый протокол э для э, обмена сообщениями. То есть, используйте те механизмы, которые больше всего подходят.

96:20

Speaker A

И это зависит от той задачи, которую вы решаете. Вот, на этом как бы всё, что я хотел вам рассказать про м, межпроцессную синхронизацию. Теперь в вашем арсенале э, появились ещё новые инструменты, и в лабораторной работе номер четыре вы с ними тоже успеете

96:41

Speaker A

поработать. Вот, на этом у меня лекция закончена. Остались ли вопросы? Ну ладно, во, вопросов нету, что-то новое вы узнали для себя.

96:58

Speaker A

Теперь вы знаете, как можно процессам друг с другом общаться, лабораторную работу сможете сделать. Вот, на этом лекция закончена.

97:08

Speaker A

Всем пока. Ещё.

Topics: синхронизация процессов std::barrier C++20 барьер синхронизации дедлок shared memory доменные сокеты межпроцессное взаимодействие инверсия приоритетов многопоточность

Учить по этому видео

- [Карточки по этому видео](https://sozai.app/ru/tools/ai-flashcard-generator/?from=transcript&id=82903)
- [Тест по этому видео](https://sozai.app/ru/tools/quiz-generator/?from=transcript&id=82903)

Бесплатно, составлено ИИ по этой расшифровке. Без регистрации.


---
This is the markdown twin of https://sozai.app/transcript/inter-process-synchronization-raw-version/ — the same content, without the markup.
Published by SozAI (https://sozai.app). Reuse and quotation are allowed with attribution and a link back.
Machine-readable index: https://sozai.app/llms.txt · data API: https://sozai.app/api/
