Digs - Персональная территория

Авторский проект Артема Глазкова
? 
        Версия для печати (цвет)  

Использование материалов
Заметка #8
10 июня 2005

Потоки (Threads)


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

    Что же такое поток
    Поток, это процесс, который исполняется параллельно основному процессу. В принципе, это та же задача с точки зрения Windows, но она выполняется в том же адресном пространстве, что и основной поток программы, пользуясь теми же переменными.
    Для такой реализации в Delphi присутствует класс TThread. Сам класс не является полностью функциональным, так как метод Execute, который должен содержать код потока, в данном классе абстрактный.

    Кодинг
    Как уже было сказано, класс будет содержать список команд, которые будут по очереди извлекаться из этого списка и выполняться. Причем добавление команд происходит в основном потоке, а извлечение из списка и выполнение в другом. Соответственно, при доступе к списку команд может произойти конфликт, что приведет к непредсказуемой ситуации.
    Для предотвращения подобных конфликтов используется особый ресурс, который может быть заблокирован только одним процессом. При попытке блокировки другим процессом, последний будет остановлен до тех пор, пока первый не разблокирует данный ресурс. Такой ресурс еще называют критической секцией. В Delphi его функции выполняет класс TCriticalSection. У класса есть два основных метода Enter и Leave, которые, соответственно, блокируют и разблокируют ресурс. В нашем случае мы соотнесем ресурс с доступом к списку команд.
    Основное нам уже понятно, можно приступать к кодингу. Выполняемую команду определим как класс:

TCmd = class
  procedure Execute; virtual;
end;

    Метод Execute будет заниматься выполнением команды. Мы его делаем виртуальным, чтобы можно было сделать кучу наследников, если все исполняемые команды будет не рационально затолкать в один класс.
    Теперь исполнитель:

TCmdList = class(TThread)
private
  FCmds : TList;
  FEmpty : Boolean;
  csList : TCriticalSection;
  FEvent : TSimpleEvent;
public
  constructor Create;
  destructor  Destroy; override;
  procedure   AddItem(AItem : TCmd);
  procedure   Execute; override;
end;

    Немного остановимся на конструкторе TThread.Create, а точнее на его параметре CreateSuspended. Если его значение false, то конструктор сразу же запускает метод Execute. Если true, то происходит только инициализация класса, а для старта придется дополнительно вызвать метод Resume. Так как мы не хотим обременять себя в дальнейшем дополнительными строками кода, будем вызывать Create c параметром false. Но до этого проинициализируем нужные нам структуры.

constructor Create;
begin
  FCmdList := TList.Create;
  csList := TCriticalSection.Create;
  FEvent := TSimpleEvent.Create; //об этом позже
  FEmpty := true;
  inherited Create(false);
end;

    Теперь метод добавления с учетом блокировки ресурсов. Сразу отмечу переменную FEmpty. Она принимает значение true когда список пуст и все команды отработали.

procedure TCmdList.AddItem(AItem: TCmd);
begin
  try
    csList.Enter;          //блокируем ресурс
    FCmdList.Add(AItem);   //добавляем команду в список
    FEmpty := false;       //сбрасываем флаг «Пусто»
    FEvent.SetEvent;       //об этом чуть позже
  finally
    csList.Leave;
  end;
end;

    Пока все просто.
    Перед тем как писать основной цикл, разберемся, что такое семафор. Семафор в жизни поездов разрешает или запрещает им дальнейшее следование. В жизни потоков происходит приблизительно то же самое. В Delphi семафор представлен классом TEvent. Мы будет использовать его наследника TSimpleEvent, который проще в обращении. В этом классе нас будут интересовать:
    1. WaitFor(Timeout: DWORD). Данная функция включает ожидание установки семафора в течение Timeout миллисекунд.
    2. SetEvent. Эта процедура позволяет установить «зеленый свет» семафора и тем самым разблокирует выполнение другого потока, остановленного при помощи WaitFor.
    3. ResetEvent. Данный метод сбрасывает семафор в состояние, при котором возможно ожидание с помощью WaitFor.
    Зачем же нам нужен семафор? А нужен он для того, чтобы заставить стоять поток до тех пор, пока в списке не появится очередная команда, либо не будет подан знак, для завершения работы потока, т.е. для выхода из Execute. Это снизит нагрузку на процессор и даст больше процессорного времени для основного потока.
    Теперь мы все знаем и можем приступить к написанию главной процедуры.

procedure TCmdList.Execute;
var Item : TCmd;
begin
  repeat
    try
      csList.Enter; //блокируем ресурс
      if FCmds.Count<>0 then
        begin
          //Если очередь команд не пустая,
          //получаем команду
          Item := FCmdList[0];
          FCmdList.Delete(0)
        end
      else
        begin
          //иначе делаем сброс семафора
          FEvent.ResetEvent;
          Item := nil
        end;
    finally
      //разблокировали ресурс
      csList.Leave;
    end;
    //проверяем полученный Item
    if Item<>nil then
      begin
        try
          //Если есть, производим выполнение
          Item.Execute;
        except
        end;
        Item.Free;
      end
    else
      begin
        //Если нету, то сообщаем что список пуст
        FEmpty := true;
        //И заставляем поток ждать
        FEvent.WaitFor(1000);
      end;
  until FExit;
end;

    Цикл заканчивается проверкой переменной FExit, которая сигнализирует завершение работы потока.
    В завершение нам нужно написать пару методов. Первый Stop будет ожидать конца выполнения всех команд, второй Clear будет очищать очередь команд, если их выполнение становится для нас не важным.

procedure TCmdList.Stop;
begin
  FExit := true;
  repeat
    //устанавливаем "зеленый", чтобы разблокировать Execute
    FEvent.SetEvent;
  until FEmpty;  //ожидаем, пока список не будет пустой
end;

procedure TCmdList.Clear;
var Itm : TCmd;
    k : Integer;
begin
  try
    //блокируем ресурс
    csList.Enter;
    //удаляем все элементы
    for k:=0 to FCmdList.Count-1 do
      begin
        Itm := FCmdList[k];
        Itm.Free;
      end;
    //чистим список ссылок
    FCmdList.Clear;
  finally
    csList.Leave;
  end;
  Stop;
end;

   Вот и все.


© 2005-16, Powered By Digs (Написать письмо, vk)