понедельник, 1 декабря 2008 г.

Почему вам никогда не следует приостанавливать поток

Это перевод Why you should never suspend a thread. Автор: Реймон Чен.

Приостановка (suspend) потока почти также плоха, как его уничтожение (terminate).

Вместо того, чтобы ответить на вопрос, я собираюсь задать вам несколько вопросов и посмотреть, какие ответы вы сможете на них дать.

Рассмотрим следующую программу (*):
program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils, Classes;

type
  TTestThread = class(TThread)
  protected
    procedure Execute; override;
  end;

{ TTestThread }

procedure TTestThread.Execute;
var
  P: Pointer;
begin
  while not Terminated do
  begin
    GetMem(P, 1024); FreeMem(P);
  end;
end;

var
  Thread: TTestThread;
begin
  try
    Thread := TTestThread.Create(False);
    try
      WriteLn(DateTimeToStr(Now) + ': press Enter to suspend');
      ReadLn;
      Thread.Suspend;
      WriteLn(DateTimeToStr(Now) + ': press Enter to resume');
      ReadLn;
      Thread.Resume;
    finally
      FreeAndNil(Thread);
    end;
  except
    on E: Exception do
      WriteLn(E.Classname, ': ', E.Message);
  end;
end.
Когда вы запускаете эту программу и нажимаете Enter для приостановки, программа виснет. Но если вы измените метод Execute на пустой бесконечный цикл (закомментарить строчку с GetMem/FreeMem), то программа работает отлично. Посмотрим, сумеете ли вы понять почему.

Рабочий поток проводит почти всё своё время внутри вызовов функций менеджера памяти - GetMem/FreeMem, поэтому когда вы вызываете Thread.Suspend, рабочий поток почти обязательно будет внутри вызова одной из функций менеджера памяти.

Q: Являются ли вызовы функций менеджера памяти потоко-безопасными?

Окей, на этот вопрос отвечу я: да, являются (**). Мне даже не нужно просматривать документацию, чтобы это сообразить. Эта программа работает с памятью (GetMem и строки в WriteLn) безо всякой синхронизации. Везде в своих программах мы свободно используем строковые переменные без синхронизации, вне зависимости от потоковой модели, так что им лучше бы быть потоко-безопасными, или у нас будут большие проблемы ещё до того, как мы дойдём до вопроса приостаноки потока.

Q: Как обычно объект делают потоко-безопасным?

Q: Каков будет результат приостановки потока в середине потоко-безопасной операции?

Q: Что случится, если впоследствии вы попытаетесь обратиться к тому же объекту (менеджеру памяти в нашем случае) из другого потока?

Эти результаты применимы не только к Delphi, но и к любой другой модели потоков, в том числе в C#. К примеру, в чистом Win32 куча процесса является потоко-безопасным объектом, а поскольку без кучи в Win32 весьма тяжело делать какую-либо полезную работу, то приостановка потока в Win32 имеет высокий шанс блокировки вашего процесса.

Тогда почему вообще есть такая функция как SuspendThread?

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

(*) Примечание: этот пример для старых Delphi (например, версии 7). В новых Delphi используется другой менеджер памяти FastMM, который по-другому производит блокировку, так, что поток проводит значительно меньше времени внутри блокировки. Поэтому с новым менеджером памяти такую ситуацию воспроизвести сложнее. Кстати, в оригинальном посте Реймонда здесь стоял пример как раз на C#, а основным защищаемым объектом являлась консоль.
(**) Примечание: на самом деле, в Delphi с целью экономии ресурсов менеджер памяти переходит в потоко-безопасный режим только при создании дополнительных потоков. Это позволяет экономить ресурсы в однопоточных приложениях, которых, пока, большинство. Создание потока классом TThread или с помощью BeginThread автоматически переводит менеджер памяти в потоко-безопасный режим. Для всех других способов создания потока (например, CreateThread), вам нужно руками включить потоко-безопасный режим, установив переменную IsMultiThread.

6 комментариев:

  1. В принципе, вызов SuspendThread самим потоком будет безопасен с этой точки зрения. Но всё равно, лучше использовать другие механизмы синхронизации - иначе этот вызов SuspendThread можно в дальнейшем по ошибке перенести/вызвать из другого потока.

    ОтветитьУдалить
  2. А Delphi добавляет к вышеуказанной проблеме ещё и свои собственные.

    ОтветитьУдалить
  3. С новым менеджером памяти ситуацию возпроизвести также просто. Достаточно добавить GetMem и FreeMem сразу после вызова Suspend. И память выделять лучше сразу по мегабайту.

    ОтветитьУдалить
  4. >>> Достаточно добавить GetMem и FreeMem сразу после вызова Suspend
    Вообще-то именно это делает строка "WriteLn(DateTimeToStr(Now) + ': press Enter to resume')" ;)

    Имелось ввиду, что FastMM как-то более хитро использует блокировки (окей, мне лень копать :) ), поэтому с ним ситуацию воспроизвести можно, но не всегда (в отличие от старого менеджера памяти).

    ОтветитьУдалить
  5. Хе. А если нужно поставить на паузу закачку файла, причем закачка идет в потоке через создаваемый в нем idhttp ?

    ОтветитьУдалить
  6. Я не очень знаком с Indy, но вроде там есть событие OnWork.

    Это раз.

    Два - это останов и перезакачка.

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

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

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

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

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

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

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