# Пояснения к презентации лекции 6 (текст преподавателя)

**Тема:** Методы обеспечения многозадачности в ОС. Процессы, потоки, волокна. Межпроцессное взаимодействие. Синхронизация

**Файл презентации:** `6.os.process.ru.pdf`. Номера слайдов совпадают с номером в правом нижнем углу слайда. Титульный, разделительные (тёмно-синие) и заключительный слайды не нумеруются.

**Для каждого слайда:**

* **Что сказать** — объяснение для студентов простым языком;
* **Пример из жизни** — аналогия, облегчающая понимание;
* **Вопрос аудитории** — для активизации студентов (ответ в скобках);
* **Демонстрация** — команда, которую можно показать вживую в терминале;
* **Время** — ориентировочная продолжительность.

Лекция рассчитана примерно на **85–90 минут**. Если времени мало, слайды со знаком ★ можно пройти кратко. Для живых демонстраций заранее подготовьте виртуальную машину Linux и скомпилируйте `race.c`, `pipe.c`, `pc.c`, `sig.c`.

---

## Титульный слайд — «Методы обеспечения многозадачности в ОС»

**Что сказать:**
Прямо сейчас на вашем ноутбуке одновременно работают браузер, Telegram, музыка и десятки системных служб. А ядер у процессора всего 4–8. Как это возможно? Сегодня разберём, как ОС «одновременно» выполняет много работы, как программы общаются друг с другом и что делается, чтобы они друг другу не мешали.

**Пример из жизни:** Один повар одновременно готовит суп, плов и салат: пока варится суп, режет морковь; пока плов томится, украшает салат. Повар — процессор, блюда — процессы.

**Время:** 1 минута.

---

## Слайд 1. План лекции

**Что сказать:**
Лекция состоит из четырёх больших частей:

1. **Процессы и планирование** (1–6): что такое процесс, как он рождается и умирает, как ОС делит процессорное время.
2. **Потоки и волокна** (7–8): как выполнять несколько дел внутри одного процесса.
3. **Межпроцессное взаимодействие** (9–11): как изолированные процессы обмениваются данными.
4. **Синхронизация** (12–15): к каким ошибкам приводит одновременная работа и как от них защищаться.

В конце — расчётные задачи: планирование и алгоритм банкира. Они обязательно будут на контрольной.

**Время:** 1 минута.

---

# Раздел 1. Многозадачность: основные понятия

## Слайд 2. Что такое многозадачность?

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

Покажите рисунок. Различите два понятия:

* **Конкурентность** (слева): одно ядро, A, B, C чередуются. Каждая немного поработает и уступит место другой.
* **Параллелизм** (справа): два ядра, A и B работают **действительно** одновременно.

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

**Пример из жизни:** Один официант по очереди обслуживает пять столиков — это конкурентность. Пять официантов обслуживают пять столиков одновременно — параллелизм.

**Вопрос аудитории:** «Можно ли было на старом одноядерном компьютере слушать музыку и печатать текст?» (Да — это конкурентность, а не параллелизм.)

**Время:** 2 минуты.

---

## Слайд 3. История. Кооперативная и вытесняющая многозадачность

**Что сказать:**
Таблица — это история. Сначала программы работали строго по очереди. Потом заметили: пока программа ждёт диска, процессор простаивает — это расточительство. Так появилось **мультипрограммирование**: пока одна ждёт, работает другая. Затем — **разделение времени**: каждому по маленькому кусочку времени.

Два блока внизу — очень важное различие:

* **Кооперативная:** программа **сама** возвращает процессор. Если в программе ошибка и она зациклилась — она никогда его не вернёт, и зависает весь компьютер. Именно так было в Windows 3.1.
* **Вытесняющая:** ОС **принудительно** забирает процессор. Это делает аппаратный **таймер**: он регулярно посылает прерывание, и управление возвращается ядру. Так работают все современные ОС.

**Пример из жизни:** Кооперативная — докладчик говорит, пока сам не отдаст микрофон. Вытесняющая — каждому докладчику 5 минут, и как только время вышло, модератор выключает микрофон.

**Время:** 2–3 минуты.

---

## Слайд 4. Задачи CPU-bound и I/O-bound ★

**Что сказать:**
Любая программа переключается между двумя состояниями: вычисления на процессоре (CPU burst) и ожидание чего-либо (I/O burst — диск, сеть, клавиатура).

Две полосы на рисунке:
* **CPU-bound** — длинные синие отрезки: сжатие видео, компиляция. В основном считает.
* **I/O-bound** — короткие зелёные отрезки и долгие ожидания: текстовый редактор, веб-сервер. В основном ждёт.

Почему это важно? Если планировщик отдаёт приоритет I/O-bound задачам, они быстро возвращают процессор, а пользователь получает быстрый отклик. Если приоритет у сжатия видео — при наборе текста будет задержка на экране.

**Вопрос аудитории:** «Telegram — CPU-bound или I/O-bound?» (I/O-bound: в основном ждёт сеть и пользователя.)

**Время:** 1–2 минуты.

---

# Раздел 2. Процесс: модель, состояния, PCB

## Слайд 5. Процесс и его адресное пространство

**Что сказать:**
Различите два слова: **программа** — файл на диске, она ничего не делает. **Процесс** — запущенная программа: ей выделены память, открытые файлы, процессорное время. Из одной программы может быть несколько процессов: три окна терминала — три процесса `bash`.

Рисунок справа — устройство памяти процесса:
* внизу **код** (только чтение — чтобы программа не испортила собственный код);
* **глобальные переменные** (Data, BSS);
* **куча** (heap) — память от `malloc`, растёт вверх;
* вверху **стек** — локальные переменные функций, растёт вниз.

Главная мысль: у каждого процесса **своё** адресное пространство. Адрес 1000 в одном процессе — не то же место, что адрес 1000 в другом. Поэтому если упадёт браузер, Word продолжит работать.

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

**Время:** 2–3 минуты.

---

## Слайд 6. Модель состояний процесса

**Что сказать:**
Эта диаграмма — один из важнейших рисунков курса ОС, её нужно уметь рисовать. Пройдите по стрелкам:

1. **New** — процесс создаётся.
2. **Ready** — всё готово, ждёт только процессор. Обычно в этой очереди много процессов.
3. **Running** — выполняется на процессоре.
4. Из Running два пути:
   * **квант истёк** → обратно в Ready (он мог бы работать, но очередь перешла к другому);
   * **ожидание I/O** → в Waiting (сам продолжать не может — нужны данные с диска).
5. **I/O завершён** → в Ready (внимание: не сразу в Running!).
6. **exit** → Terminated.

