Данная статья посвящена мьютексам и основана на личных экспериментах и материалах сети.
Что такое мьютексы?
Мьютекс это объект синхронизации потоков разных процессов, скажем 2-х копий одной и той же программы. Он очень похож на критические секции, но разница в том, что мьютекс обеспечивает межпроцессное взаимодействие. Например, если 2-м программам нужно писать в один файл, то как организовать корректное взаимодействие ведь каждая из программ это отдельный процесс? Как одной программе, заметьте программе, а не потоку сказать, чтобы ждала другую? Вот для этого и предназначены мьютексы. В итоге конечно используются потоки, но потоки разных процессов – в этом основное отличие от критических секций.
Одному процессу соответствует несколько потоков – посмотрите на рисунок ниже. Процессов гораздо меньше чем потоков.
Если Вы, скажем, ранним утром, завтракая под радио, намазываете масло на бутерброд, а в этот момент на вас смотрит недовольная жена с куском хлеба, потому что она тоже хотела взять нож для масла, но вы оказались первее, то это и есть мьютекс. Мьютекс организовывает правильное поведение на уровне процессов, скажем копий вашей программы, в ином случае жена полезла бы отбирать у Вас нож для масла и случился бы конфликт из-за разделяемого ресурса.
Как использовать мьютексы?
Я нашел как минимум 2 способа как их можно использовать. Через Thandle и через TMutex из syncobjs. Причем, при использовании TMutex также есть варианты – один вариант захватывать участок кода через Acquire, а другой вариант – захватывать участок кода через WaitFor(). Все это мы посмотрим ниже на примерах, а пока рассмотрим общие схемы…
1 вариант через THandle
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
... var //В глобальных переменных объявляем ... OldMutex: THandle; // здесь важен тип THandle ... _________________________________ // В FormCreate... ... oldMutex := CreateMutex(NIL, FALSE, 'UniqueMutexName'); //Проверка, создался или нет if OldMutex = 0 then RaiseLastWin32Error; ... ______________________________ //Где-то внутри потока(TThread), который мы захватываем ... WaitForSingleObject(OldMutex, INFINITE); // Начало блокировки кода //Здесь какой-то код потока, который мы блокируем ReleaseMutex(OldMutex); //Освобождаем заблокированный код ... ____________________________________ //В FormDestroy... ... CloseHandle(OldMutex); // Закрываем THandle ... |
Пример использования Мьютекса через THandle
Напишем простую программу, которая будет записывать в текстовый файл слово ‘sometext ‘ в количестве 50000 раз. Сама запись будет происходить по нажатию кнопки Start_Writing_To_FileClick. Прогресс записи будет отражаться в файле ProgressBar.
Смысл программы в том, чтобы запустить 2 экземпляра этой программы и нажать на Start_Writing_To_FileClick в обоих экземплярах. В результате, если не отрегулировать потоки – запись будет вестись одновременно из 2 потоков, что приведет к непредсказуемым результатам.
В uses добавим
1 |
...System.IOUtils, System.Types,syncobjs |
В секции type объявляем свой класс потока
1 2 3 4 5 6 |
... TFirstThread=class(TThread) protected procedure execute;override; end; ... |
В глобальных переменных пишем
1 2 3 4 5 |
var ... OldMutex: THandle; FirstThread:TFirstThread; ... |
В FormCreate пишем…
1 2 3 4 5 6 7 |
procedure TForm2.FormCreate(Sender: TObject); begin OldMutex := CreateMutex(NIL, FALSE, 'UniqueMutexName'); //Проверка, создался или нет if OldMutex = 0 then RaiseLastWin32Error; end; |
В FormDestroy
1 2 3 4 |
procedure TForm2.FormDestroy(Sender: TObject); begin CloseHandle(OldMutex); end; |
Далее создадим специальную процедуру, которую будем вызывать в теле потока…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
procedure CreateFileWriteToFileClick(Sender: TObject); var fs:TFilestream; fileName:string; i:integer; begin WaitForSingleObject(OldMutex, INFINITE); //Начало блокировки кода filename:='Test.txt'; if not Tfile.Exists(GetCurrentDir+'\'+FileName) then fs:=TFile.Create(GetCurrentDir+'\'+FileName); try form.ProgressBar.Max:=50000; for i := 0 to 50000 do begin form.ProgressBar.Position := i; Tfile.AppendAllText(GetCurrentDir+'\'+FileName,'sometext '); end; finally fs.Free; ReleaseMutex(OldMutex); // Конец блокировки кода end; end; |
Далее, обработаем метод execute потока
1 2 3 4 5 |
procedure TFirstThread.execute; begin inherited; CreateFileWriteToFileClick(Self); end; |
Далее, непосредственно запускаем поток
1 2 3 4 5 |
procedure TForm2.Start_Writing_To_FileClick(Sender: TObject); begin FirstThread:=TFirstthread.Create(false); FirstThread.FreeOnTerminate:=true; end; |
Тестируем пример
Запускаем 2 копии программы, на одной из них жмем на кнопку – поток запускается. В это же время пытаемся запустить тот же поток из второго экземпляра программы – не запускается.
Когда поток первой копии программы отработал до конца – запускается второй. Это видно на рисунке ниже.
Вот так отработали потоки в моем самом простом эксперименте. Мы использовали мьютексы при помощи THandle. Это, как я понимаю более старый вариант по сравнению с TMutex, поэтому я и использовал имя для переменной мьютекса oldMutex.
Пример использования Мьютекса через TMutex
Теперь сделаем почти тоже самое, только будем использовать TMutex
В uses добавим
1 |
...System.IOUtils, System.Types,syncobjs |
В type
1 2 3 4 5 6 |
... TFirstThread=class(TThread) protected procedure execute;override; end; ... |
В глобальные переменные
1 2 3 4 5 |
... var NewMutex:TMutex; //uses syncobjs FirstThread:TFirstThread; ... |
В FormCreate
1 2 3 4 5 6 7 8 |
... NewMutex:=TMutex.Create( nil, false, 'NewUniqueMutex', false ); ... |
В FormDestroy
1 2 3 |
... NewMutex.Free; ... |
Далее подкорректируем немного процедуру вывода результата
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
procedure CreateFileWriteToFileClick(Sender: TObject); var fs:TFilestream; FileName:string; i:integer; begin NewMutex.Acquire; // Начало блокировки кода потоком //Другие потоки будут ждать до конца выполнения кода // NewMutex.WaitFor(Infinite);// Если раскомментировать и использовать //функцию WaitFor(), то можно регулировать время ожидания данного кода //А также, с помощью WaitFor() из другого потока интересоваться - освободился код или нет //Например так, if NewMutex.WaitFor(250)<>wrSignaled // Заглянули на 250 миллисекунд - освободился код или нет // Если <> wrSignaled, тогда не освободился Filename := 'Test.txt'; if not Tfile.Exists(GetCurrentDir+'\'+FileName) then fs:=TFile.Create(GetCurrentDir+'\'+FileName); try form.ProgressBar.Max:=50000; for i := 0 to 50000 do begin form.ProgressBar.Position := i; Tfile.AppendAllText(GetCurrentDir+'\'+FileName,'sometext '); end; finally fs.Free; NewMutex.Release; // Освобождение заблокированного кода end; end; |
Далее обработаем execute
1 2 3 4 5 |
procedure TFirstThread.execute; begin inherited; CreateFileWriteToFileClick(Self); end; |
Далее вызовем через кнопку этот поток
1 2 3 4 5 |
procedure TForm2.Start_Writing_To_FileClick(Sender: TObject); begin FirstThread:=TFirstthread.Create(false); FirstThread.FreeOnTerminate:=true; end; |
Тестирование покажет те же результаты. Пробуем нажать на кнопку во втором экземпляре программы и ничего.
Когда заканчивает работать первый поток, автоматически запускается второй
Если использовать функцию WaitFor() вместо Acuire, то мы сможем регулировать время ожидания вторым потоком, например WaitFor(2000) – тогда поток из второго экземпляра будет ждать не до конца выполнения, а только 2 секунды.
У меня при тестировании происходила такая картина, если у второй программы нажать на кнопку несколько раз, то поток выполнялся несколько раз во 2-м экземпляре программы. Так вот, чтобы это предотвратить – можно использовать WaitFor() с каким либо числовым параметром, не INFINITE. Скажем WaitFor(2000).
В принципе мы рассмотрели основные методы применения Мьютекса. Узнали что это – написали простейшие программы и протестировали их.
Самое главное запомнить про мютексы, что они нужны для управления потоками из разных процессов, например из 2 копий программ.
А вот что записалось у нас в файл