Держите приложение отзывчивым во время длительной задачи

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

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

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

// Обновлено: Наше текущее решение - поток. Однако это боль в заднице из-за ввода пользователя. И во многих подпрограммах может быть много входного кода. Это дает мне ощущение, что поток нет - правильное решение.

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

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;

Тебе есть что добавить?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
0
1 664
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

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

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

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

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

Развивайте и отлаживайте свою медленную функцию.

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

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

Это примерно то, что мы делаем сейчас, но из-за количества возможных взаимодействий с пользователем синхронизация действительно раздражает. :-)

Uli Gerhardt 12.01.2009 22:14

Но какое взаимодействие может произойти? обычно есть индикатор выполнения и кнопка отмены.

Bernd Ott 12.01.2009 22:24

Это зависит от обстоятельств - иногда на самом деле это просто индикатор выполнения. Иногда возникает масса вопросов: «Вы хотите, чтобы это было здесь или там?» "Вы хотите X или Y?" Или: «Внимание - вы однажды указали опцию ABC, которая вылетает из-за ваших последних изменений. Что это должно быть?»

Uli Gerhardt 12.01.2009 22:46

Мне нравятся потоки Delphi, но сказать, что «TThread идеален» - это большое преувеличение.

JosephStyons 13.01.2009 00:40

Ули, тогда тебе стоит подумать о переделке этой задачи. Позвольте потоку работать, пока не появится первый вопрос. вернитесь к потоку main-vcl, выполните заполнение формы, а затем перезапустите поток или создайте новый.

Bernd Ott 13.01.2009 11:43

* не замораживать графический интерфейс - что наиболее важно - потому что пользователь может подумать, что приложение разбилось, и, возможно, завершить процесс с помощью диспетчера задач * использовать преимущества машин с гиперпоточностью / несколькими процессорами

Bernd Ott 13.01.2009 12:08

Первого можно достичь с помощью Application.ProcessMessages (), а также проверить запрос отмены или выполнение в Application.OnIdle (). Второе неверно, если поток графического интерфейса ожидает завершения только одного рабочего потока. Также TThread.Synchronize (), порожденный из ада, убьет ...

mghie 13.01.2009 12:23

любое преимущество гиперпоточности или многоядерных машин за счет принудительного использования всего одного гигантского механизма сериализации, который также чрезвычайно подвержен ошибкам в использовании. Для получения дополнительной информации поищите «Способы избежать проблем с потоками, V1.2» в delphigroups.info/2/4/208119.html.

mghie 13.01.2009 12:27

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

Сначала я бы создал интерфейс (или абстрактный базовый класс) с такими методами, как:

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

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

procedure MyTransformation(AGuiAdapter: IModelTransformationGUIAdapter);

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

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

Редактировать:

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

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

Само преобразование нелегко распараллелить. Это действительно серия шагов, которые зависят друг от друга. Мы используем поток просто как средство, чтобы приложение оставалось отзывчивым. Глупая идея?

Uli Gerhardt 12.01.2009 22:54

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

mghie 12.01.2009 22:58

IIUYC подход GuiAdapter похож на то, что у нас уже есть. Однако мы использовали не централизованный интерфейс, а набор событий. Например. TMyModelForm.GetSomeUserInput из моей исходной публикации привязан к свойству события в модели, которое вызывается во время преобразования.

Uli Gerhardt 13.01.2009 12:51

Он действительно похож, я просто предпочитаю интерфейс, потому что он проще, проще в настройке и тестировании. Вы видите, что в вашем фрагменте у вас больше косвенности, и вам нужно написать больше связующего кода. Я бы также не использовал Synchronize (), если этого можно избежать, см. Мои комментарии к Bot.

mghie 13.01.2009 13:03

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

mghie 13.01.2009 13:45
Ответ принят как подходящий

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

Может быть, проблемы, с которыми сталкивается трансформация, - это то, что можно «накопить» до конца. Должно ли преобразование нужно знать ответы немедленно, или оно может завершить все остальное, а затем просто внести некоторые «исправления» после этого?

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

Uli Gerhardt 12.01.2009 23:01

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

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

Если вы можете разделить код преобразования на маленькие фрагменты, то вы сможете запускать этот код, когда процессор простаивает. Просто создайте обработчик событий, подключите его к событию 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;

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

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

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

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

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

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

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

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

Если вы решите использовать потоки, которые я также считаю несколько сложными из-за их реализации в Delphi, я бы порекомендовал OmniThreadLibrary Приможа Габриелчича или Габр, как его называют здесь, в Stack Overflow.

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

Другие вопросы по теме