**Пример из жизни:** Поликлиника: пациенты в очереди — Ready; в кабинете врача — Running (один врач — один пациент); пациент, ждущий результатов анализа, — Waiting. Когда результат пришёл, пациент не заходит сразу в кабинет, а сначала возвращается в очередь.

**Вопрос аудитории:** «Почему нельзя сразу из Waiting в Running?» (Процессор занят другим процессом; решение принимает планировщик.)

**Время:** 3 минуты.

---

## Слайд 7. Состояния в Linux и PCB

**Что сказать:**
Слева — буквы из команды `ps`. На практике вы будете видеть их часто: `R` — работает, `S` — спит (самое частое), `D` — ждёт диска (его даже `kill -9` сразу не остановит), `T` — остановлен, `Z` — зомби.

Справа — **PCB**, «паспорт» процесса. Здесь всё, что ОС знает о процессе: PID, состояние, регистры, приоритет, таблицы памяти, открытые файлы, владелец. В Linux он называется `task_struct`, и в нём сотни полей. При переключении процессор сохраняет регистры именно в PCB и загружает их оттуда.

**Демонстрация:** `cat /proc/$$/status | head -15` — часть PCB текущей оболочки. Покажите строки `State`, `Pid`, `PPid`, `Threads`.

**Пример из жизни:** PCB — медицинская карта пациента. Даже если сменится врач, по карте понятно, на чём остановились.

**Время:** 2 минуты.

---

# Раздел 3. Жизненный цикл процесса: fork, exec, wait, exit

## Слайд 8. `fork()` — один вызов, два возврата

**Что сказать:**
`fork()` — самая странная и самая красивая функция Unix. Её вызывают один раз, а возвращается она **дважды**: в родителе и в новом процессе-потомке. Как различить? По возвращаемому значению: в потомке 0, в родителе — PID потомка, при ошибке −1.

Прочитайте код построчно. Затем спросите: «Чему равен `x` у родителя?» Большинство ответит 15. Правильный ответ — **5**. Почему? Потому что `fork()` **полностью копирует** процесс. Потомок изменил `x` в своей копии, а `x` родителя остался нетронутым. Это две отдельные памяти.

**Copy-on-Write:** «Неужели каждый раз копируются гигабайты?» — Нет. Сначала родитель и потомок делят одну память, а копия создаётся, только когда кто-то **пишет**. Это очень экономно.

**Пример из жизни:** `fork()` — ксерокопия документа. Если что-то зачеркнуть в копии, оригинал не изменится. Copy-on-write — умный ксерокс: он копирует страницу только тогда, когда вы собрались в ней писать.

**Демонстрация:** запустите `fork1.c` и покажите два разных PID.

**Время:** 3 минуты.

---

## Слайд 9. Подсчёт процессов: $n$ вызовов fork $\Rightarrow 2^n$

**Что сказать:**
Это любимая задача контрольных. Три `fork()` подряд. Нарисуйте дерево вместе со студентами:

* Сначала только P0.
* fork 1: P0 создаёт копию → 2 процесса.
* fork 2: копию создают **оба** → 4.
* fork 3: все четверо → 8.

Каждый процесс доходит до `printf`, поэтому «salom» печатается 8 раз.

Запомните правило: `n` последовательных `fork` → **2ⁿ** процессов. Почему? Потому что после каждого `fork` следующий `fork` выполняют **все существующие** процессы.

**Вопрос аудитории:** «Сколько процессов после четырёх `fork()`?» (16.) «Сколько из них новых?» (15.)

**Время:** 3 минуты.

---

## Слайд 10. Ловушки: логические операторы и буферизация ★

**Что сказать:**
Две ловушки — обе встречаются на экзамене.

**Первая:** операторы `&&` и `||` «короткозамкнутые»: если результат ясен по первой части, вторая не вычисляется. В `fork() && fork()`: у потомка первый `fork()` вернул 0, `0 && ...` — ложь, второй `fork` не выполняется. У родителя второй `fork` выполняется. Итого 4 процесса, а не 8. Нарисуйте дерево на доске.

**Вторая:** буферизация. `printf` не пишет текст сразу на экран — сначала кладёт его в буфер в памяти. Если `fork()` выполнится до сброса буфера, буфер тоже скопируется, и текст выведется дважды. Особенно когда вывод перенаправлен в файл или в `| wc -l`. Решение — `fflush(stdout)` перед `fork()`.

**Пример из жизни (буферизация):** Вы написали письмо, но ещё не отправили. В этот момент вас «клонировали» — теперь письмо есть у обоих, и оба его отправят.

**Время:** 2–3 минуты.

---

## Слайд 11. `exec()` и как оболочка запускает команду

**Что сказать:**
`fork` создаёт копию. Но нам чаще нужна **другая** программа. Это делает `exec`: он **заменяет** код, данные и стек процесса новой программой. PID остаётся тем же. При успехе `exec` никогда не возвращается — ведь старого кода, куда возвращаться, больше нет! Поэтому `perror` после `exec` срабатывает только при ошибке.

Схема справа — что происходит, когда вы набираете в терминале `ls`: `bash` копирует себя (`fork`), копия превращается в `ls` (`exec`), `ls` заканчивает работу и становится зомби, а `bash` через `waitpid` забирает его код возврата.

**Пример из жизни:** `fork` — создать близнеца сотрудника. `exec` — обучить близнеца совершенно новой профессии: тело (PID) то же, но работа теперь другая.

**Демонстрация:** `strace -f -e trace=clone,execve,wait4 bash -c 'ls > /dev/null'` — покажите `clone` (fork), `execve` и `wait4`.

**Время:** 2–3 минуты.

---

## Слайд 12. Зомби и сироты

**Что сказать:**
Два странных слова, но с очень точным смыслом:

* **Зомби** — потомок завершился, а родитель ещё не спросил его результат (`wait`). Самого процесса уже нет, осталась только запись в таблице: «код возврата: 0». Зомби не ест ни память, ни CPU, но занимает номер PID.
* **Сирота** — родитель умер раньше потомка. Потомка «усыновляет» `systemd` (PID 1) и потом сам забирает его результат.

Красный блок: зомби нельзя убить `kill -9` — он уже мёртв! Единственный путь — родитель должен вызвать `wait` или нужно завершить родителя.

Почему важно: если сервер на каждый запрос создаёт потомка и не вызывает `wait`, накопятся тысячи зомби, кончатся PID — новый процесс создать будет нельзя.

**Пример из жизни:** Зомби — уволившийся сотрудник, справку которого никто не забрал: он не работает, но числится в списке отдела кадров. Сирота — ребёнок без родителей, которого берёт под опеку государство (systemd).

**Демонстрация:** `bash -c 'sleep 1 & exec sleep 30' &` и через 2 секунды `ps -o pid,ppid,stat,cmd --ppid $!` — покажите состояние `Z`.

