Sources
Delphi Russian Knowledge Base
DRKB - это самая большая и удобная в использовании база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Блокирующий режим против неблокирующего

01.01.2007

5. Блокирующий режим против неблокирующего

5.1. Модели программирования

В Windows есть две модели программирования сокетов – блокирующий и неблокирующий. Иногда они также называются как – синхронный (blocking) и асинхронный (non-blocking). В данном документе мы будем использовать термины блокирующий и неблокирующий.

На платформе Unix, поддержан только блокирующий режим.

5.2. Другие модели

В действительности есть еще несколько реализованных моделей. Это completion ports, and overlapped I/O. Но использование этих моделей требует гораздо больше кода и обычно используется только в очень сложных серверных приложениях.

В дополнение, данные модели не кросс платформенные и их реализация сильно отличает от одной операционной системы к другой.

Indy 10 содержит поддержку и этих моделей.

5.3. Блокирующий режим

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

С Indy, вы просто вызываете метод Connect и просто ждете возврата из него. Если соединение будет успешное, то будет возврат из метода по окончанию соединения. Если же соединение не произойдет, то будет возбуждено исключение.

5.4. Неблокирующий режим

Работа неблокирующих сокетов основана на системных событиях. После того как произведен вызов, будет возбуждено событие.

Например, для попытки соединения сокета, вы должны вызвать метод Connect. Данный метод немедленно возвращает управление в программу. Когда сокет будет подсоединен, то будет возбуждено событие. Это требует, что бы логика связи была разделена по многим процедурам или использовать циклы опроса.

5.5. История Winsock

В начале был Unix. Это был Berkely Unix. Он имел стандартное API для поддержки сокетов. Это API было адоптировано в большинстве Unix систем.

Затем появился Windows, и кто-то посчитал, что это хорошая идея иметь возможность программировать TCP/IP и в Windows. Поэтому они портировали API Unix сокетов. Это позволило большинство Unix кода с легкостью портировать и в Windows.

5.6. Блокирующий режим это не смертельно

Из-за блокирующего режима мы неоднократно были биты нашими противниками, но блокирующий режим не является дьяволом.

Когда API Unix сокетов было портировано в Windows, то ему дали имя Winsock. Это сокращение от "Windows Sockets".

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

Windows 3.x не мог распараллеливаться и плохо поддерживал многозадачность. Windows 3.1 также не имел поддержки потоков. Использование блокирующих сокетов замораживало пользовательский интерфейс и делало программы не реагирующими. Поскольку это было не приемлемо, то к WinSock были добавлены неблокирующие сокеты, позволяя Windows 3.x с его ограничениями использовать Winsock без замораживания всей системы. Это потребовало другого программирования сокетов, Microsoft и другие страстно поносили блокирующие режимы, что бы скрыть недостатки Windows 3.x.

Затем пришли  Windows NT и Windows 95, Windows стала поддержать вытесняющую многозадачность и потоки. Но к этому моменту мозги уже были запудрены (то есть разработчики считали блокирующие сокеты порождением дьявола), и уже было тяжело изменить содеянное. По этому поношение блокирующих режимов продолжается.

В действительности, блокирующее API единственное которое поддерживает Unix.

Некоторые расширения, для поддержки неблокирующих сокетов были добавлены и в Unix. Эти расширения работают совсем не так как в Windows. Эти расширения не стандартны для всех Unix платформ и не используются широко. Блокирующие сокеты в Unix все еще используются в каждом приложении и будут продолжаться использоваться и дальше.

Блокирующие сокеты также имеют и другие преимущества. Блокирующие сокеты много лучше для поточности, безопасности и по другим аспектам.

5.7. Достоинства блокирующего режима

1.Проще программировать - Блокирующие сокеты проще программировать. Весь пользовательский код может находиться в одном месте и выполняться в естественном, последовательном порядке.
2.Кросс-платформенность – поскольку Unix использует блокирующие сокеты, по переносимый код легче писать. Indy использует данный факт для использования своего кода между платформами. Другие сокет компоненты, которые кросс платформенные, на самом деле эмулируют это с помощью внутреннего вызова блокирующих сокетов.
3.Удобнее работать с потоками - Поскольку у блокирующих сокетов последовательность приобретена по наследственности, поэтому их очень просто использовать в потоках.
4.Независимость от сообщений – неблокирующие сокеты зависят от системы оконных сообщений. Когда используются потоки, то создается отдельная очередь сообщений. Но когда потоки не используются, то узким местом становится обработка множества соединений.
5.8. Недостатки блокирующего режима

1.Пользовательский интерфейс замораживается в клиентах - Вызов блокирующего сокета не возвращает управления, пока не выполнит свою задачу. Когда подобные вызовы делаются в главном кодовом потоке, то приложение замораживает пользовательский интерфейс. Замораживание происходит, поскольку сообщения обновления, перерисовки и другие сообщения не обрабатываются до окончания вызова блокирующего сокета.
5.9. Компонент TIdAntiFreeze

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

Использование TIdAntiFreeze позволяет получить все преимущества блокирующих сокетов, без видимых недостатков.

5.10. Достоинства неблокирующего режима

1.Пользовательский интерфейс не замораживается – поскольку пользовательский код обрабатывает оконные сообщения, то имеет контроль и над сокетными сообщениями. Поэтому Windows также может обрабатывать и другие сообщения.
2.Многозадачность без использования потоков – используется единственный кодовый поток для обработки множества сокетов.
3.Очень малая нагрузки при множестве сокетов – поскольку множество сокетов могут обрабатываться без потоков, то нагрузка на память и процессор значительно ниже.
5.11. Недостатки неблокирующего режима

