✅ Periodically (asynchronously) yielding in an otherwise synchronous method

I have a long-running CPU-bound algorithm that is being executed in a Blazor WASM application. I would like to cooperatively allow the UI to update / do event loop stuff etc. Would you think this approach would work:
/*
we have garanteed async yields via the flag, so we should return the
task instead of synchronously waiting and thus force consumers to also
await. The intended consumer is of course calling our method from a single
threaded context like in Blazor WASM, allowing for periodic UI updates and
event loops to execute. Because we assume a single threaded context, even
calling ConfigureAwait(false) would not help, as even the thread pool only has
the single thread available.
see also: https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
*/
public static Task MyAlgoAsync() => MyAlgoCore(periodicallyYield: true);
/*
we can guarantee the task returned by MyAlgoCore to have already completed
because we disable all yields via the flag
*/
public static void MyAlgo() => MyAlgoCore(periodicallyYield: false).Wait();
private static async Task MyAlgoCore(bool periodicallyYield)
{
//our algorithm takes a large number of steps
for(var i = 0; i < 1000000; i++)
{
//we can put Task.Yield inbetween those steps
if(periodicallyYield)
{
await Task.Yield();
}
//do some stuff
}
}
/*
we have garanteed async yields via the flag, so we should return the
task instead of synchronously waiting and thus force consumers to also
await. The intended consumer is of course calling our method from a single
threaded context like in Blazor WASM, allowing for periodic UI updates and
event loops to execute. Because we assume a single threaded context, even
calling ConfigureAwait(false) would not help, as even the thread pool only has
the single thread available.
see also: https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
*/
public static Task MyAlgoAsync() => MyAlgoCore(periodicallyYield: true);
/*
we can guarantee the task returned by MyAlgoCore to have already completed
because we disable all yields via the flag
*/
public static void MyAlgo() => MyAlgoCore(periodicallyYield: false).Wait();
private static async Task MyAlgoCore(bool periodicallyYield)
{
//our algorithm takes a large number of steps
for(var i = 0; i < 1000000; i++)
{
//we can put Task.Yield inbetween those steps
if(periodicallyYield)
{
await Task.Yield();
}
//do some stuff
}
}
Essentially we hide the await Task.Yield() path behind a flag, guarantueeing synchronous execution when desired, and enabling periodic yielding of the thread if not. CCing @BenMcLean since we got to the idea together.
28 Replies
BenMcLean
BenMcLean5mo ago
Another thought: send a progress report before yielding? (but only when periodicallyYield is true of course)
HtmlCompiler
HtmlCompiler5mo ago
if it's a long running algorithm it could deserve its own thread
SleepWellPupper
SleepWellPupperOP5mo ago
There is only one thread (Blazor WASM app)
HtmlCompiler
HtmlCompiler5mo ago
really? never used that still i would use a timed CancellationTokenSource maybe instead of a raw count or even a timer or an await-delay loop
SleepWellPupper
SleepWellPupperOP5mo ago
The for loop is just a placeholder for the expensive algorithm.
HtmlCompiler
HtmlCompiler5mo ago
aaah ok
SleepWellPupper
SleepWellPupperOP5mo ago
There is no cancellation required in the algorithm itself. We don't need delay etc. Simply asynchronously yielding every once in a while to allow the UI to do its thing
HtmlCompiler
HtmlCompiler5mo ago
every once in a while
that's what i mean with timer/cts/etc the while part, the how much to wait before yielding
SleepWellPupper
SleepWellPupperOP5mo ago
Yeah imagine n number of complicated steps that we can put Task.Yield() in between.
HtmlCompiler
HtmlCompiler5mo ago
well the less complicated the better
SleepWellPupper
SleepWellPupperOP5mo ago
Maybe someone else has some input/opinions on the idea?
Evyr
Evyr5mo ago
Task.Yield Method (System.Threading.Tasks)
Creates an awaitable task that asynchronously yields back to the current context when awaited.
BenMcLean
BenMcLean5mo ago
It warns against doing this without saying what to do instead from what I can tell. But maybe it's in that article it links to only I might be too tired to understand it at this moment. I'll look again tomorrow or some other day. I think the hint is that ContinueWith is what's preferable to keep UI responsive isntead of Task.Yield but since the linked article says absolutely nothing about responsive UI, it isn't definite that this is what's being recommended. Also, the linked article is from 2008, twelve years before Blazor WebAssembly was a thing. Should I really be taking its advice? There's so much "Do this." "No, don't do this, do that!" "No, don't do that, do this other third thing!" online and it's hard to keep it all straight. I think I just need to ignore this 2008 note from Microsoft and just do the thing that far more recent online tutorials say to do unless you've got a really compelling argument @Evyr
Evyr
Evyr5mo ago
if there's no synchronization context, or if it's configured in a certain way, it shouldn't cause any problems I just don't know how blazor handles that stuff so it's something to consider have you tested it?
BenMcLean
BenMcLean5mo ago
Not yet I am considering that instead of
if (shouldYield.Invoke())
{
progress?.Report(0d);
await Task.Yield();
}
if (shouldYield.Invoke())
{
progress?.Report(0d);
await Task.Yield();
}
maybe it should be something like
if (shouldYield.Invoke())
{
progress?.Report(0d);
do {
await Task.Yield();
} while (shieldYield.Invoke());
}
if (shouldYield.Invoke())
{
progress?.Report(0d);
do {
await Task.Yield();
} while (shieldYield.Invoke());
}
That way, all the tasks would keep yielding until told they shouldn't. That would, I think, guarantee the UI would get to run, correct?
HtmlCompiler
HtmlCompiler5mo ago
instead of yielding i would opt for trying "dividing" this task into a series of task of shorter duration (one could almost say coroutine) so for example after say 10~100 cycles you start getting the datetime and after ~30 msec (from the previous interruption) you return change signature of main method to IAsyncEnumerable so that you have to await foreach it then take some measurement to adjust waiting constants
SleepWellPupper
SleepWellPupperOP5mo ago
The sync context in blazor wasm is irrelevant, as there is only one thread anyway. So ConfigureAwait(false) won't make a difference, as scheduling the continuation to the threadpool instead of the captured context (UI context) results in the same (single) thread being used. @Evyr Also, @tippy what's all this about enumerables now? We're not doing async enumeration, we're just interspersing async pauses in our algorithm.
HtmlCompiler
HtmlCompiler5mo ago
yes but the point was you could use enumerable to return control to the other part of the code without recurring to thread yield
Unknown User
Unknown User5mo ago
Message Not Public
Sign In & Join Server To View
SleepWellPupper
SleepWellPupperOP5mo ago
$close
MODiX
MODiX5mo ago
If you have no further questions, please use /close to mark the forum thread as answered
BenMcLean
BenMcLean5mo ago
That might be what I need actually.
BenMcLean
BenMcLean5mo ago
Uh, I wasn't actually done asking stuff about this. Made a new tweet about it today: https://x.com/McLeanBen/status/1820480854357131562
Benjamin McLean (@McLeanBen) on X
I'm trying to do what you said in this blog @meziantou https://t.co/WV6562IKL7 What confuses me is that your blog doesn't seem to guarantee that the UI will ever get to the top when there are multiple tasks, no matter how many times they yield. How does the UI get to the top?
Twitter
BenMcLean
BenMcLean5mo ago
How's this done? I feel like I'm missing something in what you're saying. Can we re-open this?
SleepWellPupper
SleepWellPupperOP5mo ago
Maybe create a new help post and tag this one
Unknown User
Unknown User5mo ago
Message Not Public
Sign In & Join Server To View
BenMcLean
BenMcLean5mo ago
OK I think I will need to both post a newly revised version of this one AND make one about what object oriented design pattern(s) my code should follow
Want results from more Discord servers?
Add your server