**Время:** 3 минуты.

---

# Раздел 4. Процессы в Windows

## Слайд 13. CreateProcess и сравнение с POSIX ★

**Что сказать:**
В Windows нет `fork`. Процесс создаётся одним вызовом `CreateProcess`, который сразу загружает нужную программу — то есть fork и exec вместе.

Основные различия по таблице:
* в Windows к процессу обращаются через **HANDLE** (дескриптор);
* ожидание — `WaitForSingleObject`;
* иерархия слабая: если родитель умер, потомок этого даже не заметит;
* в обеих системах процессор планирует **потоки**, а не процессы.

**Вопрос аудитории:** «Почему в Unix создание разделено на два шага?» (Между fork и exec потомок может настроить своё окружение: перенаправить файлы, подключить каналы. Именно так оболочка реализует `>` и `|`.)

**Время:** 2 минуты.

---

# Раздел 5. Планирование процессора

## Слайд 14. Планировщик и критерии качества

**Что сказать:**
В очереди Ready десятки процессов, а ядро одно. Кого выбрать? Это решает **планировщик**.

Слева — четыре момента, когда принимается решение. Если только когда процесс уходит сам (1 и 4) — невытесняющее планирование. Если ещё по таймеру и по окончании I/O — вытесняющее.

Справа — как измерить «хорошее» планирование. Два критерия запомните точно, потому что будем их считать:
* **Время оборота** = завершение − поступление. Сколько процесс провёл в системе.
* **Время ожидания** = оборот − burst. Сколько простоял в очереди без дела.

**Пример из жизни:** Банк: время оборота — от входа до выхода; время ожидания — сколько просидели в очереди; время обслуживания — у окна кассы.

**Время:** 2 минуты.

---

## Слайд 15. Алгоритмы планирования

**Что сказать:**
Шесть алгоритмов, по фразе на каждый:

* **FCFS** — обычная очередь. Проблема — **эффект конвоя**: если впереди одна длинная задача, все короткие ждут за ней.
* **SJF** — сначала самая короткая. Уменьшает среднее ожидание, но нужно заранее знать, какая задача короткая.
* **SRTF** — вытесняющий вариант SJF: если пришла задача короче оставшейся части текущей, текущая прерывается.
* **Приоритетный** — сначала важные. Опасность — низкий приоритет может ждать вечно. Решение — **старение**: чем дольше ждёт, тем выше приоритет.
* **RR** — по кругу, каждому равный квант. Для интерактивных систем.
* **MLFQ** — несколько очередей; задача, съевшая много процессора, опускается ниже. Так I/O-bound задачи автоматически остаются наверху.

**Пример из жизни (эффект конвоя):** На кассе супермаркета перед вами человек с полной тележкой, а у вас только хлеб. По FCFS — ждёте. SJF — «пропустите человека с хлебом».

**Время:** 3 минуты.

---

## Слайд 16. Пример: FCFS и SJF

**Что сказать:**
Этот слайд объясняйте медленно, повторяя на доске, — студенты должны уметь считать сами.

**FCFS:** по порядку прихода: P1 (0–8), P2 (8–12), P3 (12–21), P4 (21–26). Ожидание каждого = старт − приход: P1 = 0, P2 = 8−1 = 7, P3 = 12−2 = 10, P4 = 21−3 = 18. Сумма 35, среднее **8,75**.

**SJF:** в момент 0 есть только P1 — он стартует и, поскольку алгоритм невытесняющий, работает до конца. В момент 8 остались: P2 (4), P3 (9), P4 (5). Кратчайший — P2, затем P4, последним P3. Ожидание: 0 + 7 + 9 + 15 = 31, среднее **7,75**.

Подчеркните: в SJF P1 работал первым, хотя он длинный, — потому что в момент 0 больше никого не было. Это частая ошибка: студенты ставят первым P2.

**Вопрос аудитории:** «Если бы P1 приходил не в 0, а в 2, кто был бы первым в SJF?» (P2 приходит в 1, и больше никого нет — первым P2.)

**Время:** 4 минуты.

---

## Слайд 17. Пример: SRTF и RR (q = 4)

**Что сказать:**
**SRTF:** в 0 стартует P1. В 1 приходит P2: P2 нужно 4, у P1 ещё осталось 7. 4 < 7 — P1 прерывается, работает P2 (1–5). В 5 остались: P1 (7), P3 (9), P4 (5) → P4 (5–10), затем P1 (10–17), затем P3 (17–26). Для вытесняющего алгоритма ожидание удобно считать по формуле **оборот − burst**. Среднее **6,5** — лучший результат.

**RR (q = 4):** каждый по очереди получает 4 единицы. Не закончивший возвращается в конец очереди. Проследите по диаграмме Ганта. Среднее ожидание **11,75** — худшее!

Таблица внизу — итог. Возникает вопрос: если RR худший, почему Linux и Windows используют нечто похожее? Ответ — на следующем слайде.

**Время:** 4 минуты.

---

## Слайд 18. Вывод и выбор кванта RR

**Что сказать:**
SRTF даёт лучшие цифры, но требует **знать будущее** — сколько времени нужно каждой задаче. Настоящая ОС этого не знает.

RR проигрывает по среднему ожиданию, но у него другое достоинство: **никто долго не ждёт**. Если 10 процессов и квант 10 мс, каждый получает процессор снова не позже чем через 90 мс. Для пользователя это значит «система отвечает».

Выбор кванта — баланс: слишком большой — RR превращается в FCFS; слишком маленький — большая часть процессорного времени уходит на переключения.

**Пример из жизни:** Время на вопрос каждому студенту на семинаре: дадите по 30 минут — выскажутся двое, остальные ждут; по 5 секунд — никто не успеет договорить, всё время уйдёт на передачу микрофона.

**Время:** 2 минуты.

---

# Раздел 6. Планировщики Linux и Windows

## Слайд 19. Linux: CFS, EEVDF, nice. Windows: 32 уровня ★

**Что сказать:**
Linux использует «справедливое» планирование. **CFS** ведёт учёт, сколько процессорного времени получила каждая задача (`vruntime`), и всегда выбирает ту, **которая получила меньше всех**. С ядра 6.6 его сменил **EEVDF** — идея та же, но лучше учитываются задачи, чувствительные к задержке.

**nice** — «вежливость» задачи: от −20 до +19. Большой nice — «я вежливый, уступаю другим». Повысить приоритет может только root — иначе каждый поставил бы своей программе −20.

Windows устроена иначе: 32 уровня приоритета и RR внутри каждого уровня. Активное окно получает временную надбавку приоритета — поэтому окно, на которое вы нажали, отвечает быстрее.

