Keep app responsive during long task

后端 未结 10 2099
逝去的感伤
逝去的感伤 2020-12-09 13:57

A certain form in our application displays a graphical view of a model. The user can, amongst loads of other stuff, initiate a transformation of the model that can take quit

相关标签:
10条回答
  • 2020-12-09 14:25

    For an optimal solution you will have to analyse your code anyway, and find all the places to check whether the user wants to cancel the long-running operation. This is true both for a simple procedure and a threaded solution - you want the action to finish after a few tenths of a second to have your program appear responsive to the user.

    Now what I would do first is to create an interface (or abstract base class) with methods like:

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

    and change the procedure to have a parameter of this interface or class:

    procedure MyTransformation(AGuiAdapter: IModelTransformationGUIAdapter);
    

    Now you are prepared to implement things in a background thread or directly in the main GUI thread, the transformation code itself will not need to be changed, once you have added code to update the progress and check for a cancel request. You only implement the interface in different ways.

    I would definitely go without a worker thread, especially if you want to disable the GUI anyway. To make use of multiple processor cores you can always find parts of the transformation process that are relatively separated and process them in their own worker threads. This will give you much better throughput than a single worker thread, and it is easy to accomplish using AsyncCalls. Just start as many of them in parallel as you have processor cores.

    Edit:

    IMO this answer by Rob Kennedy is the most insightful yet, as it does not focus on the details of the implementation, but on the best experience for the user. This is surely the thing your program should be optimised for.

    If there really is no way to either get all information before the transformation is started, or to run it and patch some things up later, then you still have the opportunity to make the computer do more work so that the user has a better experience. I see from your various comments that the transformation process has a lot of points where the execution branches depending on user input. One example that comes to mind is a point where the user has to choose between two alternatives (like horizontal or vertical direction) - you could simply use AsyncCalls to initiate both transformations, and there are chances that the moment the user has chosen his alternative both results are already calculated, so you can simply present the next input dialog. This would better utilise multi-core machines. Maybe an idea to follow up on.

    0 讨论(0)
  • 2020-12-09 14:26

    If you decide going with Threads, which I also find somewhat complex the way they are implemented in Delphi, I would recommend the OmniThreadLibrary by Primož Gabrijelčič or Gabr as he is known here at Stack Overflow.

    It is the simplest to use threading library I know of. Gabr writes great stuff.

    0 讨论(0)
  • 2020-12-09 14:31

    Process asynchronously by sending a message to a queue and have the listener do the processing. The controller sends an ACK message to the user that says "We've received your request for processing. Please check back later for results." Give the user a mailbox or link to check back and see how things are progressing.

    0 讨论(0)
  • 2020-12-09 14:36

    I think your folly is thinking of the transformation as a single task. If user input is required as part of the calculation and the input asked for depends on the caclulation up to that point, then I would refactor the single task into a number of tasks.

    You can then run a task, ask for user input, run the next task, ask for more input, run the next task, etc.

    If you model the process as a workflow, it should become clear what tasks, decisions and user input is required.

    I would run each task in a background thread to keep the user interface interactive, but without all the marshaling issues.

    0 讨论(0)
  • 2020-12-09 14:36

    While I don't completely understand what your trying to do, what I can offer is my view on a possible solution. My understanding is that you have a series of n things to do, and along the way decisions on one could cause one or more different things to be added to the "transformation". If this is the case, then I would attempt to separate (as much as possible) the GUI and decisions from the actual work that needs to be done. When the user kicks off the "transformation" I would (not in a thread yet) loop through each of the necessary decisions but not performing any work...just asking the questions required to do the work and then pushing the step along with the parameters into a list.

    When the last question is done, spawn your thread passing it the list of steps to run along with the parameters. The advantage of this method is you can show a progress bar of 1 of n items to give the user an idea of how long it might take when they come back after getting their coffee.

    0 讨论(0)
  • 2020-12-09 14:36

    If you can split your transformation code into little chunks, then you can run that code when the processor is idle. Just create an event handler, hook it up to the Application.OnIdle event. As long as you make sure that each chunk of code is fairly short (the amount of time you want the application to be unresponsive...say 1/2 a second. The important thing is to set the done flag to false at the end of your handler :

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

    So for example if you have a loop, instead of using a for loop, use a while loop, make sure the scope of the loop variable is at the form level. Set it to zero before setting the onIdle event, then for example perform 10 loops per onidle hit until you hit the end of the loop.

    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;
    
    0 讨论(0)
提交回复
热议问题