Многопоточность: синхронизация

Часть вторая. Семафоры и иже с ними

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


А так ли хороши критические секции?

Почему мы продолжаем говорить о синхронизации? Ведь, казалось бы, мы уже довольно-таки подробно обсудили критические секции, посмотрели на то, как они работают, и пришли к выводу, что они довольно просты в использовании и не требуют от программиста глубоких познаний в Windows API. Кстати, раз мы говорим о Delphi (а я говорил, что сейчас мы будем рассматривать многопоточные приложения именно с позиций этого языка программирования), то там можно и вовсе обойтись без контакта с Windows API напрямую, используя специально предназначенные для работы с критическими секциями классы библиотеки VCL. Однако, рассказав о критических секциях, я почему-то на этом не успокоился и решил рассказывать про синхронизацию потоков в многопоточных приложениях дальше. И ведь неспроста - наверняка должна существовать веская причина, чтобы занимать синхронизацией газетные полосы.

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


Мьютексы vs. семафоры vs. критические секции

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

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

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


Семафоры собственной персоной

С семафорами, как и ранее с критическими секциями, мы будем работать посредством специальных функций из арсенала Windows API - для того, чтобы примеры было легче портировать с Delphi на другие языки для платформы Windows.

Для начала познакомимся с функцией, ответственной за создание семафоров. Если обратиться к документации по Windows API, то можно увидеть, что выглядит она следующим образом: HANDLE CreateSemaphore (LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG InitialCount, LONG lMaximumCount, LPCTSTR lpName). Описание здесь, как то принято в документации по Windows API, дано не на Delphi, а на C, но, думаю, вы легко в нём сориентируетесь. Первый параметр - это атрибуты безопасности, которые передаются функции для возможности использования семафора другими процессами. Нам пока такие сложности ни к чему, поэтому здесь мы будем передавать функции просто нулевой указатель - nil. Второй и третий параметры - это начальное и максимальное значения счётчика, встроенного в каждый семафор. Об этом счётчике я говорил выше - он считает количество потоков, находящихся в данный момент времени "под юрисдикцией" семафора. Само собой, максимальное количество должно быть не меньше, чем начальное, а начальное должно быть не меньше нуля. Последний параметр, который передаётся функции CreateSemaphore при её вызове - это имя семафора, которое не должно содержать обратных слэшей (\).

Ещё одна функция, которая понадобится нам при работе с семафорами, - это WaitForSingleObject. Выглядит она в справке по Windows API следующим образом: DWORD WaitForSingleObject (HANDLE hHandle, DWORD dwMilliseconds). Как видите, параметров у неё совсем немного - всего два, - но это не мешает данной функции быть весьма и весьма важной. Для чего же она, собственно говоря, нужна? Она переводит поток в состояние ожидания освобождения какого-либо занятого ресурса, из которого он может выйти в двух случаях: либо освобождения данного ресурса другим потоком, либо же истечения времени, выделенного на данное ожидание. Соответственно, передаваемые при вызове этой функции параметры как раз и определяют эти два значения: первый параметр - это дескриптор того объекта, освобождения которого будет ожидать наш поток, а второй - это количество миллисекунд, которое должно истечь до того, как поток как бы устанет ждать. Если мы хотим, чтобы поток просто проверил состояние объекта, то можем поставить в качестве времени 0, а если нужно, чтобы ожидание продлилось до победного конца, который наступит вообще неизвестно когда, то нужно использовать константу INFINITE.

Наконец, третьей по счёту функцией, необходимой тем, кто будет работать с семафорами, является функция ReleaseSemaphore, которая используется для работы со счётчиком семафора. Она будет выглядеть следующим образом: BOOL ReleaseSemaphore (HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount). Думаю, здесь несложно догадаться, что первый параметр - это дескриптор того самого семафора, со счётчиком которого мы с вами будем иметь дело. Второй параметр - это число, на которое мы уменьшаем значение внутреннего счётчика семафора с помощью этой функции. Ну а в третьем параметре программа сохранит предыдущее значение счётчика. Третий параметр функции ReleaseSemaphore - это указатель на целочисленную переменную. Нужен он по той простой причине, что семафор не принадлежит какому-то одному потоку. Впрочем, мы пока будем использовать в качестве этого параметра nil.

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


Программируем!

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

В листинге вы можете увидеть код метода Execute нашего класса потока.

procedure SampleThread.Execute;
var
 s: string;
 Wait_: DWord;
begin
 Wait_ := WaitForSingleObject(Sem, INFINITE);
 if Wait_ = WAIT_OBJECT_0 then
 begin
  repeat
  Synchronize(SetThreadInfo);
  Sleep(1000);
  Inc(Timer);
  until Terminated;
 end;
 ReleaseSemaphore(Sem, 1, nil);
end;

Как видите, именно в этом методе используется функция WaitForSingleObject. Когда значение, возвращаемое ею, будет равно WAIT_OBJECT_0, тогда наш поток "просыпается".

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

procedure TForm1.FormCreate(Sender: TObject);
begin
 Sem := CreateSemaphore(nil, 1, 1, nil);
 T1 := SampleThread.Create(true);
 T2 := SampleThread.Create(true);
 T1.Priority:=tpLower;
 T2.Priority:=tpLower;
 T1.fInternalID := 'Thread 1';
 T2.fInternalID := 'Thread 2';
 T1.Resume;
 T2.Resume;
end;

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

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


А так ли хороши семафоры?

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


Резюме

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

(Продолжение следует)

Вадим СТАНКЕВИЧ,
dreamdrusch@tut.by

Версия для печатиВерсия для печати

Номер: 

03 за 2009 год

Рубрика: 

Software
Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!