**Демонстрация:** `nice -n 19 yes > /dev/null &` и покажите столбец `NI` в `top`. Затем `kill %1`.

**Время:** 2–3 минуты.

---

## Слайд 20. Переключение контекста

**Что сказать:**
Что происходит, когда процессор переходит от A к B? По схеме: прерывание таймера → ядро сохраняет регистры A в его PCB → планировщик выбирает B → регистры B загружаются из его PCB → B продолжает, как будто ничего не случилось.

Красный блок: это **не бесплатно**. Во время переключения полезной работы нет. Кроме того, кэш процессора был заполнен данными A — для B он «холодный». Поэтому если переключения слишком частые, система замедляется.

Важно: переключение между **потоками** одного процесса дешевле — таблицы памяти не меняются.

**Пример из жизни:** Вы читаете книгу, звонит телефон. Вы кладёте закладку (сохраняете регистры), разговариваете, потом продолжаете с закладки. Но чтобы вспомнить, где в сюжете остановились, нужно немного времени — это «холодный кэш».

**Демонстрация:** `vmstat 1 5` — столбец `cs` (переключений в секунду).

**Время:** 2 минуты.

---

# Раздел 7. Потоки (threads)

## Слайд 21. Поток: что общее, что своё

**Что сказать:**
Поток — «работник» внутри процесса. В одном процессе может быть несколько потоков, и они живут в **одной памяти**.

Рисунок: сверху общее — код, глобальные переменные, куча, открытые файлы. Снизу у каждого потока своё — регистры, счётчик команд и **стек**. Почему стек отдельный? Потому что каждый поток вызывает свои функции и должен иметь свои локальные переменные.

**Пример из жизни:** Процесс — кухня. Потоки — повара. Холодильник, плита, продукты общие (общая память), но у каждого повара свой нож с доской и своя страница рецепта (стек и регистры). Если два повара одновременно потянутся к одной солонке — проблема; её потом назовём «синхронизацией».

**Время:** 2 минуты.

---

## Слайд 22. Зачем нужны потоки? Процесс или поток?

**Что сказать:**
Четыре причины: программа не «зависает» (один поток загружает файл, другой отвечает на кнопки), обмен данными прост (память общая), дёшево и использует много ядер.

Красный блок — цена: изоляции нет. Если один поток ошибётся (например, неверный указатель), умрёт весь процесс.

Таблица справа — когда что выбирать. Лучший пример — Chrome: каждая вкладка — **отдельный процесс**. Почему не поток? Потому что если один сайт уронит браузер, закроется только эта вкладка. Это ради безопасности и стабильности.

**Вопрос аудитории:** «Веб-сервер должен создавать для каждого клиента процесс или поток?» (Обычно поток или пул потоков — дешевле; но если важна безопасность, используют и процессы.)

**Время:** 2 минуты.

---

## Слайд 23. Модели реализации потоков

**Что сказать:**
На рисунке кружки — потоки программы, квадраты — потоки, известные ядру.

* **N:1** — много потоков программы, один поток ядра. Переключение очень быстрое, но если один поток ждёт диска — останавливаются **все**, потому что ядро видит только один поток. Параллелизма нет.
* **1:1** — каждый поток известен ядру. Так работают Linux и Windows. Настоящий параллелизм.
* **M:N** — гибрид: много лёгких потоков распределяются по небольшому числу потоков ядра. Так работают горутины Go — миллионы горутин на нескольких потоках ОС.

**Пример из жизни:** N:1 — несколько продавцов по очереди работают за одной кассой: один звонит по телефону — касса стоит. 1:1 — у каждого продавца своя касса. M:N — 20 продавцов делят 4 кассы: кто свободен, тот садится за кассу.

**Время:** 2–3 минуты.

---

## Слайд 24. Потоки в Linux и pthreads

**Что сказать:**
Интересная особенность Linux: для ядра и процесс, и поток — одна и та же **task**. Разница только в том, какие флаги переданы `clone()`: `CLONE_VM` — «делить память», `CLONE_FILES` — «делить файлы» и так далее. Если делится всё — это поток.

Справа — простейшая программа с потоками POSIX: создаём три потока (`pthread_create`) и ждём их (`pthread_join` — аналог `wait` для процессов).

Важно: порядок вывода строк **не определён**. Когда я проверял, получилось «1, 3, 2». От запуска к запуску порядок может меняться. Если программа полагается на определённый порядок — это ошибка.

**Демонстрация:** запустите `threads` несколько раз — порядок меняется. Затем `ps -eLf | grep threads` (добавив в программу `sleep`) — один PID, несколько LWP.

**Время:** 3 минуты.

---

# Раздел 8. Волокна (fibers) и сопрограммы

## Слайд 25. Волокно: лёгкий кооперативный «поток»

**Что сказать:**
Волокно ещё легче потока. Им управляет не ядро, а **сама программа**. Волокна живут внутри одного потока и передают друг другу управление **сами** (`yield`) — то есть это кооперативная многозадачность, как на слайде 3.

Основные отличия по таблице: переключение очень быстрое (наносекунды, потому что нет системного вызова); можно создать миллионы волокон. Но: параллелизма нет, и если одно волокно сделает блокирующий вызов — остановится весь поток, то есть все волокна.

**Пример из жизни:** Потоки — несколько телевизоров, каждый со своим пультом, а судья (ОС) их переключает. Волокна — один телевизор и несколько человек, которые сами передают друг другу пульт. Если кто-то пульт не отдаст — все смотрят его канал.

**Время:** 2 минуты.

---

## Слайд 26. Сопрограммы и сравнение единиц выполнения ★

**Что сказать:**
Сопрограммы вы уже использовали: `async`/`await` в Python или JavaScript — это они. Функция останавливается на `await` (например, ждёт ответ из сети), в это время работают другие сопрограммы, а когда ответ пришёл — функция продолжает с того же места.

Где полезно: чат-сервер на 100 000 соединений. Поток ОС на каждое — слишком много памяти. С сопрограммами — всё на нескольких потоках.

Таблица справа — итог раздела: процесс → поток → волокно: изоляция уменьшается, скорость растёт.

**Вопрос аудитории:** «Ускорит ли `async`-функция в Python тяжёлые вычисления на процессоре?» (Нет — сопрограммы экономят ожидание, а не вычисления; параллелизма нет.)

**Время:** 2 минуты.

---

# Раздел 9. Межпроцессное взаимодействие (IPC)

## Слайд 27. Две модели IPC

**Что сказать:**
Процессы изолированы — для безопасности это хорошо. Но им нужно общаться: например, в `ls | grep` программа `ls` должна передать результат `grep`. Есть два способа.

