Шаг 299.
Модели потоков. Взаимная блокировка

    На этом шаге мы рассмотрим пример возникновения взаимной блокировки.

    Рассмотрим следующий пример. Предположим, в главной форме приложения находится переменная, к которой осуществляется доступ из нескольких потоков как на чтение, так и на запись. Соответственно, для доступа к ней реализуем синхронизацию с использованием семафора:


Рис.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;
Текст этого приложения можно взять здесь (214,8 Кб).

    Данное приложение работает вполне корректно: при щелчке на кнопке в текстовом поле выводятся промежуточные значения и после окончания расчетов приходит сообщение о завершении потока. Теперь немного изменим код процедуры 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;
Текст этого приложения можно взять здесь (214,8 Кб).

    При щелчке на кнопке в этом приложении вывод промежуточной информации не осуществляется, и сколько бы времени мы ни ждали, сообщение о завершении работы потока не появится на экране. То есть в данном приложении поток никогда не завершит свою работу. Для понимания причины необходимо вспомнить работу метода WaitForSingleObject: он ожидает сигнального состояния семафора, и при получении сигнала или по истечении времени ожидания начинает выполняться последующий код, и одновременно семафор переходит в несигнальное состояние. Время ожидания в данном проекте установлено бесконечно большим. Сначала семафор находится в сигнальном состоянии, вызов метода WaitForSingleObject из TMyThread.ReadValue переводит его в несигнальное состояние, и код продолжает выполняться. После этого происходит обращение к методу WaitForSingleObject из кода главного потока в методе GetValue для чтения значения FValue, при этом фоновый поток переводится в состояние ожидания. Но в методе GetValue:WaitForSingleObject семафор не переводится в сигнальное состояние, поэтому главный поток также переходит в режим ожидания. Таким образом, для дальнейшей работы метода TMyThread.ReadValue необходимо завершение метода TForm1.GetValue, а для работы метода TForm1.GetValue - завершение метода TMyThread.GetValue и перехода семафора в сигнальное состояние. Такое бесконечное ожидание называется взаимной блокировкой (deadlock).

    На первый взгляд, данный пример может показаться надуманным - в самом деле, кто же после вызова метода WaitForSingleObject будет вызывать его повторно, не переводя семафор в сигнальное состояние? Однако надуманность кажущаяся - повторное обращение может осуществляться в результате долгой цепочки вызовов, при этом программист может не догадываться о существовании блокировок. Ниже даны некоторые конкретные примеры.

    На следующем шаге мы рассмотрим потокозащищенные классы Delphi.




Предыдущий шаг Содержание Следующий шаг