TaskEx.Yield(TaskScheduler)
Asked Answered
G

2

3

Last month I asked the following question which resulted in my learning of TaskEx.Yield:

Can async methods have expensive code before the first 'await'?

However, I have since realized that this method actually submits all subsequent code to the ambient TaskScheduler. In true DI spirit, our team has agreed to avoid using ambient instances where possible, so I would like to know if it's possible to explicitly specify a TaskScheduler to use?

Something like the following would be great:

public static YieldAwaitable Yield(TaskScheduler taskScheduler)
{
    return new YieldAwaitable(taskScheduler);
}

However, the current implementation of Async CTP only offers:

public static YieldAwaitable Yield()
{
    return new YieldAwaitable(SynchronizationContext.Current ?? TaskScheduler.Current);
}

Would the following provide an acceptably efficient alternative?

await Task.Factory.StartNew(() => { }, CancellationToken.None, TaskCreationOptions.None, this.TaskScheduler);
Goodwill answered 5/1, 2012 at 12:31 Comment(1)
I do understand your concern, but I think using the ambient TaskScheduler in fine in most cases.Unzip
I
5

In true DI spirit, our team has agreed to avoid using ambient instances where possible...

The async language support is based around an implicit scheduling context. I don't see the need for dependency injection here. Any method calling your async method may supply its own context if necessary.

Your alternative:

await Task.Factory.StartNew(() => { }, CancellationToken.None, TaskCreationOptions.None, this.TaskScheduler);

will not work as expected. This will queue a noop lambda to a specific TaskScheduler and then resume the method on the implicit scheduling context.

An earlier version of the Async CTP did provide a "yield to another context" method, called SwitchTo. It was removed because it's too easy to misuse.

Personally, I think it's cleaner to keep your async code using its implicit scheduling context, which is provided by its calling method.

P.S. It's not (too) difficult to create and install your own context, e.g., for testing purposes. I wrote AsyncContext as a simple scheduling context for unit testing and Console programs. The Async CTP comes with GeneralThreadAffineContext, WindowsFormsContext, and WpfContext for testing. Any of these can be installed using SynchronizationContext.SetSynchronizationContext. IMO, DI is overkill.

Iconoduly answered 5/1, 2012 at 23:2 Comment(5)
'...then resume the method on the implicit scheduling context.' - According to my tests, code always appears to continue using the same TaskScheduler as specified by the last .StartNew or .Run. Am I mistaken?Goodwill
That's not what I see. Note that TaskScheduler.Current will return TaskScheduler.Default if not called from a scheduled task (which is the case for most async methods).Iconoduly
The reason I disagreed is that we recently had a problem where awaiting a CopyToAsync(Stream) call would result in all subsequent code executing on a different task scheduler. We had been explicitly using an injected TaskScheduler for our own tasks which used the UI thread. However, having not defined the default scheduler, the CopyToAsync method (a CTP extension) would switch to the ThreadPool scheduler, causing concurrency havoc in all our subsequent code. To switch back to our own, we used the alternative method you've quoted in this answer, and it appears to have solved the problem.Goodwill
That sounds like a serious issue, and no workaround should be necessary. I recommend you post a small repro to the Async forum for discussion.Iconoduly
The link explaining the removal of SwitchTo() also says how could you reimplement it yourself.Unzip
Z
2

The language support for async allows for awaitables to control their own scheduling. There is default scheduling that kicks in when you await a Task, but there are many ways you can change the default behavior with a little bit more code.

Specifically, await is intended to do the following:

  1. Test to see if the awaitable is "done" (GetAwaiter().IsCompleted).
  2. If (and only if) the awaitable wasn't done, ask for it to schedule the rest of the method (GetAwaiter().OnCompleted(...))
  3. "Realize" the awaitable's result. This means either give back the returned value or ensure that the exceptions encountered reemerge.

So keep in mind that if the awaitable claimed it was "done", then nothing is ever scheduled.

Task.Yield() is novel because it gives back an awaitable that is never done, for the express purpose of giving you a way to explicitly stop execution for now, and immediately schedule the rest up for execution. It uses the ambient context, but there are many other ways to do similarly without ambient contexts.

An example of overriding the default behavior is when you await an incomplete task, but using the ConfigureAwait(false) method. ConfigureAwait(false) wraps the task in a special awaitable that always uses the default task scheduler, effectively always resuming on the thread pool. 'false' is there to explicitly ignore the ambient sync context.

There isn't a Task.Yield().ConfigureAwait(false), but consider the following hypothetical:

// ... A ...
await Task.Yield().ConfigureAwait(false);
// ... B ...

The above can be pretty much achieved by

// ... A ...
await Task.Run(() => {
    // ... B ...
});

There is a little more explicitness and nesting there, but this isn't necessarily bad considering what's happening. Part 'A' always runs on the invoking thread, whereas part 'B' always runs on the thread pool. There are definitely differences on how you should look at code that is in sections A and B, so thus having some more curlies in between should hopefully get people to pause before assuming both sections are the same context.

Zayin answered 6/1, 2012 at 4:48 Comment(3)
The point of await is that you don't have to write nested code like that anymore.Unzip
Sometimes nesting is appropriate - and that was the entire premise why SwitchTo(...) was removed (was in early CTPs vs removed in later), and is absent in the .NET 4.5 Developer Preview. In the case where one block is in a distinctly different context than the previous, it's actually beneficial to have block separation. For example, imagine SwitchTo(...) were still remaining. That means for "A() if (...) { SwitchTo(...); } B();" in that case, A() is in one context, and there's a condition that may or may not switch to a different context before B().Zayin
SwitchTo(...) has even worse effects on reasoning about catch blocks. Now you need to reason about what conditions were hit before the throw, in order to see what context will be present for the catch block. Requiring the Task.Run(...) or Dispatcher.InvokeAsync(...) makes the alternate contexts much more explicit and helps prevent ambiguous contexts in catch blocks.Zayin

© 2022 - 2024 — McMap. All rights reserved.