* **Передача сообщений** (слева): A отдаёт данные ядру, ядро доставляет их B. Просто и безопасно — ядро всё контролирует. Но каждое сообщение копируется.
* **Разделяемая память** (справа): оба процесса видят одну область памяти. Очень быстро — копирования нет. Но что будет, если они одновременно пишут? Эту проблему программист решает сам — синхронизация.

**Пример из жизни:** Передача сообщений — отправка письма почтой: надёжно, но медленно. Разделяемая память — общая доска в офисе: все сразу видят, но если двое одновременно пишут в одно место — путаница.

**Время:** 2 минуты.

---

## Слайд 28. Обзор механизмов IPC ★

**Что сказать:**
Таблица — карта следующих слайдов. Обратите внимание на три столбца:
* **Родство:** анонимный канал работает только между процессами, связанными через `fork`; остальные — между любыми.
* **Сеть:** только сокеты TCP/UDP работают между разными компьютерами.
* **Особенность:** сигнал несёт только номер, разделяемая память — самая быстрая.

Каждый механизм разберём на следующих слайдах.

**Время:** 1 минута.

---

# Раздел 10. Каналы (pipe, FIFO) и сигналы

## Слайд 29. Анонимный канал (pipe)

**Что сказать:**
Канал — труба в ядре: с одного конца пишете, с другого читаете. `pipe(fd)` даёт два дескриптора: `fd[0]` — чтение, `fd[1]` — запись (запомнить: 0 — вход, как стандартный ввод; 1 — выход, как стандартный вывод).

Код: родитель пишет, потомок читает. Каждый **закрывает** ненужный ему конец.

Правила справа — важны для экзамена:
* чтение из пустого канала — ждёт;
* запись в полный канал — ждёт (в Linux 64 КиБ);
* все писатели закрыли — читатель получает EOF;
* читателя нет — писатель получает `SIGPIPE`.

Красный блок — самая частая ошибка: если потомок не закроет у себя `fd[1]`, считается, что в канале ещё есть «писатель» — он сам! Он будет ждать EOF вечно.

**Пример из жизни:** Канал — водопроводная труба. С одной стороны наливаете, с другой вытекает. Пока кран (писатель) открыт, человек внизу ждёт: «может, ещё польётся».

**Демонстрация:** запустите `./pipe`.

**Время:** 3 минуты.

---

## Слайд 30. Именованный канал (FIFO)

**Что сказать:**
У анонимного канала нет имени, поэтому им могут пользоваться только «родственники» — получившие дескриптор по наследству через `fork`. У FIFO есть имя в файловой системе, поэтому его может открыть любой процесс.

В `ls -l` первая буква `p` — pipe. Обратите внимание: данные через FIFO **на диск не пишутся**, они идут только через память. Имя служит лишь «адресом».

**Демонстрация:** откройте два терминала. В первом `mkfifo /tmp/myfifo; cat /tmp/myfifo` — он будет ждать. Во втором `echo salom > /tmp/myfifo` — в первом появится «salom». Студентам это очень нравится.

**Время:** 2 минуты.

---

## Слайд 31. Сигналы

**Что сказать:**
Сигнал — короткий «звонок» процессу. Данных в нём нет, только номер: «пришёл сигнал 2».

Главные из таблицы:
* **SIGINT** — когда вы нажали Ctrl+C.
* **SIGTERM** — «пожалуйста, завершись». Программа может его перехватить, сохранить работу и красиво завершиться.
* **SIGKILL** — «умри немедленно». Его нельзя ни перехватить, ни игнорировать.
* **SIGSEGV** — обращение к неверной памяти («Segmentation fault»).
* **SIGCHLD** — «твой потомок завершился» — для уборки зомби.

Блок справа: всегда сначала `SIGTERM`, потом при необходимости `SIGKILL`. Если сразу `kill -9`, база данных может оставить файл недописанным.

**Пример из жизни:** SIGTERM — объявление «Магазин закрывается через 10 минут, завершайте покупки». SIGKILL — выключить свет и запереть дверь.

**Вопрос аудитории:** «Почему SIGKILL нельзя перехватить?» (Иначе вредоносная или зависшая программа защитилась бы от убийства — администратор не смог бы её остановить.)

**Время:** 3 минуты.

---

## Слайд 32. Обработчик сигнала

**Что сказать:**
Программа может установить на сигнал свою реакцию — **обработчик** (handler). В коде: через `sigaction` связываем SIGINT с функцией `on_sigint`. При Ctrl+C программа не умирает, а вызывает функцию.

Красный блок — тонкое, но важное правило: внутри обработчика почти ничего нельзя делать, даже `printf`! Причина: сигнал приходит в любой момент — например, когда программа сама находится внутри `printf`. Если и обработчик вызовет `printf`, внутреннее состояние испортится или произойдёт блокировка. Поэтому правильно — только установить флаг, а работу делать в основном цикле.

**Демонстрация:** запустите `./sig`, нажмите Ctrl+C — «SIGINT received». Затем из другого терминала `kill -9 <PID>` — программа умирает, ничего не написав. В bash: `trap 'echo poymano' INT`.

**Время:** 2–3 минуты.

---

# Раздел 11. Разделяемая память, очереди, сокеты

## Слайд 33. Разделяемая память POSIX

**Что сказать:**
Самый быстрый способ IPC. Писатель создаёт именованный объект памяти через `shm_open`, задаёт размер через `ftruncate`, «подключает» его к своей памяти через `mmap` и пишет обычным `strcpy`. Читатель открывает то же имя и читает. Никакого копирования, никаких системных вызовов — как будто работаете с обычным массивом.

Красный блок: но никто не отвечает на вопрос «писатель уже закончил?». Читатель может прочитать недописанные данные. Поэтому разделяемую память всегда используют вместе с семафором.

**Демонстрация:** `./shm_writer`, затем `ls -l /dev/shm` (объект виден), затем `./shm_reader` (он удаляет объект).

**Пример из жизни:** Общий Google Doc: все мгновенно видят изменения. Но если двое одновременно правят одно предложение — путаница.

**Время:** 2–3 минуты.

---

## Слайд 34. Очереди сообщений и сокеты

**Что сказать:**
**Очередь сообщений** похожа на канал, но с двумя отличиями: (1) границы сообщений сохраняются — отправили «task-42», получите ровно «task-42», а не половину; (2) есть приоритеты — важные сообщения достаются первыми.

**Сокеты** — самый универсальный механизм. Одними и теми же функциями общаются процессы на одном компьютере (Unix-сокеты — так работает Docker) или компьютеры в Интернете. Ваш браузер подключается к сайту именно через сокет.

**Демонстрация:** `ss -tlnp` — покажите, какие программы слушают какие порты.

