9. Reporting progress of a big job with coroutines
So you’re running this big set of calculations that takes lots of time, and you want to keep the user informed on how it’s going. But there’s a problem. No matter what you print, or draw on the screen, nothing actually appears until the whole job is done. But there’s a solution, something called coroutines. I’m not going to try to explain them in depth, but I want to show you how to use them to solve this problem. (I am aware you could use parameter.watch to keep track of a counter, but there is a cleaner way). To restate the problem, we want to interrupt our big job periodically to put some progress on the screen. With normal programming, this is easy, because your big job can write to the screen whenever it wants, to keep the user up to date. Now you would imagine you could do the same in Codea, ie put a statement in your big job that prints progress regularly. The problem is that it doesn’t get to draw anything until your job is finished, and then all the progress messages arrive on the screen at once, ie they have been stacked up waiting for a chance to print. This is because the all the printing to screen, management of touches, etc, only occur when the draw function gets a turn to run, and that won’t happen until your big job finishes. So all the drawing freezes until then. You can see this if you copy this code into a new project and run it. Copy it from here or here, rather than from below.
function setup() parameter.action('Run_without_coroutine',bigJob) parameter.action('Run_with_coroutine',WithCoroutine) end function WithCoroutine() cos=coroutine.create(bigJobCos) coroutine.resume(cos) end function bigJob() output.clear() --clear print output area for n=1,10 do --time consuming code here for j=1,5000000 do local x=3.1^4.5 end print('completed',n) end print('Job done!') end function bigJobCos() output.clear() --clear print output area progress=0 --used for printing status of job for n=1,10 do --time consuming code here for j=1,5000000 do local x=3.1^4.5 end progress=n coroutine.yield() end progress=-1 --signal end of job if cos then cos=nil end --just to be tidy, kill this off end function draw() if progress then --only report if job is running, ie n is not nil if progress>0 then print('completed',progress) --print progress of job coroutine.resume(cos) --carry on calculating elseif progress==-1 then print('Job done!') progress=nil --to stop any more printing end end end
Run this program, then click the first button at top left, to see what I’m talking about. Nothing will happen for a while, then bang!, everything arrives at once. Then try clicking the second button and see the progress reports appear on screen, the way you want them. So how do we do this? Essentially, because we can’t update the screen while our big job is running, we suspend the big job, let Codea carry on – which in this case, means running draw, and get the draw function to get the big job going again after it’s finished drawing. We do it with coroutines, like so. First, you tell Codea you want a new coroutine, and give it a function to run. Then you set it going with the resume command.
cos=coroutine.create(bigJobCos) coroutine.resume(cos)
Codea will now start running the bigJobCos function perfectly normally. When we want to suspend the big job and do a report, we set the value of the progress indicator imaginatively called “progress”, so draw will have something to print, and then we insert this line
coroutine.yield()
This pauses the bigJobCos function, and lets Codea get on with drawing. Now in the draw function, I have placed some code that is watching for progress.
if progress then --only report if job is running, ie n is not nil if progress>0 then print('completed',progress) --print progress of job coroutine.resume(cos) --carry on calculating elseif progress==-1 then print('Job done!') progress=nil --to stop any more printing end end
Note how this code only executes if progress is not nil, so a job is running, and it prints the progress message and then resumes the coroutine (the cos in brackets is just the name of the variable I set up to hold the coroutine when I created it). Codea goes back to the big job, exactly where it left off, and continues working there, until the next pause, and so on. When the big job is finished, it sets progress=-1, just to let draw know the job is finished. This is just something I put in, it’s not part of the coroutine stuff. When draw sees this, it prints the end of job message and sets progress to nil so it can ignore it from here on. Note also that when the big job finishes, it sets the coroutine variable cos to nil, to avoid loose ends. So a coroutine involves the following steps
- create it and give it a function to run
- resume it, to get it started
- use coroutine.yield() to pause it
- use coroutine.resume() to continue where you left off
As with many things in Lua, coroutines can be used for many exotic uses, but this is a very practical and useful one. There’s another nice tutorial here.
That blew my mind for a while but I think I now understand it. Very useful though, thanks for posting.
The code is giving an error because of this line:
if progress>0 then
Can you correct it? (I have copied it from this page, I can’t open pastebin.com because it is banned by the f… Government)
Try it now
Thanks.