А если теперь добавить в дом несколько детей — тут все станет еще интереснее.
Назад к процессам и потокам
Так же как и дом занимает некоторый участок земли в жилом массиве, так и процесс занимает некоторый объем памяти компьютера. Аналогично тому, как и обитатели в доме могут свободно войти в любую комнату, в которую пожелают, потоки в процессах все вместе имеют общий доступ к этой памяти. Если поток получает доступ к некоему объекту (мама покупает игрушку), все другие потоки немедленно получают к нему доступ, потому что этот объект существует в общем адресном пространстве — в доме. Аналогично, если процесс распределяет для себя память, эта память становится доступной для всех потоков. Хитрость здесь состоит в том, что необходимо знать, должна ли эта память быть доступной для всех потоков в процессе. Если это так, то доступ потоков к ней придется синхронизировать. Если это не так, то будем считать, что эта память относится к одному конкретному потоку. В этом случае, поскольку только один поток имеет доступ к этой памяти, можно считать, что синхронизация не потребуется — не будет же этот поток сам ставить себе подножки!
Из нашего повседневного опыта мы знаем, что вещи не так просты, как кажутся. Теперь, когда мы рассмотрели основные характеристики (резюме: любой объект является разделяемым!) давайте обратимся к более интересным ситуациям и выясним, чем же они так интересны.
На рисунке, представленном ниже, показано, как мы в дальнейшем будем представлять потоки и процессы. Процесс здесь — это круг, отображающий «контейнерную» концепцию (адресное пространство), а три ломаных линии — это потоки. Вы найдете найдете подобные иллюстрации далее во всех разделах этой книги.
Процесс как контейнер потоков.
Взаимное исключение
Если вы хотите принять душ, и в доме есть еще кто-то, и этот кто-то уже в ванной, вам придется подождать. Как же поток функционирует в аналогичной ситуации?
Потоки используют то, что мы называем взаимным исключением (mutual exclusion). Означает это в значительной степени то, о чем вы и подумали — несколько потоков являются взаимно исключающими, когда речь идёт идет об определенном ресурсе.
Если вы хотите принять душ, это значит, что вы хотите получить эксклюзивный доступ к ванной комнате. Для этого вы должны сначала войти в ванную, а затем закрыть ее дверь изнутри. Если при этом данной ванной комнатой попытается воспользоваться кто-либо другой, его остановит запертая дверь. После того как вы закончили свои дела в ванной, вы откроете дверь и этим позволите еще кому-либо получить доступ в душ.
Именно так и поступает поток. Поток использует объект, называемый мутексом (сокращенно от MUTual Exclusion — взаимное исключение). Этот объект подобен замку в двери: как только поток заблокирует мутекс, никакой другой поток не сможет получить доступ к мутексу до тех пор, пока владеющий мутексом поток его не разблокирует — иными словами, мутекс будет удерживать другие потоки, подобно дверному замку.
Другая интересная параллель, которая проявляется как с мутексами, так и по аналогии с дверными замками, состоит в том, что мутекс является действительно «рекомендательной» блокировкой. Если поток не подчиняется правилам использования мутексов, то такая защита бессмысленна. В нашей аналогии с жилым домом эта ситуация подобна тому, как кто-либо вломился бы в ванную комнату через одну из стен, игнорируя соглашение о запертой двери.
Приоритеты
А что если ванная комната в настоящее время заперта, и множество людей ожидают момента, чтобы ею воспользоваться? Очевидно, все они располагаются вне ее, ожидая, когда же тот, кто в ней находится, наконец выйдет. Закономерный вопрос: «А что произойдет, когда дверь откроется? Кто должен войти следующим?»
Можно предположить, что было бы «справедливым» позволить войти следующим тому, кто ожидает более длительное время. Или было бы «справедливо» позволить войти в ванную следующим тому, кто бы был, например, самый старший по возрасту, или самый высокий, или самый главный. Имеется множество способов определить то, что признавать «справедливым».
Применительно к потокам, мы решаем эту проблему с учетом только двух факторов: приоритета и продолжительности ожидания.
Предположим, что одновременно два человека оказываются у запертой двери в ванную комнату. Одного из них уже «поджимает» время (он опаздывает на совещание), в то время как другой тоже опаздывает, но не так уж сильно. Разве не имело бы смысл позволить тому, кого поджимает время, войти в ванную следующим? Разумеется, имело бы. Остается единственный вопрос о том, как вы принимаете решение о том, кто более «важен» в такой ситуации. Это можно сделать, например, назначив приоритет (давайте использовать номера приоритетов такие, какие приняты в QNX/Neutrino: для рассматриваемой версии QNX/Neutrino номер 1 — самый низкий, номер 63 — самый высокий). Людям в доме, которые имеют неотложные дела, следовало бы дать более высокий приоритет, а тем, у которых таких дел нет, — более низкий. Так же дела обстоят и с потоками. Если бы на момент разблокировки мутекса в ожидании находилось множество потоков, мы бы отдали этот мутекс ожидающему потоку с наивысшим приоритетом. Предположим, однако, что оба человека имеют тот же самый приоритет. Что делать? Хорошо, в этом случае было бы «справедливо» позволить человеку, который ожидал более длительное время, войти следующим. Это было бы не только «справедливо», но и так же, как это делает ядро в QNX/ Neutrino. В случае, когда в ожидании находится группа потоков, мы выстраиваем их сначала по приоритету, а уже в пределах каждого приоритета — по продолжительности ожидания.
Мутекс, конечно же, не единственное средство синхронизации из тех, которые нам доведется встретить. Давайте же рассмотрим и некоторые другие тоже.
Семафоры
Давайте переместимся из ванной комнаты на кухню, так как это социально адаптированное помещение для одновременного обитания более чем одного человека. На кухне вы можете не пожелать, чтобы все и каждый находились бы там одновременно. В действительности вы бы, вероятно, пожелали ограничить число людей на кухне (поваров, например).
Скажем, вы не хотите, чтобы на кухне находилось одновременно более двух человек. Смогли бы вы это реализовать с помощью мутекса? В пределах принятого определения — нет. Почему нет? Это действительно очень интересная проблема в нашей аналогии с домом. Давайте разобьем возникшую проблему на части и проанализируем ситуацию поэтапно.
В ванной комнате возможна одна из двух ситуаций, каждая из которых характеризуется двумя жестко взаимосвязанными состояниями:
• дверь открыта, и в ванной никого нет;
• дверь закрыта, и в помещении находится один человек.
Здесь никакая другая комбинация состояний невозможна — в пустом помещении дверь не может быть никем заперта изнутри (иначе как мы бы ее тогда открыли?), и дверь не может быть открыта кем-либо вне ванной (иначе как бы мы тогда обеспечили приватность использования?). Это и есть пример семафора с единичным значением счетчика — в помещении может находиться не более одного человека, или, иными словами, только один поток может использовать семафор.
Ключевым здесь (прошу прощения за каламбур) является подход к определению замка. В типовой ванной комнате вы сможете запереть и отпереть дверь только изнутри — снаружи средств для этого не предусмотрено. В действительности это означает что блокировка мутекса — это атомарная операция, и невозможна ситуация, в которой, пока вы находитесь в процессе блокировки мутекса, его заблокирует некоторый другой поток, так что в результате вы оба стали бы владельцами этого мутекса. В наше аналогии с жилым домом это не так очевидно — хотя бы потому, что люди гораздо умнее, чем нули и единицы.
Что нам действительно потребуется на кухне, так это замок другого типа.