Home > database >  How can I queue code to be run without interrupting currently-executing code?
How can I queue code to be run without interrupting currently-executing code?

Time:11-12

I'm writing a function that does some work to provide a result to the caller; but once this work is done there exists some followup work that is not required by the caller but is known is going to be useful in the future. I'd like this followup work to be executed when the MATLAB process is no longer busy doing what is immediately required of it so it doesn't slow down, or worse block execution of, other priority tasks.

On the front end interface, MATLAB has a clear sense of whether code is currently executing. When there is, the status bar reads "Busy" and the Editor ribbon strip updates to show a "Pause" button to interrupt execution. You can enter new commands to the command window, but they aren't read and executed until the currently executing code completes. Once execution is completed, the status bar is cleared of text, the ribbon "Pause" button is replaced with a "Run" button, and the command window displays a >> indicating that new commands entered there will be executed immediately.

Effectively I'd like code running in the middle of a function to be able to have the equivalent effect of entering new commands into the command window, to be run after currently-executing code is done.

For example, consider the function:

function testfun(x)

    disp("Starting main execution with input "   x);

    pause(1)

    disp("Completed main execution with input "   x);
    
    disp("Starting followup execution with input "   x);

    pause(1)

    disp("Completed followup execution with input "   x);

end

A function or script that invokes this a couple of times (and where the extra disp calls are, there could be other unspecified time-consuming activities):

testfun(1)
disp("Done testfun(1).")
testfun(2)
disp("Done testfun(2).")

would result in the output:

Starting main execution with input 1
Completed main execution with input 1
Starting followup execution with input 1
Completed followup execution with input 1
Done testfun(1).
Starting main execution with input 2
Completed main execution with input 2
Starting followup execution with input 2
Completed followup execution with input 2
Done testfun(2).

and take four seconds in total. An alternative solution would result in the output:

Starting main execution with input 1
Completed main execution with input 1
Done testfun(1).
Starting main execution with input 2
Completed main execution with input 2
Done testfun(2).
Starting followup execution with input 1
Completed followup execution with input 1
Starting followup execution with input 2
Completed followup execution with input 2

The output Done testfun(2), which is the last of the higher-priority output results to appear, would only have taken 2 seconds to appear, rather than the original 4.

Is this possible? Alternatively, if I could allow execution to clear the current execution stack (as I could if interrupting the code with the debugger, then using dbstep out all the way up to the Base workspace, then calling new code from the command window) this would be a useful compromise even if it leaves open the possibility there are more function calls queued in the Base workspace. (The actual choice of workspace the code executes in doesn't matter too much, but stepping out of deeper nested workspaces would at least allow the remaining code queued within those workspaces to be completed before the workspace is destroyed.)

CodePudding user response:

The best part-solution to this I'm aware of, with thanks to Jan Simon at MathWorks, is to observe the actual status bar of the MATLAB window. This is accessible with:

statusbar = com.mathworks.mde.desk.MLDesktop.getInstance.getMainFrame.getStatusBar;

The text contained in the status bar is then retrievable with statusbar.getText. When code is executing, the text contains the word "Busy", and when nothing is executing it doesn't, so I can use logic from this to decide MATLAB is currently busy (with careful attention to the possibility that other things, such as the profiler, will also modify the contents of this text).

A timer can poll this text so a callback will fire a short time (though not instantly, and dependent on how aggressively I'm willing to poll the interface) after MATLAB's Busy status ends.

This is not ideal because it only indirectly infers the busy status from a UI element that isn't designed to be involved in application control, and may be unreliable, and because it depends on repeated polling, but in some circumstances it will be better than nothing.

Applying this to the simplified example, we can break the task into two functions, with the main one taking an extra argument t to receive a timer object to interact with:

function testmain(x,t)

    disp("Starting main execution with input "   x);

    pause(1)

    disp("Completed main execution with input "   x);

    t.UserData.followups(end 1) = x;

end

function testfollowup(x)

    disp("Starting followup execution with input "   x);

    pause(1)

    disp("Completed followup execution with input "   x);

end

A callback for the timer could then be:

function followupcallback(t,~)

    if isempty(t.UserData.followups)
        return
    end

    status = t.UserData.statusbar.getText;

    if ~isempty(status) && contains(string(status),"Busy")
        return
    end

    arrayfun(@testfollowup,t.UserData.followups);
    t.UserData.followups = [];
    t.stop

end

and the polling timer can then be set up:

t = timer( ...
    "TimerFcn",@followupcallback, ...
    "Period",0.25, ...
    "ExecutionMode","fixedRate", ...
    "UserData",struct( ...
        "statusbar",com.mathworks.mde.desk.MLDesktop.getInstance.getMainFrame.getStatusBar, ...
        "followups",[]));
t.start;

With the example script modified to pass the timer through:

testfun(1,t)
disp("Done testfun(1).")
testfun(2,t)
disp("Done testfun(2).")

The resulting output is reorded as intended.

CodePudding user response:

I think you could build an actual work queue. I'm using a global variable here for that, so that multiple different functions can add to the work queue. I'm also using nested functions to encapsulate arbitrary code and variables into a function handle that can be stored and run later on.

I'm still hazy as to why this would be useful, but it does process things in the order given by the example in the OP.

initialize_work_queue
testfun(1)
disp("Done testfun(1).")
testfun(2)
disp("Done testfun(2).")
process_work_queue
disp("Done process_work_queue.")

function testfun(x)
    disp("Starting main execution with input "   x);
    pause(1)
    disp("Completed main execution with input "   x);
    function follow_up_work
        disp("Starting followup execution with input "   x);
        pause(1)
        disp("Completed followup execution with input "   x);
    end
    global my_work_queue
    my_work_queue{end 1} = @follow_up_work;
end

function initialize_work_queue
    global my_work_queue
    my_work_queue = {};
end

function process_work_queue
    global my_work_queue
    while ~isempty(my_work_queue)
        func = my_work_queue{1};
        my_work_queue(1) = [];
        func();
    end
end
  • Related