**Время:** 2 минуты.

---

## Слайд 35. IPC в Windows ★

**Что сказать:**
В Windows те же идеи, только другие названия. Два интересных отличия: именованные каналы Windows **двусторонние** и работают по сети; сигналов в Windows нет — вместо них используются события (events).

**Время:** 1 минута.

---

# Раздел 12. Гонка и критическая секция

## Слайд 36. Состояние гонки (race condition)

**Что сказать:**
Теперь самая интересная и самая опасная тема. Посмотрите на код: два потока по миллиону раз увеличивают общий `counter`. Сколько должно получиться? 2 000 000. Что получается на самом деле? У меня — 1 068 612. Запустил ещё раз — 1 049 141. Каждый раз по-разному и всегда меньше!

Почему? `counter++` — одна строка, но для процессора это **три** операции: прочитать из памяти, прибавить, записать обратно. Потоки могут переключиться между этими тремя шагами.

**Демонстрация:** запустите `./race` 3–4 раза. Каждый раз другое число — это производит сильное впечатление.

**Вопрос аудитории:** «Если на компьютере одно ядро, ошибка исчезнет?» (Нет — поток может быть вытеснен между тремя шагами; ошибок меньше, но они есть.)

**Время:** 3 минуты.

---

## Слайд 37. Потерянное обновление (lost update)

**Что сказать:**
Пройдите таблицу по шагам: A прочитал 5. B тоже прочитал 5 (A ещё не записал!). A вычислил 6. B тоже вычислил 6. A записал 6. B тоже записал 6. Увеличили дважды, а результат не 7, а 6. Одно увеличение **потерялось**.

Красный блок: такая ситуация — **data race**. В C и C++ это «неопределённое поведение»: стандарт не гарантирует, что программа вообще будет делать.

**Пример из жизни:** Два кассира одновременно зачисляют деньги на один банковский счёт. Оба видят баланс 100 000. Один добавляет 50 000 и записывает 150 000, другой добавляет 30 000 и записывает 130 000. В итоге на счету 130 000 — 50 000 пропали!

**Время:** 2–3 минуты.

---

## Слайд 38. Критическая секция и три требования

**Что сказать:**
**Критическая секция** — участок кода, работающий с общими данными (у нас — `counter++`). Правило: в ней одновременно может быть только один поток. Схема слева: вход (взять замок) → критическая секция → выход (вернуть замок).

Любое правильное решение должно выполнять три требования:
1. **Взаимное исключение** — двое одновременно не войдут.
2. **Прогресс** — если внутри никого нет, желающий войти не будет ждать вечно.
3. **Ограниченное ожидание** — никто не застрянет в очереди навсегда.

Красный блок: «А если сделать обычной переменной?» — не работает. Оба потока могут одновременно увидеть `busy == 0`, и оба войдут. Между проверкой и установкой — та же гонка.

**Пример из жизни:** Одноместная кабинка: табличка «занято» на двери — это замок. Если двое одновременно увидят «свободно» и одновременно откроют дверь — неловко. Настоящий замок делает «проверить и закрыть» одним движением.

**Время:** 3 минуты.

---

## Слайд 39. Алгоритм Петерсона и атомарные операции

**Что сказать:**
**Алгоритм Петерсона** — программное решение для двух потоков. Идея очень человеческая: «Я хочу войти (`flag`), но проходи ты первым (`turn`)». Если оба хотят войти одновременно, ждёт тот, кто последним записал `turn`. Все три требования выполняются.

Но проблема: современные процессоры ради скорости **переупорядочивают** операции с памятью — и алгоритм ломается. Поэтому на практике используют **аппаратную** поддержку: процессор умеет выполнять «прочитать-изменить-записать» как одну неделимую операцию (Test-and-Set, Compare-and-Swap).

Справа: `atomic_fetch_add` — атомарный `counter++`. С ним результат всегда 2 000 000. Я проверял.

**Пример из жизни:** Петерсон — двое вежливых людей у двери: «Проходите!» — «Нет, вы!». Ждёт тот, кто последним сказал «проходите».

**Время:** 3 минуты.

---

# Раздел 13. Примитивы синхронизации

## Слайд 40. Мьютекс и спин-блокировка

**Что сказать:**
**Мьютекс** — самый используемый замок. `lock` — взять замок (если занят — ждать), `unlock` — вернуть. У мьютекса есть **владелец**: открыть его может только тот поток, который закрыл. Поток, ждущий занятый мьютекс, **спит** — процессор не тратит.

**Спин-блокировка** — ждущий поток не спит, а крутится: «освободилось? освободилось?». Это тратит процессор, но при очень коротком ожидании быстрее, чем заснуть и проснуться. Поэтому её используют внутри ядра.

Красный блок: на одноядерной системе spinlock — худшая идея: пока вы крутитесь, владелец замка вообще не может работать.

**Пример из жизни:** Мьютекс — сидеть на скамейке у занятой кабинки и читать книгу (занимаетесь другим). Spinlock — стоять у двери и каждую секунду дёргать ручку.

**Демонстрация:** `./race m` — с мьютексом всегда 2 000 000. Сравните через `time`: версия с мьютексом медленнее — у синхронизации есть цена.

**Время:** 3 минуты.

---

## Слайд 41. Семафор (Дейкстра, 1965)

**Что сказать:**
Семафор — замок со счётчиком. Две операции: `wait` — «занимаю одно место» (если мест нет — жду), `signal` — «возвращаю одно место».

Пример: к базе данных одновременно должно быть не больше 3 подключений — семафор начинается с 3. Четвёртый поток ждёт.

Приём справа: семафор может обеспечивать и **порядок**. Семафор начинается с 0; B ждёт на `wait`, A выполняет свою работу и делает `post` — только после этого работает B.

Таблица: разница мьютекса и семафора — у мьютекса есть владелец, у семафора нет; мьютекс — только «занят/свободен», семафор — число.

**Пример из жизни:** Семафор — парковка на 3 места и табло на въезде «свободных мест: 3». Каждая въехавшая машина уменьшает число, выехавшая — увеличивает. При 0 ждёте на въезде.

**Время:** 3 минуты.

---

## Слайд 42. Условная переменная, монитор, rwlock, барьер

**Что сказать:**
Иногда замка недостаточно: поток должен ждать, пока «в очереди появится элемент». Проверять в цикле — расточительно. С **условной переменной** поток спит и просыпается, когда другой поток делает `signal`.

Самое важное место в коде: `pthread_cond_wait` атомарно отпускает мьютекс и засыпает. Иначе другой поток не смог бы взять мьютекс и изменить условие.

