вторник, 31 августа 2010 г.

Важность указания флага WT_EXECUTELONGFUNCTION в QueueUserWorkItem

Это перевод The importance of passing the WT_EXECUTELONGFUNCTION flag to QueueUserWorkItem. Автор: Реймонд Чен.

Одним из флагом функции QueueUserWorkItem является WT_EXECUTELONGFUNCTION. Документация для этого флага говорит:
Функция обратного вызова может выполняться длительное время. Этот флаг помогает системе решить, надо ли ей создавать новый поток.

Как указано в документации, пул потоков использует этот флаг для решения, надо ли ему создавать новый поток или же ждать завершения работы запущенного элемента. Если все потоки текущего пула потоков заняты работой и появляется новая задача, то она будет ждать завершения любой уже запущенной задачи, если они "короткие", потому что ожидается, что они очень скоро завершатся и освободят поток для запуска следующей задачи. С другой стороны, если все задачи помечены флагом WT_EXECUTELONGFUNCTION, то пул потоков знает, что ожидание завершения таких задач не будет очень-то продуктивным, поэтому он предпочтёт создать новый поток для запуска новой задачи.

Если вы не укажете флаг WT_EXECUTELONGFUNCTION для "долгой" задачи, то пул потоков будет ожидать завершения этой задачи до запуска новой, когда ему следовало бы создать новый поток немедленно. Если ваша задача не завершиться быстро, то в итоге у пула потоков закончится терпение, и он поймёт, что вы ему наврали - так что он всё равно создаст новый поток. Но это займёт некоторое время - пока пул потоков осознает, что он ждал напрасно.

Давайте продемонстрируем это простой консольной программой (прим.пер.: этот пример будет демонстрировать задуманное только на однопроцессорной машине):
program Project1;

{$APPTYPE CONSOLE}

uses
  Windows, SysUtils;

var
  LastTick: Cardinal;

procedure Tick(Context: Pointer; Success: Boolean); stdcall;
var
  Tick: Cardinal;
begin
  Tick := GetTickCount;
  Writeln(Format('%5d', [Tick - LastTick]));
end;

function Clog(Context: Pointer): Integer; stdcall;
begin
  Sleep(4000);
  Result := 0;
end;

var
  hTimer: THandle;
begin
  LastTick := GetTickCount;

  case ParamCount of
    1: QueueUserWorkItem(Clog, nil, 0);
    2: QueueUserWorkItem(Clog, nil, WT_EXECUTELONGFUNCTION);
  end;

  CreateTimerQueueTimer(hTimer, 0, Tick, nil, 250, 250, 0);

  Sleep(INFINITE);
end.
Эта программа создаёт задачу каждый раз, когда срабатывает таймер Tick - примерно каждые 250 мс. Сама задача просто печатает, как много времени прошло с момента запуска. Для начала просто запустите программу, без параметров - заметьте, что задача срабатывает примерно каждые 250 мс, как и ожидалось:
  251
  501
  751
 1012
^C
Затем запустите программу с одним (любым) параметром командной строки. Это приведёт к срабатыванию первого случая для case, где добавляется задача "Clog". "Clog" делает, что и означает её имя ("пробка", "затор"): она тормозит очередь задач задерживая своё выполнение на длительное время (четыре секунды). Заметьте, что теперь первый вызов не происходит целую секунду:
 1001
 1011
 1021
 1021
 1252
 1502
 1752
^C
Это потому что мы поставили в очередь задачу "Clog" без указания флага WT_EXECUTELONGFUNCTION. Другими словами, мы сказали пулу потоков: "А, не волнуйся за этого парня, сейчас он всё быстро сделает". Затем пул потоков хочет запустить задачу Tick, а поскольку задача Clog была помечена как "быстрая", то он решает подождать её, чтобы повторно использовать поток, вместо создания нового. После примерно секунды у пула потоков кончается терпение, и он создаёт новый поток, чтобы обслужить скопившиеся в очереди задачи Tick.

Заметьте, что как только срабатывает первая задача Tick, также срабатывают ещё три задачи - друг за другом. Это потому что пул потоков осознал, что он не выполнил четыре задачи (спасибо затору, созданному Clog), и поэтому он выполняет их немедленно - только для того, чтобы очистить очередь задач. Пятый и дальнейшие вызовы срабатывают примерно вовремя, потому что пул потоков уже знает, что задача Clog является тормозом, и пометил её как "длительную".

Наконец, запустите программу с двумя (любыми) аргументами командной строки. Это приведёт к срабатыванию второй строки в case, где мы планируем всю ту же задачу Clog, но теперь уже указывая флаг WT_EXECUTELONGFUNCTION:
  251
  511
  761
 1012
^C
Заметьте, что с этой подсказкой, пул потоков больше не будет одурачен задачей Clog - и он значет, что ему надо создать новый поток для обработки задач Tick.

Мораль истории: если вы собираетесь работать с пулом потоков - убедитесь, что вы дружелюбны к другим задачам и уведомляете пул потоков о времени, которое вам понадобится для работы. Это позволит пулу потоков создавать минимум рабочих потоков (и, таким образом, использовать преимущества пула), но при этом обеспечивать максимально гладкое выполнение входящих задач.

Упражнение: каковы последствия для пула потоков, если вы создадите таймер пула потока, чья функция обратного вызова выполняется дольше, чем интервал этого таймера?

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.