Шаг 286.
Модели потоков. Понятие о синхронизации (окончание)

    На этом шаге мы рассмотрим необходимость использования синхронизации.

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

  12 December 2012
  1. Поток, который должен считывать значение, начал чтение и считал первые 8 символов: 12 Decem.
  2. Происходит вытеснение первого потока, и второй поток заносит новое значение: 15 February 2013.
  3. Через некоторое время восстанавливается первый поток, который продолжает чтение буфера с прерванного места и считывает оставшуюся часть: агу 2013.

    Значение даты, которое считал первый поток (12 Decemary 2013), абсолютно бессмысленно. Если, например, занести данное значение в базу данных, то очевидны трудности с последующей интерпретацией этой строки. К сказанному следует добавить, что если хранить эту переменную не в постоянном буфере, а использовать переменную типа string, могут происходить нарушения в защите памяти.

    Таким образом, если два или более потока обращаются к какой-либо общей переменной, то требуется написание специального программного кода, смысл которого состоит в том, что если какой-либо из потоков работает с общей переменной, то другие потоки не имеют нрава прервать выполнение этого потока до окончания работы с ней. Такой код обеспечивает синхронизацию доступа к данным. Синхронизация не требуется, если из разных потоков происходит только чтение данных. Однако если хотя бы один из потоков изменяет данные, то требуется синхронизация как при чтении переменной, так и при ее записи.

    Метод Synchronize, который определен в классе TThread, обеспечивает синхронизацию фонового и главного потоков. В качестве параметра этот метод использует адрес другого метода - например, DoSomething. При выполнении кода, реализованного в методе DoSomething, главный поток не прерывает фоновый до завершения выполнения метода DoSomething. В коде DoSomething можно обращаться к общим переменным, не опасаясь описанных выше коллизий. Ясно, что метод Synchronize нельзя использовать для синхронизации доступа к данным из двух фоновых потоков. Для этого требуются специальные объекты - критические секции, семафоры, мыотексы, сообщения. О них будет рассказано в следующих шагах.

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

    В классе TThread определен метод WaitFor. Вызов этого метода означает прекращение выполнения кода главного потока до окончания работы фонового. Создадим новый проект, поместим па форму две кнопки, объявим класс ТМуThread - потомок класса TThread и реализуем следующий код:

procedure TMyThread.Execute;
begin
  // Модель реального кода вычислений
  repeat
    Inc(FL);
    Sleep(100);
    Beep;
  until (FL >= 30) or Terminated;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  if FS <> nil then Exit;
  FS := TMyThread.Create(True);
  with FS do
  begin
    FreeOnTerminate := True;
    OnTerminate := ThreadDone;
    Resume;
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  if FS <> nil then FS.WaitFor;
  ShowMessage('Все');
end;

procedure TForm1.ThreadDone(Sender: TObject);
begin
  FS := nil;
end;
Текст этого приложения можно взять здесь (214,0 Кб).

    Данный код можно тестировать в операционной системе Windows 95/98/NT, но не Windows 2000/XP. При щелчке на первой кнопке начинаются вычисления - это контролируется на слух по писку динамиков. Если в процессе вычислений щелкнуть на второй кнопке, то сообщение "Все" появится не сразу, а только после окончания писка в динамиках.

    Вследствие ошибки в VCL при реализации метода TThread.WaitFor, а также изменений в операционной системе Windows 2000/XP, вызов деструктора класса TThread осуществляется до того, как завершится код метода TThread.WaitFor. Это приводит к появлению исключения EOSExeption с кодом 6 - Invalid Handle. Поэтому если может быть вызван метод WaitFor, то свойство FreeOnTerminate должно иметь значение False. В этом случае запрещено вызывать деструктор в явном виде из обработчика событий OnTerminate - он будет вызываться раньше завершения метода WaitFor.

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




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