Красный блок — почему `while`, а не `if`? Когда поток проснулся, условие может снова оказаться ложным: «ложное пробуждение» или другой поток уже забрал элемент. Поэтому после пробуждения всегда проверяем заново.

Остальное кратко: **монитор** — это делает за вас язык (Java `synchronized`); **rwlock** — много читателей или один писатель; **барьер** — ждать, пока дойдут все.

**Пример из жизни:** Условная переменная — флажок на почтовом ящике. Вы не бегаете к ящику каждые 5 минут; когда пришло письмо, почтальон поднимает флажок (signal). Но даже увидев флажок, ящик всё равно проверяете — сосед мог уже забрать письмо (while).

**Время:** 3 минуты.

---

## Слайд 43. Выбор примитива. Аналоги в Windows ★

**Что сказать:**
Таблица слева — практическая шпаргалка: простой счётчик — атомарная переменная; структура данных — мьютекс; много чтений — rwlock; ограниченный ресурс — семафор; ожидание — условная переменная; между разными процессами — именованный семафор.

Таблица справа — названия в Windows. Идеи те же.

**Время:** 1–2 минуты.

---

# Раздел 14. Классические задачи синхронизации

## Слайд 44. Производитель–потребитель

**Что сказать:**
Эта задача встречается повсюду: `ls | grep`, очередь печати, пул потоков. Одна сторона кладёт в буфер, другая забирает. Правила: буфер полон — производитель ждёт; пуст — потребитель ждёт; с самим буфером одновременно работает только один.

Три семафора: `empty` — свободные места (начинается с N), `full` — занятые (с 0), `mutex` — доступ к буферу.

Красный блок — любимый вопрос экзамена: что будет, если производитель возьмёт сначала `mutex`, а потом `empty`? Буфер полон: он держит мьютекс и засыпает, ожидая `empty`. Потребитель не может взять мьютекс, чтобы освободить место. Оба ждут вечно — взаимоблокировка!

**Демонстрация:** `./pc` — `sum = 55` (сумма от 1 до 10).

**Пример из жизни:** Пекарня: пекарь кладёт хлеб на полку (5 мест), продавец забирает. Полка полна — пекарь ждёт; пуста — продавец ждёт.

**Время:** 3–4 минуты.

---

## Слайд 45. Читатели–писатели

**Что сказать:**
Данные могут одновременно читать многие — это безопасно. Но пока кто-то пишет, никто другой не должен ни читать, ни писать.

Идея решения: **первый** пришедший читатель «закрывает дверь для писателей», **последний** уходящий — «открывает». Число читателей считает `read_count`, его защищает `mutex`.

Красный блок: проблема — если читатели идут непрерывно, `read_count` никогда не станет 0, и писатель будет ждать вечно. Это **голодание**.

**Пример из жизни:** Доска объявлений в библиотеке: много студентов читают одновременно. Библиотекарь ждёт, пока все уйдут, чтобы повесить новое объявление. Если студенты идут без перерыва — объявление так и не обновится.

**Время:** 2–3 минуты.

---

## Слайд 46. Обедающие философы

**Что сказать:**
Покажите рисунок: 5 философов, между ними 5 вилок. Чтобы есть, нужны обе соседние вилки.

Наивное правило: «сначала возьми левую вилку, потом правую». Если все пятеро одновременно возьмут левую — каждый ждёт правую, а её держит сосед. Все голодают — взаимоблокировка.

Назовите четыре решения. Главное — первое: пронумеруйте вилки, и пусть каждый берёт сначала вилку с **меньшим** номером. Тогда Ф4 сначала возьмёт не в4, а в0 — он конкурирует с Ф0, и цикл разрывается. Эта идея — «единый порядок» — в следующем разделе станет главным лекарством от deadlock.

**Вопрос аудитории:** «Почему помогает посадить за стол только 4 человек?» (5 вилок и 4 человека — хотя бы один возьмёт обе вилки, поест и вернёт их.)

**Время:** 3 минуты.

---

# Раздел 15. Взаимоблокировки (deadlock)

## Слайд 47. Взаимоблокировка и условия Коффмана

**Что сказать:**
Код слева — простейшая взаимоблокировка: поток 1 взял A и ждёт B; поток 2 взял B и ждёт A. Оба ждут вечно.

4 условия Коффмана — deadlock возникает только тогда, когда все четыре выполняются **вместе**:
1. Ресурсом пользуется один.
2. Держит одно и ждёт другое.
3. Ресурс нельзя отобрать.
4. Есть цикл ожидания.

Главный вывод (внизу): нарушьте одно — и deadlock невозможен.

**Пример из жизни:** Узкий мост, две машины с разных сторон. Каждая заняла половину (удерживает), ждёт половину другой, никто не сдаёт назад (нельзя отобрать). Решение — правило: «на мост въезжают только машины одного направления».

**Время:** 3 минуты.

---

## Слайд 48. Граф распределения ресурсов

**Что сказать:**
Deadlock можно увидеть на рисунке. Круг — процесс, квадрат — ресурс. Стрелка от P к R — «запрашиваю», от R к P — «выделен мне».

Обойдите рисунок: P1 запрашивает B → B выделен P2 → P2 запрашивает A → A выделен P1 → снова P1. Цикл!

Правило: если каждый ресурс в одном экземпляре — есть цикл = есть deadlock. Если экземпляров много (например, 3 принтера), даже при цикле deadlock может не быть — может освободиться другой экземпляр.

**Время:** 2 минуты.

---

## Слайд 49. Методы борьбы

**Что сказать:**
Четыре стратегии:
* **Предотвращение** — писать программу так, чтобы deadlock вообще был невозможен. Проще всего — все берут блокировки в одном порядке.
* **Обход** — ОС при каждом запросе проверяет: «если выдам, останусь ли в безопасности?» Это алгоритм банкира.
* **Обнаружение и восстановление** — допускаем deadlock, но находим его и «жертвуем» одним процессом. Так делают базы данных: «deadlock detected, transaction rolled back».
* **Игнорирование** — «страусиный алгоритм»: голову в песок. Linux и Windows не решают deadlock в пользовательских программах — это задача программиста.

Зелёный блок — практический совет: всегда берите блокировки в одном и том же порядке.

**Время:** 2–3 минуты.

---

## Слайд 50. Алгоритм банкира: состояние

**Что сказать:**
Название алгоритма не случайно. Банк знает кредитный лимит всех клиентов (Max). Сколько каждому уже выдано (Allocation). Сколько ещё может понадобиться (Need = Max − Allocation). Сколько наличных в банке (Available). Банк выдаёт кредит только так, чтобы потом в итоге суметь удовлетворить **всех** клиентов.

Проверьте вместе столбец Need: P0: 7−0, 5−1, 3−0 = (7, 4, 3). Попросите студентов посчитать ещё один.

