Держите приложение отзывчивым во время долгой задачи - PullRequest
4 голосов
/ 12 января 2009

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

Возможные подходы:

  1. Игнорировать проблему, просто поместите код преобразования в процедуру и вызовите ее. Плохо, потому что приложение кажется зависшим в тех случаях, когда преобразование требует некоторого времени, но не требует ввода данных пользователем.
  2. Обсыпайте код обратными вызовами: это навязчиво - вы должны поместить лот этих вызовов в код преобразования - а также непредсказуемо - вы никогда не сможете быть уверены, что нашли правильные места.
  3. Посыпать код с помощью Application.ProcessMessages: те же проблемы, что и с обратными вызовами. Кроме того, вы получаете все проблемы с ProcessMessages.
  4. Использовать поток: это освобождает нас от «навязчивой и непредсказуемой» части 2. и 3. Однако это большая работа из-за «сортировки», которая необходима для ввода пользователя - вызовите Synchronize, вставьте любой необходимые параметры в индивидуальных записях и т. д. Это также кошмар для отладки и подвержен ошибкам.

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

Я собираюсь опозориться и опубликую набросок нечестивого сочетания GUI и рабочего кода, который я произвел:

type
  // Helper type to get the parameters into the Synchronize'd routine:
  PGetSomeUserInputInfo = ^TGetSomeUserInputInfo;
  TGetSomeUserInputInfo = record
    FMyModelForm: TMyModelForm;
    FModel: TMyModel;
    // lots of in- and output parameters
    FResult: Boolean;
  end;

{ TMyThread }

function TMyThread.GetSomeUserInput(AMyModelForm: TMyModelForm;
  AModel: TMyModel; (* the same parameters as in TGetSomeUserInputInfo *)): Boolean;
var
  GSUII: TGetSomeUserInputInfo;
begin
  GSUII.FMyModelForm := AMyModelForm;
  GSUII.FModel := AModel;
  // Set the input parameters in GSUII

  FpCallbackParams := @GSUII; // FpCallbackParams is a Pointer field in TMyThread
  Synchronize(DelegateGetSomeUserInput);
  // Read the output parameters from GSUII
  Result := GSUII.FResult;
end;

procedure TMyThread.DelegateGetSomeUserInput;
begin
  with PGetSomeUserInputInfo(FpCallbackParams)^ do
    FResult := FMyModelForm.DoGetSomeUserInput(FModel, (* the params go here *));
end;

{ TMyModelForm }

function TMyModelForm.DoGetSomeUserInput(Sender: TMyModel; (* and here *)): Boolean;
begin
  // Show the dialog
end;

function TMyModelForm.GetSomeUserInput(Sender: TMyModel; (* the params again *)): Boolean;
begin
  // The input can be necessary in different situations - some within a thread, some not.
  if Assigned(FMyThread) then
    Result := FMyThread.GetSomeUserInput(Self, Sender, (* the params *))
  else
    Result := DoGetSomeUserInput(Sender, (* the params *));
end;

Есть ли у вас какие-либо комментарии?

Ответы [ 10 ]

7 голосов
/ 12 января 2009

Я думаю, что пока ваши длительные преобразования требуют взаимодействия с пользователем, вы не будете по-настоящему довольны ни одним ответом, который получите. Итак, давайте вернемся на минуту: зачем вам прерывать преобразование запросами на дополнительную информацию? Это действительно те вопросы, которые вы не могли предвидеть до начала трансформации? Конечно, пользователи не слишком довольны перерывами, верно? Они не могут просто начать преобразование, а затем пойти выпить чашку кофе; им нужно сидеть и наблюдать за индикатором прогресса в случае возникновения проблемы. Тьфу.

Может быть, проблемы, с которыми сталкивается трансформация, - это вещи, которые можно было бы "сохранить" до конца. Нужно ли преобразованию немедленно узнать ответы, или же оно может закончить все остальное, а потом просто сделать некоторые исправления?

4 голосов
/ 12 января 2009

Определенно выберите вариант с резьбой ( даже после редактирования, говоря, что вы считаете его сложным). Решение, которое предлагает duffymo , на мой взгляд, очень плохой дизайн пользовательского интерфейса (хотя это явно не внешний вид, а способ взаимодействия пользователя с вашим приложением). Программы, которые делают это, раздражают, потому что вы не представляете, сколько времени займет задание, когда оно будет выполнено и т. Д. Единственный способ улучшить этот подход - пометить результаты датой / временем генерации, но даже затем вам нужно, чтобы пользователь запомнил, когда он запустил процесс.

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

3 голосов
/ 12 января 2009

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

Теперь, что я хотел бы сделать в первую очередь, это создать интерфейс (или абстрактный базовый класс) с такими методами, как:

IModelTransformationGUIAdapter = interface
  function isCanceled: boolean;
  procedure setProgress(AStep: integer; AProgress, AProgressMax: integer);
  procedure getUserInput1(...);
  ....
end;

и измените процедуру, чтобы иметь параметр этого интерфейса или класса:

procedure MyTransformation(AGuiAdapter: IModelTransformationGUIAdapter);

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

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

Edit:

IMO этот ответ Роба Кеннеди является самым проницательным из всех, поскольку он не фокусируется на деталях реализации, а на лучшем опыте для пользователя. Это то, для чего ваша программа должна быть оптимизирована.

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

2 голосов
/ 13 января 2009

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

Затем вы можете запустить задачу, запросить ввод данных пользователем, выполнить следующую задачу, запросить дополнительную информацию, выполнить следующую задачу и т. Д.

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

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

2 голосов
/ 12 января 2009

TThread идеально подходит и прост в использовании.

Разработка и отладка медленной функции.

если он готов, поместите вызов в метод выполнения tthread. Используйте событие onThreadTerminate, чтобы выяснить конец вашей функции.

для обратной связи с пользователем используйте syncronize!

1 голос
/ 14 января 2009

Если вы решите использовать потоки, которые я также нахожу несколько сложными при их реализации в Delphi, я бы порекомендовал OmniThreadLibrary от Primož Gabrijelčič или Gabr , как он известен здесь, в переполнении стека.

Это самая простая в использовании библиотека потоков, которую я знаю. Габр пишет отличные вещи.

1 голос
/ 13 января 2009

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

Итак, для этого я использую различные переменные в потоке, к которым обращаются процедуры Get / Set, использующие критические секции, для хранения информации о состоянии. Для начала, я бы хотел установить свойство «Отменено» для графического интерфейса, чтобы попросить поток остановить, пожалуйста. Затем свойство «Status», которое указывает, ожидает ли поток, занят или завершен. У вас может быть статус «человекочитаемый», чтобы указать, что происходит, или процент выполнения.

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

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

1 голос
/ 13 января 2009

Хотя я не совсем понимаю, что вы пытаетесь сделать, я могу предложить свой взгляд на возможное решение. Насколько я понимаю, у вас есть ряд n вещей, которые можно сделать, и решения по одному из них могут привести к добавлению одной или нескольких разных вещей к «трансформации». Если это так, то я бы попытался отделить (насколько это возможно) графический интерфейс и решения от фактической работы, которая должна быть выполнена. Когда пользователь запускает «преобразование», я (пока не в потоке) перебираю каждое из необходимых решений, но не выполняю какую-либо работу ... просто задаю вопросы, необходимые для выполнения работы, а затем нажимаю на шаг вместе с параметры в список.

Когда последний вопрос будет задан, создайте поток, передав ему список шагов для выполнения вместе с параметрами. Преимущество этого метода в том, что вы можете показать индикатор выполнения из 1 из n элементов, чтобы дать пользователю представление о том, сколько времени может потребоваться, когда он вернется после получения кофе.

1 голос
/ 12 января 2009

Обрабатывайте асинхронно, отправляя сообщение в очередь, и обработчик выполняет обработчик. Контроллер отправляет ACK-сообщение пользователю, в котором говорится: «Мы получили ваш запрос на обработку. Пожалуйста, проверьте результаты позже». Дайте пользователю почтовый ящик или ссылку, чтобы проверить и посмотреть, как идут дела.

0 голосов
/ 13 января 2009

Если вы можете разбить ваш код преобразования на маленькие куски, то вы можете запустить этот код, когда процессор простаивает. Просто создайте обработчик события, подключите его к событию Application.OnIdle. До тех пор, пока вы убедитесь, что каждый фрагмент кода достаточно короткий (количество времени, которое вы хотите, чтобы приложение не отвечало ... скажем, 1/2 секунды. Важно, чтобы в конце для флага done было установлено значение false вашего обработчика:

procedure TMyForm .IdleEventHandler(Sender: TObject;
  var Done: Boolean);
begin
  {Do a small bit of work here}
  Done := false;
end;

Так, например, если у вас есть цикл, вместо цикла for используйте цикл while, убедитесь, что область видимости переменной цикла находится на уровне формы. Установите его в ноль перед установкой события onIdle, затем, например, выполняйте 10 циклов на одно попадание, пока вы не достигнете конца цикла.

Count := 0;
Application.OnIdle := IdleEventHandler;

...
...

procedure TMyForm .IdleEventHandler(Sender: TObject;
  var Done: Boolean);
var
  LocalCount : Integer;
begin
  LocalCount := 0;

  while (Count < MaxCount) and (Count < 10) do
  begin
    {Do a small bit of work here}
    Inc(Count);
    Inc(LocalCount);
  end;
  Done := false;
end;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...