Синхронизация TMonitor / Application.ProcessMessages - PullRequest
2 голосов
/ 20 февраля 2009

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

TMonitor.Enter (FTCPClient);
try
  WorkerThread := TWorkerThread.Create (SomeLengthyServerOperation);
  while (not WorkerThread.Ready) do
    Application.ProcessMessages;
  DoSometingWithResults (WorkerThread.Result);
  WorkerThread.Free;      
finally
  TMonitor.Exit (FTCPClient);
end;

WorkerThread - это простой класс, производный от TThread, который выполняет функцию, переданную его конструктору, а затем завершается (с Ready = True и результатом в Result). Представленный код выполняется при каждом нажатии кнопки.

Теперь к моему вопросу: если я нажимаю кнопку дважды очень быстро, я получаю некоторые странные ошибки, которые выглядят так, как будто связь между сервером и клиентом каким-то образом искажена, чего я хотел избежать, блокируя объект FTCPClient. В каком потоке обрабатываются обработчики событий после Application.ProcessMessages? Является ли блокировка TMonitor на поток? Означает ли это, что блокировка не работает, если я использую Application.ProcessMessages?

Я не могу объяснить это лучше в данный момент. Я надеюсь, что кто-то поймет мою точку зрения. Если нет, не стесняйтесь задавать вопросы.

РЕДАКТИРОВАТЬ: Для отключения и включения кнопки: я ничего не знаю о клиентском коде. Может быть обработчик события кнопки, может быть что-то еще. В основном я хочу скрыть блокировку от клиентского кода.

Ответы [ 2 ]

7 голосов
/ 20 февраля 2009

TMonitor блокирует только другой поток от получения блокировки. Происходит следующее: обрабатывая сообщения изнутри блокировки, вы возвращаетесь к той же функции в том же потоке, что вызывает рекурсивное получение блокировки. Затем ваш код создает новый рабочий поток и начинает цикл заново. Вы можете отключить кнопку, чтобы не нажимать ее снова, пока рабочий поток не завершится. Обязательно отключите кнопку до , начните обрабатывать сообщения и используйте еще один блок try..finally, чтобы обеспечить его повторное включение. В зависимости от того, как устроен остальной код, вам может даже не понадобиться блокировка.

3 голосов
/ 20 февраля 2009

Некоторые комментарии:

  1. Ваш WorkerThread звучит так, как будто вы только что реализовали AsyncCalls . Использование проверенной и проверенной реализации, вероятно, лучше, чем написание своей собственной (если вы не делаете это для эффекта обучения или у вас есть особые требования). Пожалуйста, ознакомьтесь с AsyncCalls и OmniThreadLibrary .

  2. Блокировка должна происходить как можно короче, поэтому оборачивать всю реакцию на нажатие кнопки в Monitor.Enter () и Monitor.Exit () кажется неправильным. Также не совсем понятно, для чего это нужно.

  3. Вызов Application.ProcessMessages () при удержании блокировки может привести к всевозможным неприятным сюрпризам. Если вам нужно предотвратить повторный ввод кода, обычно лучше отключить все элементы пользовательского интерфейса, как первое, что делает обработчик OnClick, и повторно включить их, когда обработчик будет завершен. Также обратите внимание, что блокировки могут вводиться несколько раз из одного потока, они получают эксклюзивный доступ только из нескольких потоков.

  4. Весь VCL выполняется в основном потоке графического интерфейса, поэтому блокировка необходима только при вызове одного и того же кода из фоновых потоков.

  5. Если вы посмотрите на этот код, вы увидите, что вы можете достичь того же самого, выполняя свою работу в потоке графического интерфейса вместо порождения рабочего потока.

Я уже несколько раз публиковал эту ссылку в StackOverflow, но, пожалуйста, следуйте рекомендациям в этом списке, публикующем , о некоторых вещах, которые следует учитывать при выполнении многопоточного программирования. У него есть несколько полезных советов, особенно в отношении пятого пункта.

Редактировать: Трудно сказать точно, но ваш код, вероятно, должен выглядеть примерно так:

procedure TForm1.ActionStartExecute(Sender: TObject);
begin
  ActionStart.Enabled := FALSE;
  fWorkerThread := TWorkerThread.Create (Handle, SomeLengthyServerOperation);
end;

procedure TForm1.ActionStartUpdate(Sender: TObject);
begin
  ActionStart.Enabled := fWorkerThread = nil;
end;

procedure TForm1.WMThreadFinished(var AMsg: TWMThreadFinishedMsg);
begin
  // process results
  fWorkerThread := nil;
end;

TWorkerThread освобождает себя, но в качестве последнего действия он отправляет сообщение в форму (именно поэтому он получает дескриптор окна в качестве параметра). В обработчике этого сообщения вы устанавливаете для fWorkerThread значение nil, чтобы действие снова включалось в следующем цикле простоя. Использование TAction означает, что вам не нужно заботиться о том, какой элемент пользовательского интерфейса инициировал создание потока - все они будут отключены при создании потока и будут снова включены после завершения потока. Блокировка не требуется, и новый поток не может быть создан, пока он активен.

...