На этом шаге мы рассмотрим пример возникновения взаимной блокировки.
Рассмотрим следующий пример. Предположим, в главной форме приложения находится переменная, к которой осуществляется доступ из нескольких потоков как на чтение, так и на запись. Соответственно, для доступа к ней реализуем синхронизацию с использованием семафора:
Рис.1. Внешний вид приложения
TForm1 = class(TForm) Button1: TButton; Label1: TLabel; procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private FValue:integer; procedure ThreadDone(Sender:TObject); function GetValue: integer; public property Value:integer read GetValue; end; var Form1: TForm1; implementation var hSem: THandle = 0; {$R *.dfm} procedure TForm1.ThreadDone(Sender: TObject); begin ShowMessage('Конец'); end; function TForm1.GetValue: integer; begin try WaitForSingleObject(hSem, INFINITE); Result:=FValue; finally ReleaseSemaphore(hSem, 1, nil); end; end; procedure TForm1.FormCreate(Sender: TObject); begin FValue:=50; hSem := CreateSemaphore(nil, 1, 1, nil); end; procedure TForm1.FormDestroy(Sender: TObject); begin CloseHandle(hSem); end;
В этом проекте обращение к свойству Value является потокозащищенньш - до тех пор пока не будет выполнен метод GetValue, другие потоки не могут обратиться к этой переменной.
Эта защита была бы лишней, если бы данная переменная была доступна только для чтения. Поэтому создадим класс-потомок TThread, где обращение к этой переменной будет осуществляться как на чтение, так и на запись:
type TMyThread=class(TThread) private FL:integer; procedure DataOutput; procedure ReadValue; protected procedure Execute; override; end; procedure TMyThread.DataOutput; begin Form1.Label1.Caption:=IntToStr(FL); end; procedure TMyThread.ReadValue; begin FL:=Form1.Value; try WaitForSingleObject(hSem, INFINITE); Form1.FValue:=FL+1; finally ReleaseSemaphore(hSem,1,nil); end; end; procedure TMyThread.Execute; begin ReadValue; repeat Inc(FL); Synchronize(DataOutput); Sleep(100); until (FL>=100) or Terminated; end;
В методе ReadValue происходит чтение переменной и затем записывается новое, измененное значение.
Создадим экземпляр класса TMyThread при щелчке на кнопке:
procedure TForm1.Button1Click(Sender: TObject); begin with TMyThread.Create(True) do begin FreeOnTerminate := True; OnTerminate := ThreadDone; Resume; end; end;
Данное приложение работает вполне корректно: при щелчке на кнопке в текстовом поле выводятся промежуточные значения и после окончания расчетов приходит сообщение о завершении потока. Теперь немного изменим код процедуры TMyThread.ReadValue, а именно - обратимся свойству TForm1.GetValue после вызова метода WaitForSingleObject:
procedure TMyThread.ReadValue; begin FL:=Form1.Value; try WaitForSingleObject(hSem, INFINITE); FL:=Form1.Value; Form1.FValue:=FL+1; finally ReleaseSemaphore(hSem,1,nil); end; end;
При щелчке на кнопке в этом приложении вывод промежуточной информации не осуществляется, и сколько бы времени мы ни ждали, сообщение о завершении работы потока не появится на экране. То есть в данном приложении поток никогда не завершит свою работу. Для понимания причины необходимо вспомнить работу метода WaitForSingleObject: он ожидает сигнального состояния семафора, и при получении сигнала или по истечении времени ожидания начинает выполняться последующий код, и одновременно семафор переходит в несигнальное состояние. Время ожидания в данном проекте установлено бесконечно большим. Сначала семафор находится в сигнальном состоянии, вызов метода WaitForSingleObject из TMyThread.ReadValue переводит его в несигнальное состояние, и код продолжает выполняться. После этого происходит обращение к методу WaitForSingleObject из кода главного потока в методе GetValue для чтения значения FValue, при этом фоновый поток переводится в состояние ожидания. Но в методе GetValue:WaitForSingleObject семафор не переводится в сигнальное состояние, поэтому главный поток также переходит в режим ожидания. Таким образом, для дальнейшей работы метода TMyThread.ReadValue необходимо завершение метода TForm1.GetValue, а для работы метода TForm1.GetValue - завершение метода TMyThread.GetValue и перехода семафора в сигнальное состояние. Такое бесконечное ожидание называется взаимной блокировкой (deadlock).
На первый взгляд, данный пример может показаться надуманным - в самом деле, кто же после вызова метода WaitForSingleObject будет вызывать его повторно, не переводя семафор в сигнальное состояние? Однако надуманность кажущаяся - повторное обращение может осуществляться в результате долгой цепочки вызовов, при этом программист может не догадываться о существовании блокировок. Ниже даны некоторые конкретные примеры.
На следующем шаге мы рассмотрим потокозащищенные классы Delphi.