Приостановка (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.
В принципе, вызов SuspendThread самим потоком будет безопасен с этой точки зрения. Но всё равно, лучше использовать другие механизмы синхронизации - иначе этот вызов SuspendThread можно в дальнейшем по ошибке перенести/вызвать из другого потока.
ОтветитьУдалитьА Delphi добавляет к вышеуказанной проблеме ещё и свои собственные.
ОтветитьУдалитьС новым менеджером памяти ситуацию возпроизвести также просто. Достаточно добавить GetMem и FreeMem сразу после вызова Suspend. И память выделять лучше сразу по мегабайту.
ОтветитьУдалить>>> Достаточно добавить GetMem и FreeMem сразу после вызова Suspend
ОтветитьУдалитьВообще-то именно это делает строка "WriteLn(DateTimeToStr(Now) + ': press Enter to resume')" ;)
Имелось ввиду, что FastMM как-то более хитро использует блокировки (окей, мне лень копать :) ), поэтому с ним ситуацию воспроизвести можно, но не всегда (в отличие от старого менеджера памяти).
Хе. А если нужно поставить на паузу закачку файла, причем закачка идет в потоке через создаваемый в нем idhttp ?
ОтветитьУдалитьЯ не очень знаком с Indy, но вроде там есть событие OnWork.
ОтветитьУдалитьЭто раз.
Два - это останов и перезакачка.