logo

68. Многопоточное программирование. Процесс и поток выполнения. Средства синхронизации потоков.

Многопоточность – в этом сценарии, мы использовали много потоков, которые могут брать задачи и приступать к работе с ними. У нас есть пулы потоков (новые потоки также создаются, основываясь на потребности и доступности ресурсов) и множество задач. Итак, поток может работать вот так:

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

Процесс состоит из одного или нескольких потоков выполнения (threads).

Поток выполнения (execution thread) – это исполняемый код, который имеет собственный стек и часть контекста процесса (регистры).

Потоки разделяют ресурсы процесса (код, память, дескрипторы)

Переключение потоков (context switching) выполняется быстрее переключения процессов.

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

Livelock – потоки не блокируются (как во взаимной блокировке), а занимается бесполезной работой, их состояние постоянно меняется – но, тем не менее, они не производят никакой полезной работы.

Пример

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

Состояние гонки (Race condition) – состояние при котором несколько потоков одновременно читают/пишут в разделяемую область памяти.

Синхронизация доступа к данным — одна из первых вещей, с которыми столкнется человек, берущийся за многопоточное программирование в низкоуровневых языках. Скажем, C вообще не имеет встроенных примитивов для синхронизации доступа, и вам как минимум потребуется использовать POSIX Semaphores или написать свое решение. В Java есть уже некоторые полезные штуки, но вам все равно придется сперва разобраться в том, как они работают.

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

Для решения этой проблемы применяют блокировки, неблокирующий доступ (CAS) и некоторые особенности конкретных языков.

Блокировки

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

Краевой случай семафора — мьютекс, максимально упрощенный семафор. Главное в нем то, что только один поток в один момент времени может владеть мьютексом. Операционные системы часто имеют свои высокопроизводительные реализации мьютексов — фьютексы в Linux, FAST_MUTEX в Windows. Кроме того, в различных языках есть свои специализированные реализации мьютексов для особых задач.