I need to do some work on a specific thread (for all intents and purposes, we can say this is the UI thread), but the method requesting that work to be done may or may not be executing in a different thread. I am completely new to multithreaded programming, but have arrived at the conclusion that the correct approach to this is to use a TaskScheduler.
After toying around for a while with a custom implementation, I found FromCurrentSynchronizationContext. This appears to do exactly what I need and save me a lot of trouble (famous last words and all that).
My question comes down to whether I am overlooking anything that will get me into trouble, or maybe I'm overcomplicating the issue altogether. Here's what I'm doing now:
TaskScheduler
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Internals
{
internal static class MainThreadTaskScheduler
{
private static readonly object taskSchedulerLock = new();
private static readonly Thread taskSchedulerThread;
private static readonly TaskScheduler taskScheduler;
static MainThreadTaskScheduler()
{
lock (taskSchedulerLock)
{
// despite calling it the "main thread", we don't really care which thread
// this is initialized with, we just need to always use the same one for
// executing the scheduled tasks
taskSchedulerThread = Thread.CurrentThread;
if (SynchronizationContext.Current is null)
{
// in implementation this may be null, a default context works
SynchronizationContext.SetSynchronizationContext(new());
}
taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
}
}
public static Task Schedule(Action action)
{
lock (taskSchedulerLock)
{
if (Thread.CurrentThread == taskSchedulerThread)
{
// if we are already on the main thread, just run the delegate
action();
return Task.CompletedTask;
}
return Task.Factory.StartNew(action, CancellationToken.None,
TaskCreationOptions.None, taskScheduler);
}
}
public static Task<TResult> Schedule<TResult>(Func<TResult> func)
{
lock (taskSchedulerLock)
{
if (Thread.CurrentThread == taskSchedulerThread)
{
// if we are already on the main thread, just run the delegate
return Task.FromResult(func());
}
return Task.Factory.StartNew(func, CancellationToken.None,
TaskCreationOptions.None, taskScheduler);
}
}
}
}
Usage
// ...elsewhere...
public static bool RunTaskInMainThread()
{
// we need to synchronously return the result from the main thread regardless of
// which thread we are currently executing in
return MainThreadTaskScheduler.Schedule(() => SomeMethod()).GetAwaiter().GetResult();
}
I had attempted to make RunTaskInMainThread an async method and use await, but it kept causing my program to hang rather than yielding a result. I'm sure I was just using that incorrectly, but I don't know how to implement it here (bonus question: how can I use await here?).
Am I doing anything wrong here? Is there a better way to get the same results?
You are not in the right direction. Not because you are not smart, but because in the general area that you are trying to move there are traps all over the place.
The TaskSchedulers are not compatible with async/await. They were invented before async/await was a thing, for tasks that we now call delegate-based tasks (tasks that represent the completion of a specific delegate), in contrast with the kind of tasks that are created by async methods and are now known as promise-style tasks (tasks that represent just a promise that sometime in the future they'll complete).
The SynchronizationContext class is useless by itself. It's only useful as a base class for implementing derived classes like the WindowsFormsSynchronizationContext or Stephen Cleary's AsyncContextSynchronizationContext. It's a pity that this class was not defined as abstract, like the TaskScheduler is, to prevent programmers from trying to use it as is. Implementing a proper SynchronizationContext-derived class is not trivial.
When a thread is used for scheduling work via a scheduler, either a TaskScheduler or a SynchronizationContext, the thread is then owned by the scheduler. You can't have a thread that is shared by two schedulers, or by a scheduler and some method that wants to use that thread at any time on demand. That's why when start a message loop on the UI thread with the Application.Run method, this call is blocking. Any code that follows this call will not execute before the loops is completed (before the associated windows Form is closed). The same is true and with Stephen Cleary's AsyncContext. The AsyncContext.Run call is blocking (example).
Some more links:
ParallelExtensionsExtras Tour – #7 – Additional TaskSchedulers
ParallelExtensionsExtras source code
A bare-bone SingleThreadTaskScheduler implementation
Related
Edit
I find Building Async Coordination Primitives, Part 1: AsyncManualResetEvent might be related to my topic.
In the case of TaskCompletionSource, that means that synchronous continuations can happen as part of a call to {Try}Set*, which means in our AsyncManualResetEvent example, those continuations could execute as part of the Set method. Depending on your needs (and whether callers of Set may be ok with a potentially longer-running Set call as all synchronous continuations execute), this may or may not be what you want.
Many thanks to all of the answers, thank you for your knowledge and patience!
Original Question
I know that Task.Run runs on a threadpool thread and threads can have re-entrancy. But I never knew that 2 tasks can run on the same thread when they are both alive!
My Question is: is that reasonable by design? Does that mean lock inside an async method is meaningless (or say, lock cannot be trusted in async method block, if I'd like a method that doesn't allow reentrancy)?
Code:
namespace TaskHijacking
{
class Program
{
static TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
static object methodLock = new object();
static void MethodNotAllowReetrance(string callerName)
{
lock(methodLock)
{
Console.WriteLine($"Enter MethodNotAllowReetrance, caller: {callerName}, on thread: {Thread.CurrentThread.ManagedThreadId}");
if (callerName == "task1")
{
tcs.SetException(new Exception("Terminate tcs"));
}
Thread.Sleep(1000);
Console.WriteLine($"Exit MethodNotAllowReetrance, caller: {callerName}, on thread: {Thread.CurrentThread.ManagedThreadId}");
}
}
static void Main(string[] args)
{
var task1 = Task.Run(async () =>
{
await Task.Delay(1000);
MethodNotAllowReetrance("task1");
});
var task2 = Task.Run(async () =>
{
try
{
await tcs.Task; // await here until task SetException on tcs
}
catch
{
// Omit the exception
}
MethodNotAllowReetrance("task2");
});
Task.WaitAll(task1, task2);
Console.ReadKey();
}
}
}
Output:
Enter MethodNotAllowReetrance, caller: task1, on thread: 6
Enter MethodNotAllowReetrance, caller: task2, on thread: 6
Exit MethodNotAllowReetrance, caller: task2, on thread: 6
Exit MethodNotAllowReetrance, caller: task1, on thread: 6
The control flow of the thread 6 is shown in the figure:
You already have several solutions. I just want to describe the problem a bit more. There are several factors at play here that combine to cause the observed re-entrancy.
First, lock is re-entrant. lock is strictly about mutual exclusion of threads, which is not the same as mutual exclusion of code. I think re-entrant locks are a bad idea in the 99% case (as described on my blog), since developers generally want mutual exclusion of code and not threads. SemaphoreSlim, since it is not re-entrant, mutually excludes code. IMO re-entrant locks are a holdover from decades ago, when they were introduced as an OS concept, and the OS is just concerned about managing threads.
Next, TaskCompletionSource<T> by default invokes task continuations synchronously.
Also, await will schedule its method continuation as a synchronous task continuation (as described on my blog).
Task continuations will sometimes run asynchronously even if scheduled synchronously, but in this scenario they will run synchronously. The context captured by await is the thread pool context, and the completing thread (the one calling TCS.TrySet*) is a thread pool thread, and in that case the continuation will almost always run synchronously.
So, you end up with a thread that takes a lock, completes a TCS, thus executing the continuations of that task, which includes continuing another method, which is then able to take that same lock.
To repeat the existing solutions in other answers, to solve this you need to break that chain at some point:
(OK) Use a non-reentrant lock. SemaphoreSlim.WaitAsync will still execute the continuations while holding the lock (not a good idea), but since SemaphoreSlim isn't re-entrant, the method continuation will (asynchronously) wait for the lock to be available.
(Best) Use TaskCompletionSource.RunContinuationsAsynchronously, which will force task continuations onto a (different) thread pool thread. This is a better solution because your code is no longer invoking arbitrary code while holding a lock (i.e., the task continuations).
You can also break the chain by using a non-thread-pool context for the method awaiting the TCS. E.g., if that method had to resume on a UI thread, then it could not be run synchronously from a thread pool thread.
From a broader perspective, if you're mixing locks and TaskCompletionSource instances, it sounds like you may be building (or may need) an asynchronous coordination primitive. I have an open-source library that implements a bunch of them, if that helps.
A task is an abstraction over some amount of work. Usually this means that the work is split into parts, where the execution can be paused and resumed between parts. When resuming it may very well run on another thread. But the pausing/resuming may only be done at the await statements. Notably, while the task is 'paused', for example because it is waiting for IO, it does not consume any thread at all, it will only use a thread while it is actually running.
My Question is: is that reasonable by design? Does that mean lock inside an async method is meaningless?
locks inside a async method is far from meaningless since it allows you to ensure a section of code is only run by one thread at a time.
In your first example there can be only one thread that has the lock at a time. While the lock is held that task cannot be paused/resumed since await is not legal while in a lock body. So a single thread will execute the entire lock body, and that thread cannot do anything else until it completes the lock body. So there is no risk of re-entrancy unless you invoke some code that can call back to the same method.
In your updated example the problem occurs due to TaskCompletionSource.SetException, this is allowed to reuse the current thread to run any continuation of the task immediately. To avoid this, and many other issues, make sure you only hold the lock while running a limited amount of code. Any method calls that may run arbitrary code risks causing deadlocks, reentrancy, and many other problems.
You can solve the specific problem by using a ManualResetEvent(Slim) to do the signaling between threads instead of using a TaskCompletionSource.
So your method is basically like this:
static void MethodNotAllowReetrance()
{
lock (methodLock) tcs.SetResult();
}
...and the tcs.Task has a continuation attached that invokes the MethodNotAllowReetrance. What happens then is the same thing that would happen if your method was like this instead:
static void MethodNotAllowReetrance()
{
lock (methodLock) MethodNotAllowReetrance();
}
The moral lesson is that you must be very careful every time you invoke any method inside a lock-protected region. In this particular case you have a couple of options:
Don't complete the TaskCompletionSource while holding the lock. Defer its completion until after you have exited the protected region:
static void MethodNotAllowReetrance()
{
bool doComplete = false;
lock (methodLock) doComplete = true;
if (doComplete) tcs.SetResult();
}
Configure the TaskCompletionSource so that it invokes its continuations asynchronously, by passing the TaskCreationOptions.RunContinuationsAsynchronously in its constructor. This is an option that you don't have very often. For example when you cancel a CancellationTokenSource, you don't have the option to invoke asynchronously the callbacks registered to its associated CancellationToken.
Refactor the MethodNotAllowReetrance method in a way that it can handle reentrancy.
Use SemaphoreSlim instead of lock, since, as the documentation says:
The SemaphoreSlim class doesn't enforce thread or task identity
In your case, it would look something like this:
// Semaphore only allows one request to enter at a time
private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1);
void SyncMethod() {
_semaphoreSlim.Wait();
try {
// Do some sync work
} finally {
_semaphoreSlim.Release();
}
}
The try/finally block is optional, but it makes sure that the semaphore is released even if an exception is thrown somewhere in your code.
Note that SemaphoreSlim also has a WaitAsync() method, if you want to wait asynchronously to enter the semaphore.
What prevents me from constantly using the async/wait pattern in my .net code is that once you create an async method the async keyword tends to spread thru my code forcing me to make all methods async. Is there a pattern to stop this efficiently?
What prevents me from constantly using the async/wait pattern in my .net code is that once you create an async method the async keyword tends to spread thru my code forcing me to make all methods async. Is there a pattern to stop this efficiently?
Let me ask you this, then: why are you using async/await? You need to take a step back and decide if you want the benefits of asynchronous code. If you do want those benefits, then your code must be asynchronous. As soon as you block a thread, you lose all the benefits of async. So really, why use asynchronous code at all, if you're just going to block a thread anyway?
That said, there are some scenarios where a partially-async stack makes sense. For example, if your code base is temporarily in a transition state. In this case, you can use one of the hacks described in my article on brownfield async:
Blocking directly (may cause deadlocks).
Blocking on a thread pool thread (executes code on a different thread and different context, may cause unexpected parallelism).
Blocking on the current thread with a thread pool context (executes code in a different context, may cause unexpected parallelism).
Blocking on a thread with a single-threaded context (executes code on a different thread and different context).
Blocking on a nested message loop (may cause unexpected reentrancy).
All of these are hacks, and all have different drawbacks. There is no hack that works in every scenario.
As commented earlier you should go async all the way...
In some cases we use a litle helper class to cut off the async pattern. This class alows you to wait for the result without ending up with a deadlock by replacing the SynchronizationContext.
public class NoSynchronizationContextScope : IDisposable
{
private readonly SynchronizationContext synchronizationContext;
public NoSynchronizationContextScope()
{
synchronizationContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
}
public void Dispose() => SynchronizationContext.SetSynchronizationContext(synchronizationContext);
}
void fn()
{
using (new NoSynchronizationContextScope())
{
fnAsync().Wait();
// or
// var result = fnAsync().Result();
// in case you have to wait for a result
}
}
In the very particular case of a method that you want to start running but not wait for the result of (i.e. triggering an event handler) you can use async void. Just make sure you catch all the errors at that point:
public async void DoSomething() {
try {
var o = await DoThing1();
await DoThing2(o);
} catch (Exception e) {
// Log the error somewhere (to a file, database, etc.)
}
}
You can use async method and invoke not async method in task so outer method will behave like async and inner method is still sync.
public async Task OuterFuncAsync()
{
await Task.Run(() => InnerFunc());
}
I think I'm not understanding something. I had thought that Task.Yield() forced a new thread/context to be started for a task but upon re-reading this answer it seems that it merely forces the method to be async. It will still be on the same context.
What's the correct way - in an asp.net process - to create and run multiple tasks in parallel without causing deadlock?
In other words, suppose I have the following method:
async Task createFileFromLongRunningComputation(int input) {
//many levels of async code
}
And when a certain POST route is hit, I want to simultaneously launch the above methods 3 times, return immediately, but log when all three are done.
I think I need to put something like this into my action
public IHttpAction Post() {
Task.WhenAll(
createFileFromLongRunningComputation(1),
createFileFromLongRunningComputation(2),
createFileFromLongRunningComputation(3)
).ContinueWith((Task t) =>
logger.Log("Computation completed")
).ConfigureAwait(false);
return Ok();
}
What needs to go into createFileFromLongRunningComputation? I had thought Task.Yield was correct but it apparently is not.
The correct way to offload concurrent work to different threads is to use Task.Run as rossipedia suggested.
The best solutions for background processing in ASP.Net (where your AppDomain can be recycled/shut down automatically together with all your tasks) are in Scott Hanselman and Stephen Cleary's blogs (e.g. HangFire)
However, you could utilize Task.Yield together with ConfigureAwait(false) to achieve the same.
All Task.Yield does is return an awaiter that makes sure the rest of the method doesn't proceed synchronously (by having IsCompleted return false and OnCompleted execute the Action parameter immediately). ConfigureAwait(false) disregards the SynchronizationContext and so forces the rest of the method to execute on a ThreadPool thread.
If you use both together you can make sure an async method returns a task immediately which will execute on a ThreadPool thread (like Task.Run):
async Task CreateFileFromLongRunningComputation(int input)
{
await Task.Yield().ConfigureAwait(false);
// executed on a ThreadPool thread
}
Edit:
George Mauer pointed out that since Task.Yield returns YieldAwaitable you can't use ConfigureAwait(false) which is a method on the Task class.
You can achieve something similar by using Task.Delay with a very short timeout, so it wouldn't be synchronous but you wouldn't waste much time:
async Task CreateFileFromLongRunningComputation(int input)
{
await Task.Delay(1).ConfigureAwait(false);
// executed on a ThreadPool thread
}
A better option would be to create a YieldAwaitable that simply disregards the SynchronizationContext the same as using ConfigureAwait(false) does:
async Task CreateFileFromLongRunningComputation(int input)
{
await new NoContextYieldAwaitable();
// executed on a ThreadPool thread
}
public struct NoContextYieldAwaitable
{
public NoContextYieldAwaiter GetAwaiter() { return new NoContextYieldAwaiter(); }
public struct NoContextYieldAwaiter : INotifyCompletion
{
public bool IsCompleted { get { return false; } }
public void OnCompleted(Action continuation)
{
var scheduler = TaskScheduler.Current;
if (scheduler == TaskScheduler.Default)
{
ThreadPool.QueueUserWorkItem(RunAction, continuation);
}
else
{
Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.PreferFairness, scheduler);
}
}
public void GetResult() { }
private static void RunAction(object state) { ((Action)state)(); }
}
}
This isn't a recommendation, it's an answer to your Task.Yield questions.
(l3arnon's answer is the correct one. This answer is more of a discussion on whether the approach posed by the OP is a good one.)
You don't need anything special, really. The createFileFromLongRunningComputation method doesn't need anything special, just make sure you are awaiting some async method in it and the ConfigureAwait(false) should avoid the deadlock, assuming you're not doing anything out of the ordinary (probably just file I/O, given the method name).
Caveat:
This is risky. ASP.net will most likely pull the rug out from under you in this situation if the tasks take too long to finish.
As one of the commenters pointed out, there are better ways of accomplishing this. One of them is HostingEnvironment.QueueBackgroundWorkItem (which is only available in .NET 4.5.2 and up).
If the long running computation takes a significantly long time to complete, you're probably better off keeping it out of ASP.net entirely. In that situation, a better method would be to use some sort of message queue, and a service that processes those messages outside of IIS/ASP.net.
OK first of all, it's nothing that I need to implement or anything. I just need to know the answer because someone more experienced told me that asynchronous execution doesn't necessarily have to involve a new thread as threads are somewhat heavy constructs, which confused me a lot and I couldn't agree.
Now let's say, I have two methods - Execute() and ExecuteAsync(). Execute() is running on the main thread. I want to call ExecuteAsync() from within Execute() and I don't care whenever it completes executing, but when it does, may be (or may be not) I want use it's return value. That's a typical example of an asynchronous execution, right?
I know I can do this using BackgroundWorker or IAsyncResult (Delegate.BeginInvoke()), but AFAIK under the hood they spawns a secondary CLR Thread/ThreadPool Thread.
So is it anyhow possible to execute the method ExecuteAsync() asynchronously without the help of a second thread?
EDIT : I think this edit will clarify the scenario further. Invoking ExecuteAsync() is NOT the only (or last) task for Execute() to perform. Execute() should continue it's own tasks without caring about the execution of ExecuteAsync() method.
Here is an example of a program that uses asynchrony and never ever uses more than one thread:
public class Foo
{
private int _value;
private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
public int Value
{
get
{
return _value;
}
set
{
_value = value;
var oldTCS = tcs;
tcs = new TaskCompletionSource<bool>();
oldTCS.SetResult(true);
}
}
public Task ValueChanged()
{
return tcs.Task;
}
}
private static void Main(string[] args)
{
Foo foo = new Foo();
foo.ValueChanged()
.ContinueWith(t =>
{
Console.WriteLine(foo.Value);
}, TaskContinuationOptions.ExecuteSynchronously);
foo.Value = 5;
}
The Task returned from ValueChanged will be completed the next time that Value is changed. The user of the Foo class can get that returned task and wire up continuations to run on that task based on an operation that has not yet happened. Then, at some point in the future, the value of foo is changed, and the continuation will run. Note that the foo object could be passed to some other function, entirely unknown to Main, that ends up setting the value (to show why you might want to do something like this).
No new thread is needed to create the Task, nor to execute the continuation.
Here's another example that's much more practical:
We'll start with this simple (extension) method that takes a form and returns a Task indicating when that form is next closed:
public static class FormExtensions
{
public static Task WhenClosed(this Form form)
{
var tcs = new TaskCompletionSource<bool>();
form.FormClosed += (sender, args) => tcs.SetResult(true);
return tcs.Task;
}
}
Now we can have this in one of our forms:
private async void button1_Click(object sender, EventArgs args)
{
Form2 otherForm = new Form2();
otherForm.Show();
await otherForm.WhenClosed();
//take some data from that form and display it on this form:
textBox1.Text = otherForm.Name;
}
Creating and showing another form never involves the creation of new threads. Both this form and the new form use entirely the one UI thread to be created and modified.
The creation of the Task returned from WhenClosed does not need to create a new thread at all.
When the Task is awaited, no new thread is created. The current method ends and the UI thread is left to go back to processing messages. At some point, that same UI thread will do something that results in the second form being closed. That will result in the continuation of the task running, thus returning us to our button click handler where we set the text of the textbox.
All of this is done entirely with the UI thread, no other threads have been created. And yet we've just "waited" (without actually waiting) for a long running operation to finish (the user to put some information into the second form and then close it) without blocking the UI thread, thus keeping the main form responsive.
So is it anyhow possible to execute the method ExecuteAsync() asynchronously without the help of a second thread?
It is possible for some methods to run asynchronously without using a new thread. This can be done via Asynchronous I/O with a signal, for example. Most of the framework's new Async methods added in .NET 4.5 async IO whenever possible instead of threads.
This is why it's a good idea to not assume asynchronous == new thread. While asynchrony can be implemented using threading, it is not always implemented this way. It's better to just think of an asynchronous operation as an operation that (hopefully) will not block, and will complete at some point in the future.
Coroutines are a common way to implement several logical threads using a single physical thread. Older operating systems used this and other related concepts to implement cooperative multitasking.
In this context you may also be interested in continuation-passing style and Eric Lippert has a good blog series on this very topic - Part 1, Part 2, Part 3, Part 4, Part 5.
The Task Parallel Library is great and I've used it a lot in the past months. However, there's something really bothering me: the fact that TaskScheduler.Current is the default task scheduler, not TaskScheduler.Default. This is absolutely not obvious at first glance in the documentation nor samples.
Current can lead to subtle bugs since its behavior is changing depending on whether you're inside another task. Which can't be determined easily.
Suppose I am writting a library of asynchronous methods, using the standard async pattern based on events to signal completion on the original synchronisation context, in the exact same way XxxAsync methods do in the .NET Framework (eg DownloadFileAsync). I decide to use the Task Parallel Library for implementation because it's really easy to implement this behavior with the following code:
public class MyLibrary
{
public event EventHandler SomeOperationCompleted;
private void OnSomeOperationCompleted()
{
SomeOperationCompleted?.Invoke(this, EventArgs.Empty);
}
public void DoSomeOperationAsync()
{
Task.Factory.StartNew(() =>
{
Thread.Sleep(1000); // simulate a long operation
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default)
.ContinueWith(t =>
{
OnSomeOperationCompleted(); // trigger the event
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
So far, everything works well. Now, let's make a call to this library on a button click in a WPF or WinForms application:
private void Button_OnClick(object sender, EventArgs args)
{
var myLibrary = new MyLibrary();
myLibrary.SomeOperationCompleted += (s, e) => DoSomethingElse();
myLibrary.DoSomeOperationAsync(); // call that triggers the event asynchronously
}
private void DoSomethingElse() // the event handler
{
//...
Task.Factory.StartNew(() => Thread.Sleep(5000)); // simulate a long operation
//...
}
Here, the person writing the library call chose to start a new Task when the operation completes. Nothing unusual. He or she follows examples found everywhere on the web and simply use Task.Factory.StartNew without specifying the TaskScheduler (and there is no easy overload to specify it at the second parameter). The DoSomethingElse method works fine when called alone, but as soon at it's invoked by the event, the UI freezes since TaskFactory.Current will reuse the synchronization context task scheduler from my library continuation.
Finding out this could take some time, especially if the second task call is buried down in some complex call stack. Of course, the fix here is simple once you know how everything works: always specify TaskScheduler.Default for any operation you're expecting to be running on the thread pool. However, maybe the second task is started by another external library, not knowing about this behavior and naively using StartNew without a specific scheduler. I'm expecting this case to be quite common.
After wrapping my head around it, I can't understand the choice of the team writing the TPL to use TaskScheduler.Current instead of TaskScheduler.Default as the default:
It's not obvious at all, Default is not the default! And the documentation is seriously lacking.
The real task scheduler used by Current depends of the call stack! It's hard to maintain invariants with this behavior.
It's cumbersome to specify the task scheduler with StartNew since you have to specify the task creation options and cancellation token first, leading to long, less readable lines. This can be alleviated by writing an extension method or creating a TaskFactory that uses Default.
Capturing the call stack has additional performance costs.
When I really want a task to be dependent on another parent running task, I prefer to specify it explicitly to ease code reading rather than rely on call stack magic.
I know this question may sound quite subjective, but I can't find a good objective argument as to why this behavior is as it. I'm sure I'm missing something here: that's why I'm turning to you.
I think the current behavior makes sense. If I create my own task scheduler, and start some task that starts other tasks, I probably want all the tasks to use the scheduler I created.
I agree that it's odd that sometimes starting a task from the UI thread uses the default scheduler and sometimes not. But I don't know how would I make this better if I was designing it.
Regarding your specific problems:
I think the easiest way to start a new task on a specified scheduler is new Task(lambda).Start(scheduler). This has the disadvantage that you have to specify type argument if the task returns something. TaskFactory.Create can infer the type for you.
You can use Dispatcher.Invoke() instead of using TaskScheduler.FromCurrentSynchronizationContext().
[EDIT]
The following only addresses the problem with the scheduler used by Task.Factory.StartNew.
However, Task.ContinueWith has a hardcoded TaskScheduler.Current.
[/EDIT]
First, there is an easy solution available - see the bottom of this post.
The reason behind this problem is simple: There is not only a default task scheduler (TaskScheduler.Default) but also a default task scheduler for a TaskFactory (TaskFactory.Scheduler).
This default scheduler can be specified in the constructor of the TaskFactory when it's created.
However, the TaskFactory behind Task.Factory is created as follows:
s_factory = new TaskFactory();
As you can see, no TaskScheduler is specified; null is used for the default constructor - better would be TaskScheduler.Default (the documentation states that "Current" is used which has the same consequences).
This again leads to the implementation of TaskFactory.DefaultScheduler (a private member):
private TaskScheduler DefaultScheduler
{
get
{
if (m_defaultScheduler == null) return TaskScheduler.Current;
else return m_defaultScheduler;
}
}
Here you should see be able to recognize the reason for this behaviour: As Task.Factory has no default task scheduler, the current one will be used.
So why don't we run into NullReferenceExceptions then, when no Task is currently executing (i.e. we have no current TaskScheduler)?
The reason is simple:
public static TaskScheduler Current
{
get
{
Task internalCurrent = Task.InternalCurrent;
if (internalCurrent != null)
{
return internalCurrent.ExecutingTaskScheduler;
}
return Default;
}
}
TaskScheduler.Current defaults to TaskScheduler.Default.
I would call this a very unfortunate implementation.
However, there is an easy fix available: We can simply set the default TaskScheduler of Task.Factory to TaskScheduler.Default
TaskFactory factory = Task.Factory;
factory.GetType().InvokeMember("m_defaultScheduler", BindingFlags.SetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly, null, factory, new object[] { TaskScheduler.Default });
I hope I could help with my response although it's quite late :-)
Instead of Task.Factory.StartNew()
consider using: Task.Run()
This will always execute on a thread pool thread. I just had the same problem described in the question and I think that is a good way of handling this.
See this blog entry:
http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx
It's not obvious at all, Default is not the default! And the documentation is seriously lacking.
Default is the default, but it's not always the Current.
As others have already answered, if you want a task to run on the thread pool, you need to explicitly set the Current scheduler by passing the Default scheduler into either the TaskFactory or the StartNew method.
Since your question involved a library though, I think the answer is that you should not do anything that will change the Current scheduler that's seen by code outside your library. That means that you should not use TaskScheduler.FromCurrentSynchronizationContext() when you raise the SomeOperationCompleted event. Instead, do something like this:
public void DoSomeOperationAsync() {
var context = SynchronizationContext.Current;
Task.Factory
.StartNew(() => Thread.Sleep(1000) /* simulate a long operation */)
.ContinueWith(t => {
context.Post(_ => OnSomeOperationCompleted(), null);
});
}
I don't even think you need to explicitly start your task on the Default scheduler - let the caller determine the Current scheduler if they want to.
I've just spent hours trying to debug a weird issue where my task was scheduled on the UI thread, even though I didn't specify it to. It turned out the problem was exactly what your sample code demonstrated: A task continuation was scheduled on the UI thread, and somewhere in that continuation, a new task was started which then got scheduled on the UI thread, because the currently executing task had a specific TaskScheduler set.
Luckily, it's all code I own, so I can fix it by making sure my code specify TaskScheduler.Default when starting new tasks, but if you aren't so lucky, my suggestion would be to use Dispatcher.BeginInvoke instead of using the UI scheduler.
So, instead of:
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => UpdateUI(), uiScheduler);
Try:
var uiDispatcher = Dispatcher.CurrentDispatcher;
var task = Task.Factory.StartNew(() => Thread.Sleep(5000));
task.ContinueWith((t) => uiDispatcher.BeginInvoke(new Action(() => UpdateUI())));
It's a bit less readable though.