Часть 6. Снова о синхронизации: Критические секции и мьютексы.

Содержание:

  • Ограничения Synchronize.
  • Критические секции.
  • Что это все значит для программиста на Delphi?
  • На заметку.
  • Могут ли данные пропасть или остаться недоступными в буфере?
  • Как насчет сообщений out of date?
  • Проблемы Flow Control и неэффективность списка.
  • Мьютексы.


Ограничения Synchronize.

У метода Synchronize есть несколько недостатков, благодаря которым он подходит лишь для простых многопоточных приложений.

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

Правда, у Synchronize есть и одно преимущество над другими механизмами синхронизации:

  • В методе, вызываемом с помощью Synchronize, может быть любой код, в том числе и потоко-небезопасный код VCL.


Важно помнить, для чего в приложении используются потоки. Основная причина для большинства Delphi-программистов в том, что они хотят, чтобы их приложение оставалось восприимчивым к действиям пользователя, пока выполняются длительные операции или используется передача данных с блокировкой или ввод-вывод. Это часто означает, что основной поток приложения должен выполнять краткие, основанные на событиях подпрограммы,а также отвечать за пользовательский интерфейс, т.е. обеспечивать прием ввода и отображать результаты. Другие потоки приложения будут выполнять "черную работу". Основываясь на этой философии, часто приходится делать так, что большая часть кода, выполняющегося в рабочих потоках, не использует код VCL, который не является потокобезопасным. Рабочие потоки могут выполнять операции с файлами или базами данных, но они редко используют потомков TControl. В этом свете Synchronize может привести к проблемам с производительностью.
Многим потокам нужно связываться с VCL только в простых случаях, как например, передача потока (stream) данных, или выполнение запроса к базе данных и возвращение структуры данных как результат этого запроса. Как отмечено в Главе 3, при модификации общих данных нам нужно только поддерживать атомарность. В качестве простого примера можно рассмотреть поток данных (stream), который записывается рабочим потоком, и периодически читается основным потоком VCL. Нужно ли нам гарантировать, что поток VCL никогда не выполняется одновременно с рабочим? Конечно, нет! Все, что нужно обеспечить - так это то, что только один поток модифицирует этот разделяемый ресурс в каждый момент, таким образом устраняя условия для конфликтов, и делая операции с коллективным ресурсом атомарными. Такой режим называется взаимное исключение (mutual exclusion). Есть много примитивов синхронизации, которые могут быть использованы для осуществления такого режима. Простейшие из них - мьютекс (Mutex), встроенный в Win32, и близкие к мьютексам критические секции (Critical Section). Последние версии Delphi содержат класс, который инкапсулирует вызовы критических секций Win32. Этот класс здесь не обсуждается, поскольку он имеется не во всех 32-битовых версиях Delphi. У программистов, использущих этот класс, не должно быть больших трудностей в использовании соответствующих его методов для достижения таких же эффектов, как и обсуждаемые здесь.

Критические секции.

Критическая секция (Critical Section) позволяет добиться взаимного исключения. Win32 API поддерживает несколько операций с ними:

  • InitializeCriticalSection.
  • DeleteCriticalSection.
  • EnterCriticalSection.
  • LeaveCriticalSection.
  • TryEnterCriticalSection (только Windows NT).


Операции InitializeCriticalSection и DeleteCriticalSection можно рассматривать подобно созданию и освобождению объектов в куче. Обычно имеет смысл проводить действия по созданию и разрушению критических секций в одном потоке, причем в наиболее долгоживущем. Очевидно, что все потоки, которые хотят синхронизовать доступ, используя критическую секцию, должны иметь дескриптор или указатель на нее. Это может быть достигнуто прямым путем через общую переменную или независимо, что возможно, поскольку критическая секция встроена в потокобезопасный класс, к которому имеют доступ оба потока.
Когда объект критической секции создан, его можно использовать для контроля за общими ресурсами. Две главных операции - EnterCriticalSection и LeaveCriticalSection. В большей части литературы, касающейся темы синхронизации, эти операции называют Wait и Signal, или Lock и Unlock соответственно. Эти альтернативные термины используются также и для других примитивов синхронизации, где они имеют приблизительно эквивалентные значения. По умолчанию при создании критической секции ни один из потоков приложения не владеет ей (ownership). Чтобы управлять критической секцией, поток вызывает EnterCriticalSection, и если критическая секция еще не имеет владельца, то поток становится им. Затем поток обычно совершает действия с общими ресурсами (критическая часть кода, показана двойной линией), а когда заканчивает эти действия, то отказывается от владения критической секцией вызовом LeaveCriticalSection.

--Resize_Images_Alt_Text--

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

--Resize_Images_Alt_Text--

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

Что это все значит для Delphi-программиста?

Это означает, что если не нужно проводить действия с VCL, а только обеспечить доступ к данным и их изменение, при написании программ с использование потоков на Delphi программист избавлен от бремени TThread.Synchronize.

  • Потоку VCL не нужно находиться в состоянии ожидания, чтобы рабочий поток мог модифицировать общие ресурсы, он только не должен быть внутри критической секции.
  • Критические секции не знают и не заботятся о том, является ли их владелец основным потоком VCL или экземпляром объекта TThread, так как только один из потоков может использовать критическую секцию.
  • Программист, разрабатывающий многопоточное приложение, может теперь безопасно (почти) использовать WaitFor, избегая проблемы зацикливания.


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

