I'm in search of the simplest and safest way to fully guarantee the same behavior as await but using Task.ContinueWith, most specifically to ensure that if the original caller is in the UI thread then the continuation occurs on the UI thread, otherwise it should continue on any thread. My current approach throws an exception in some cases which I can't see an obvious way to prevent in advance.
My prior understanding was that this code:
await DoSomethingAsync();
DoSomethingElse();
and this code:
DoSomethingAsync().ContinueWith(
t => DoSomethingElse(),
TaskScheduler.FromCurrentSynchronizationContext());
ought to be equivalent, both resulting in DoSomethingElse() executing on the original calling thread if it was a UI thread, otherwise the continuation thread would be undefined.
However, it appears that TaskScheduler.FromCurrentSynchronizationContext() fails in at least some contexts when there is no UI thread or synchronizer defined. This occurs, at minimum, in Mono WASM projects. The documentation also mentions the method can fail if "the current SynchronizationContext may not be used as a TaskScheduler", but it doesn't say how to check whether this is the case before attempting to use it with ContinueWith.
As this code will be shared among many different consumers, from WPF to Mono WASM and others, I need a solution that fits all of them and doesn't require the consumer to specify if it's in a UI context or not.
What would be the simplest way to write the second block so that the continuation occurs on the UI thread when there is in fact a valid synchronization context such as in a WPF app - but also doesn't throw an exception if there isn't one - exactly the way await would?
I'm not sure why you wouldn't use
await. Code usingawaitis easier to write and maintain than code using the oldContinueWithmethod.That said, there are a couple major differences. The first is that
awaitcaptures the current context; this isSynchronizationContext.CurrentorTaskScheduler.Current(or no context at all, which is logically equivalent toTaskScheduler.Default).awaitschedules its continuation directly on that context and doesn't wrap theSynchronizationContextinto aTaskScheduler.So, if you want to use
ContinueWith, then you can useFromCurrentSynchronizationContext, but there's already a semantic difference: with a non-nullSynchronizationContext.Current, theawaitcontinuation would haveTaskScheduler.Currentequal toTaskScheduler.Default, whereas theContinueWithcontinuation would haveTaskScheduler.Currentequal to aTaskSchedulerinstance wrapping thatSynchronizationContext. Perhaps not a huge issue, but be aware that many low-level TPL methods implicitly useTaskScheduler.Current(includingContinueWith).The other major difference is that
ContinueWithdoesn't have a built-in understanding of asynchronous code. So ifDoSomethingElseis asynchronous, then the behavior will be different. In that case,ContinueWithwould return aTask<Task>and you should callUnwrapto more closely emulate the behavior ofawait.There's also a number of minor differences; passing
TaskContinuationOptions.DenyChildAttach | TaskContinuationOptions.ExecuteSynchronouslyshould be closer to theawaitbehavior. You could also tryTaskContinuationOptions.HideSchedulerwhich should causeTaskScheduler.Currentto beTaskScheduler.Defaultin the continuation.So in summary, these two should be mostly equivalent:
or, if asynchronous: