среда, 29 июня 2011 г.

Тревожное ожидание - это не-GUI аналог для GUI-шной прокачки сообщений

Это перевод The alertable wait is the non-GUI analog to pumping messages. Автор: Реймонд Чен.

Когда вы программируете GUI приложения, вы хорошо знаете, что прокачка сообщений (message pumping) является основным способом по приёму и диспетчеризации сообщений. Невизуальным аналогом прокачки сообщений является ожидание в тревожном состоянии (alertable wait).

APC пользовательского режима (Asynchronous Procedure Call - асинхронный вызов процедуры) - это запрос запуска функции в потоке пользовательского режима. Вы можете явно поставить APC в очередь потока с помощью функции QueueUserAPC, либо же вы можете неявно передать функцию как функцию завершения (обратного вызова) для ожидаемого таймера или асинхронного ввода-вывода (вот почему флаг, который указывает, что ожидание прервано выполнением APC, называется WAIT_IO_COMPLETION: изначально единственной вещью в очередях APC могло быть только функция завершения асинхронного ввода-вывода).

Конечно же, когда вы планируете вызов APC, то функция не может быть вызвана немедленно. Представьте, на что был бы похож мир, если бы это было возможно: функция прервала бы поток на половине того, чем бы он там ни занимался - возможно, структуры данных программы при этом были в нестабильном рабочем состоянии, что означает, что APC функции придётся работать в условиях несогласованного состояния программы. Если бы APC действительно работали бы так (прим.пер.: это, кстати, несложно сделать самому вручную), то было бы невозможно написать осмысленную APC функцию, потому что она не может надёжно обращаться к любым переменным (потому что переменные могут находиться в нестабильном состоянии), не может она и вызывать любые функции, которые обращаются к этим переменным. Учитывая эти ограничения, функция может делать не так уж много вещей.

Поэтому вместо этого APC функции диспетчеризируются когда вы выполняете так называемое "ожидание в тревожном состоянии" (alertable wait). "Ex"-варианты большинства функций ожидания (к примеру, WaitForSingleObjectEx, WaitForMultipleObjectsEx, MsgWaitForMultipleObjectsEx и SleepEx) позволяют вам указать, не хотите ли вы ждать в тревожном состоянии. Если вы включаете этот режим и в APC-очереди потока появляются одна или несколько APC-функций, то операция ожидания завершается раньше времени, возвращая код, указывающий, что ожидание было прервано выполнением APC. Если APC, выполнения которых вы ждали, ещё не выполнены (скажем, вы выполнили чей-то сторонний APC-запрос), то перезапустить ожидание будет вашей ответственностью.

Почему операционная система сама не перезапускает ожидание автоматически? "Представьте себе мир, в котором это было бы так": предположим, вы запускаете асинхронный ввод-вывод, а параллельно делаете ещё что-то, а затем ждёте завершения ввода-вывода, чтобы использовать результат:
// Когда завершится асинхронное чтение, мы запустим
// следующий запрос на чтение. Когда закончим - установим fCompleted.
var
  fCompleted: Boolean = False;
  fSuccess: Boolean;

procedure CompletionRoutine(dwErrorCode, dwNumberOfBytesTransfered: DWORD; Overlapped: LPOverlapped); stdcall;
begin
  if { закончили } then
  begin
    fSuccess := True;
    fCompleted := True;
  end
  else 
  begin
    // Запускаем следующий запрос в последовательности
    if not ReadFileEx(hFile, ..., @CompletionRoutine) then
    begin
      fSuccess := False;  // возникла проблема
      fCompleted := True; // не можем продолжать при проблеме
    end;
  end;
end;

...

  // Запускаем чтение и выполняем другую работу
  if ReadFileEx(hFile, ..., @CompletionRoutine) then
    DoOtherStuffInTheMeantime;
  { ждём установки fCompleted }
  DoStuffWithResult;
Как бы вы написали действие "ждём установки fCompleted", если бы операционная система перезапускала ожидание? Если вы запустите бесконечное ожидание в тревожном состоянии (скажем, через SleepEx(INFINITE, True)), то APC отработают, ОС перезапустит ожидание и sleep будет выполняться вечно. Вам пришлось бы использовать конечный (небольшой) интервал ожидания и постоянно проверять результаты ожидания (poll). Но у этого подхода есть два серьёзных недостатка: во-первых, опрос - это очень плохо. Во-вторых, скорость, с которой вы производите опрос, напрямую влияет на скорость, с которой ваша программа реагирует на завершение чтений в цепочке. Более высокая частота опроса даёт вам лучшую отзывчивость, но также и тратит больше CPU времени впустую.

К счастью, ожидание не перезапускается автоматически. Это даёт вам шанс самому решить, хотите ли вы делать перезапуск или нет:
...
  // Запускаем чтение и выполняем другую работу
  if ReadFileEx(hFile, ..., @CompletionRoutine) then
    DoOtherStuffInTheMeantime;
  while not fCompleted do
    SleepEx(INFINITE, True);
  DoStuffWithResult;
Цикл со SleepEx ждёт бесконечно, параллельно обрабатывая APC, пока функция обратного вызова не решит, что работа выполнена и установит флаг fCompleted.

2 комментария:

  1. Если бы APC действительно работали бы так (прим.пер.: это, кстати, несложно сделать самому вручную)
    Помоему писать драйвер довольно сложно.

    ОтветитьУдалить
  2. Имелось в виду вызывать функцию в потоке в любой момент времени. Для этого не нужен драйвер, достаточно функций смены контекста потока.

    ОтветитьУдалить

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

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

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

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

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

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