Выделить всёРазвернуть кодкод Pascal/Delphi
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:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
unit PrimeForm2;
 
interface
 
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, PrimeThread;
 
const
  WM_DATA_IN_BUF = WM_APP + 1000;
  MaxMemoLines = 20;
 
type
  TPrimeFrm = class(TForm)
    ResultMemo: TMemo;
    StartBtn: TButton;
    StartNumEdit: TEdit;
    StopBtn: TButton;
    procedure StartBtnClick(Sender: TObject);
    procedure StopBtnClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  private
    { Private declarations }
    FStringSectInit: boolean;
    FPrimeThread: TPrimeThrd2;
    FStringBuf: TStringList;
    procedure UpdateButtons;
    procedure HandleNewData(var Message: TMessage); message WM_DATA_IN_BUF;
  public
    { Public declarations }
    StringSection: TRTLCriticalSection;
    property StringBuf: TStringList read FStringBuf write FStringBuf;
  end;
 
var
  PrimeFrm: TPrimeFrm;
 
implementation
 
{$R *.DFM}
 
procedure TPrimeFrm.UpdateButtons;
begin
  StopBtn.Enabled := FStringSectInit;
  StartBtn.Enabled := not FStringSectInit;
end;
 
procedure TPrimeFrm.StartBtnClick(Sender: TObject);
begin
  if not FStringSectInit then
  begin
    InitializeCriticalSection(StringSection);
    FStringBuf := TStringList.Create;
    FStringSectInit := true;
    FPrimeThread := TPrimeThrd2.Create(true);
    SetThreadPriority(FPrimeThread.Handle, THREAD_PRIORITY_BELOW_NORMAL);
    try
      FPrimeThread.StartNum := StrToInt(StartNumEdit.Text);
    except
      on EConvertError do FPrimeThread.StartNum := 2;
    end;
    FPrimeThread.Resume;
  end;
  UpdateButtons;
end;
 
procedure TPrimeFrm.StopBtnClick(Sender: TObject);
begin
  if FStringSectInit then
  begin
    with FPrimeThread do
    begin
      Terminate;
      WaitFor;
      Free;
    end;
    FPrimeThread := nil;
    FStringBuf.Free;
    FStringBuf := nil;
    DeleteCriticalSection(StringSection);
    FStringSectInit := false;
  end;
  UpdateButtons;
end;
 
procedure TPrimeFrm.HandleNewData(var Message: TMessage);
begin
  if FStringSectInit then {Not necessarily the case!}
  begin
    EnterCriticalSection(StringSection);
    ResultMemo.Lines.Add(FStringBuf.Strings[0]);
    FStringBuf.Delete(0);
    LeaveCriticalSection(StringSection);
    {Now trim the Result Memo.}
    if ResultMemo.Lines.Count > MaxMemoLines then
      ResultMemo.Lines.Delete(0);
  end;
end;
 
procedure TPrimeFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  StopBtnClick(Self);
end;
 
end.


Это довольно похоже на предыдущие примеры в том, что касается создания потока, но есть и несколько дополнительных полей основной формы, которые следует создать. StringSection - это критическая секция, которая контролирует доступ к ресурсам, разделяемым между потоками. FStringBuf - список строк, который выступает буфером между основной формой и рабочим потоком. Рабочий поток посылает результаты в основную форму, добавляя их к этому списку, который является единственным общим ресурсом в этой программе. И наконец, имеется логическая переменная, FStringSectInit. Эта переменная служит для проверки того, что необходимые объекты синхронизации реально созданы прежде, чем их начали использовать. Общие ресурсы создаются, когда мы запускаем рабочий поток, и уничтожаются сразу после того, как мы убедимся, что рабочий поток завершился. Заметьте, что поскольку список строк, выступающий в роли буфера, распределен динамически , мы должны использовать WaitFor при уничтожения потока, чтобы убедиться, что рабочий поток перестал использовать буфер до его освобождения.
Мы можем использовать WaitFor в этой программе, не беспокоясь о зацикливании, поскольку можно доказать, что никогда не бывает ситуации, в которой оба потока ждут друг друга. Доказать это просто:

    1 Рабочий поток ждет только при попытке получить доступ к критической секции.
    2 Основной поток программы ждет только при ожидании завершения рабочего потока.
    3 Основной поток программы не ожидает, когда он завладеет критической секцией.
    4 Если рабочий поток ожидает критической секции, главная программа освободит критическую секцию до того, как она будет ждать рабочий поток.

Вот код рабочего потока

Выделить всёРазвернуть кодкод Pascal/Delphi
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:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
unit PrimeThread;
 
interface
 
uses
  Classes, Windows;
 
type
  TPrimeThrd2 = class(TThread)
  private
    { Private declarations }
    FStartNum: integer;
    function IsPrime(TestNo: integer): boolean;
  protected
    procedure Execute; override;
  public
    property StartNum: integer read FStartNum write FStartNum;
  end;
 
implementation
 
uses PrimeForm2, SysUtils;
 
function TPrimeThrd2.IsPrime(TestNo: integer): boolean;
 
var
  iter: integer;
 
begin
  result := true;
  if TestNo < then
    result := false;
  Продолжение »