**Пример из жизни:** У банка 10 миллионов. Трём предпринимателям даны лимиты 7, 5 и 4 миллиона. Банк не может выдать всем сразу весь лимит, но может выдавать по очереди — каждый завершает проект и возвращает деньги.

**Время:** 2–3 минуты.

---

## Слайд 51. Алгоритм банкира: проверка безопасности

**Что сказать:**
Алгоритм: `Work` = свободные ресурсы. Ищем процесс, чей Need помещается в Work. «Завершаем» его — он возвращает все свои ресурсы: прибавляем его Allocation к Work. Повторяем.

Пройдите таблицу по шагам:
1. Work = (3,3,2). P0 нужно (7,4,3) — не помещается. P1 нужно (1,2,2) — помещается! P1 завершается, Work = (5,3,2).
2. P3 нужно (0,1,1) — помещается. Work = (7,4,3).
3. P0 нужно (7,4,3) — теперь ровно помещается. Work = (7,5,3).
4. P2, затем P4.

Все завершились — состояние **безопасно**, последовательность ⟨P1, P3, P0, P2, P4⟩.

Фраза внизу важна: «небезопасное» состояние — ещё не deadlock, просто ОС не может дать гарантию.

**Вопрос аудитории:** «Есть ли другая безопасная последовательность?» (Да, например ⟨P1, P3, P4, P0, P2⟩ тоже безопасна — ответ не единственный.)

**Время:** 4 минуты.

---

## Слайд 52. Родственные проблемы ★

**Что сказать:**
Три «родственные» проблемы:
* **Livelock** — все активно действуют, но результата нет. В коридоре вы влево — и он влево; вы вправо — и он вправо...
* **Голодание** — поток никогда не доходит до начала очереди, потому что другие всё время проходят вперёд.
* **Инверсия приоритетов** — самая интересная история: в 1997 году марсианский аппарат Pathfinder раз за разом перезагружался. Причина: важный поток ждал мьютекс, захваченный низкоприоритетным потоком, а того вытеснял поток средней важности. Проблему исправили удалённо с Земли, включив «наследование приоритета».

**Время:** 2 минуты.

---

# Раздел 16. Инструменты наблюдения в Linux

## Слайд 53. Команды наблюдения за процессами

**Что сказать:**
Это практическая шпаргалка, она пригодится на лабораторной. Главные: `ps`, `pstree`, `top`/`htop`, `kill`, `nice`, `strace`, `lsof`.

Справа — работа с фоновыми заданиями: `&` — запустить в фоне, `jobs` — список, `$!` — PID последнего фонового процесса, `kill %1` — остановить задание 1.

Интересный факт: мьютексы в Linux работают через системный вызов `futex` — и только при конкуренции. Это видно через `strace`.

**Демонстрация:** `sleep 300 &`, `jobs`, `pstree -p $$`, `kill %1`.

**Время:** 2 минуты.

---

# Раздел 17. Практика и выводы

## Слайд 54. Практические задания

**Что сказать:**
На лабораторной выполним эти 7 заданий. Кратко: изучите дерево процессов; сами создадите зомби и найдёте его; заранее посчитаете `fork` и проверите; исправите гонку тремя способами; поработаете с каналом и FIFO; испытаете сигналы; понаблюдаете за планировщиком через `nice`.

Красный блок: всё — только в **виртуальной машине**. Ошибка с fork может создать бесконечные процессы и повесить компьютер.

**Время:** 1–2 минуты.

---

## Слайд 55. Типичные ошибки

**Что сказать:**
Каждая из этих ошибок встречается в реальных программах. Подробно назовите три-четыре:
* не вызвать `wait` → копятся зомби;
* не закрыть конец канала → программа зависает навсегда;
* `if` перед `cond_wait` → редкая, но скрытая ошибка;
* брать блокировки в разном порядке → deadlock, обычно в самый неудобный момент — при росте нагрузки;
* считать `volatile` синхронизацией → `volatile` не даёт атомарности.

**Время:** 2 минуты.

---

## Слайд 56. Что нужно запомнить

**Что сказать:**
Прочитайте вслух 12 главных мыслей. Если студенты запомнят только три, пусть это будут:
1. У процесса своя память; потоки делят память.
2. `counter++` не атомарен — к общим данным нельзя обращаться без защиты.
3. Лучшее лекарство от deadlock — всегда брать блокировки в одном и том же порядке.

**Время:** 2 минуты.

---

## Слайд 57. Контрольные вопросы (1)

**Что сказать:**
Вопросы — для самостоятельной подготовки и контроля. Если есть время, задайте на занятии 2–3:
* **Вопрос 4:** почему нет Waiting → Running? (Сначала в очередь Ready, решение принимает планировщик.)
* **Вопрос 8:** 4 fork → 16 процессов.
* **Вопрос 10:** зомби нельзя убить `kill -9` — он уже завершился.

**Время:** 2 минуты (по желанию).

---

## Слайд 58. Контрольные вопросы (2)

**Что сказать:**
Здесь вопросы по синхронизации. Рекомендуется решить на занятии:
* **Вопрос 24:** перерисуйте таблицу на доске (слайд 37).
* **Вопрос 28:** если поменять местами `wait(empty)` и `wait(mutex)` — deadlock.
* **Вопрос 30:** алгоритм банкира — можно дать домой с другими числами.

**Время:** 2 минуты (по желанию).

---

## Слайд 59. Литература

**Что сказать:**
Отдельно порекомендуйте два источника:
* **OSTEP** (*Operating Systems: Three Easy Pieces*) — бесплатная, очень понятно написанная книга; часть «Concurrency» — ровно сегодняшняя тема.
* **The Little Book of Semaphores** — бесплатная; сборник головоломок по синхронизации, отлично подходит для тренировки.

И `man 7 pthreads`, `man 7 signal` — самые точные справочники.

**Время:** 1 минута.

---

## Заключительный слайд — «Спасибо за внимание! Вопросы?»

**Что сказать:**
Ответьте на вопросы. Если вопросов нет, для закрепления спросите:

1. «Видят ли после `fork()` потомок и родитель одну и ту же переменную?» (Нет — у каждого своя копия.)
2. «Почему при `counter++` в двух потоках результат уменьшается?» (Переключение между тремя шагами — потерянное обновление.)
3. «Какое из четырёх условий deadlock проще всего нарушить?» (Циклическое ожидание — единый порядок блокировок.)

**Домашнее задание:** для процессов P1 (0; 5), P2 (1; 3), P3 (2; 8), P4 (4; 2) (приход; burst) постройте диаграммы Ганта по FCFS, SJF, SRTF и RR (q = 2) и вычислите среднее время ожидания.