1.Более сложное программирование – неблокирующие сокеты требуют использования опроса или обработки событий. События наиболее используемый метод, а циклы опроса менее эффективны. При использовании обработчиков событий, код размазан по куче процедур, поэтому требуется отслеживание состояния. Это означает большее количество ошибок и более сложная модификация кода.
5.12. Сравнение технологий

Если вы хорошо знаете Indy и его методологию, то вы можете пропустить эту главу. Но даже если вы ранее программировали сокеты, до использования Indy, то все равно данная глава будет вам полезна.

Для тех, кто никогда не программировал сокеты до Indy, то будет легко и естественно использовать его. Но для тех кто программировал сокеты ранее, Indy будет камнем преткновения. Поскольку Indy работает совсем по другому. Попытка программировать в Indy тем же самым образом. Это не означает, что другие решения неправильные, просто Indy работает иначе. Пытаться программировать в Indy так же, как с другими сокетными библиотеками, равносильна попытке приготовить пирожное в микроволновой печи, как в духовке. Результатом будет испорченное пирожное.

Если вы использовали другие сокетные библиотеки ранее, пожалуйста следуйте следующему девизу:

Забудьте все, что вы знали раньше!

Это легко сказать, труднее сделать, менять привычки тяжело. Чтобы подчеркнуть разницу, приведем абстрактный пример. Для абстрагирования концепции, используем в качестве аналога файлы. Данный документ подразумевает, что вы умеете работать с файлам. Надеемся, что Бейсик программисты не читают эту книгу.

5.13. Файлы против сокетов

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

5.14. Сценарий записи в файл

Представим простой сценарий записи в файл. Поскольку данная процедура очень простая, то она очень подходит демонстрации.

1.Открыть файл
2.Записать данные
3.Закрыть файл
5.15. Блокирующий режим записи файла

Блокирующая запись в файл выглядит следующим образом:

procedure TForm1.Button1Click(Sender: TObject);
var
  s: string;
begin
  s := 'Indy Rules the (Kudzu) World !' + #13#10;
  try
    // Open the file
    with TFileStream.Create('c:\temp\test.dat', fmCreate) do 
    try
      // Write data to the file
      WriteBuffer(s[1], Length(s));
      // Close the file
    finally 
      Free; 
    end;
  end;
end;
 

Как вы видите, это практически повторяет приведенный выше псевдокод. Код последовательный  и легкий для понимания.

5.16. Неблокирующий режим записи файла

Не существует такой вещи как неблокирующий режим записи файла (может быть исключая overlapped I/O, но это за пределами данной книги), но здесь мы можем просто эмулировать механизм это. File1 это условный неблокирующий компонент, размещенной на форме.

procedure TForm1.Button1Click(Sender: TObject);
begin
  File1.Filename := 'd:\temp\test.dat';
  File1.Open;
end;
procedure TForm1.File1OnOpen(Sender: TObject);
var
  i: integer;
begin
  FWriteData := 'Hello World!' + #13#10;
  i := File1.Write(FWriteData);
  Delete(FWriteData, 1, i);
end;
procedure TForm1.File1OnWrite(Sender: TObject);
var
  i: integer;
begin
  i := File1.Write(FWriteData);
  Delete(FWriteData, 1, i);
  if Length(FWriteData) = 0 then 
  begin
    File1.Close;
  end;
end;
procedure TForm1.File1OnClose(Sender: TObject);
begin
  Button1.Enabled := True;
end;
 

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

1.При вызове Button1Click открывается файл. Метод Open немедленно вернет управление в программу, но файл еще не открыт и нельзя с ним еще нельзя работать.
2.Обработчик события OnOpen будет возбужден, когда файл будет открыть и готов к работе. Делается попытка записать данные в файл, но все данные еще не акцептированы. Метод Write вернет количество записанных байт. Оставшиеся данные будут сохранены позже.
3.Обработчик события OnWrite будет возбужден, когда файл будет готов воспринять следующую порцию данных, и метод Write будет повторяться для оставшихся данных.
4.Шаг 3 повторяется до тех пор, пока все данные не будут записаны методом Write. По окончанию записи всех данных вызывается метод Close. Но файл пока еще не закрыт.
5.The OnClose event is fired. The file is now closed.
5.17. Сравнение записи файлов

Оба примера только записывают данные. Чтение и запись данных будут сложнее для неблокирующего режима, но только добавлением одной строки для блокирующего режима.

Для блокирующего примера, просто откройте, записывайте данные, и закройте файл когда необходимо:

·3 File1 события
·1 поле в Form
Неблокирующая версия более сложная и более тяжелая для понимания. Дадим шанс выбора между обеими, если надо будет выбирать, то большинство выберет неблокирующий путь. Большинство C++ программистов, исключая конечно просто мазохистов или вообще не будет выбирать, поскольку все они почти просты. Почти все сокетные функции используют неблокирующий режим.

5.18. Почти как файлы

Использование Indy почти равносильно использованию файлов. В действительности Indy еще проще, поскольку Indy имеет ряд методов для чтения и записи. Indy пример, эквивалентный примеру с файлами выглядит так:

with IndyClient do 
begin
     Connect; 
     Try
       WriteLn('Hello World.');
     finally 
       Disconnect; 
     end;
end;
Как вы можете видеть, Indy в действительности очень похож работе с файлами. Метод Connect замещает функцию Open, а метод Disconnect замещает функцию Close. Если вы думаете и признаете сокеты как чтение и запись в файл, то вам будет использовать Indy очень просто.