Is it possible to force a task to execute synchronously, on the current thread?
That is, is it possible, by e.g. passing some parameter to StartNew(), to ma
Yes, you can pretty much do that using custom task schedulers.
internal class MyScheduler : TaskScheduler
{
protected override IEnumerable<Task> GetScheduledTasks()
{
return Enumerable.Empty<Task>();
}
protected override void QueueTask(Task task)
{
base.TryExecuteTask(task);
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
base.TryExecuteTask(task);
return true;
}
}
static void Main(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId + " Main");
Task.Factory.StartNew(() => ThisShouldBeExecutedSynchronously(), CancellationToken.None, TaskCreationOptions.None, new MyScheduler());
}
Since you mention testing, you might prefer using a TaskCompletionSource<T> since it also lets you set an exception or set the task as cancelled (works in .Net 4 and 4.5):
Return a completed task with a result:
var tcs = new TaskCompletionSource<TRet>();
tcs.SetResult(func());
return tcs.Task;
Return a faulted task:
var tcs = new TaskCompletionSource<TRet>();
tcs.SetException(new InvalidOperationException());
return tcs.Task;
Return a canceled task:
var tcs = new TaskCompletionSource<TRet>();
tcs.SetCanceled();
return tcs.Task;
You can simply return the result of func() wrapped in a Task.
public class NoThreading : IThreads
{
public Task<TRet> StartNew<TRet>(Func<TRet> func)
{
return Task.FromResult(func());
}
}
Now you can attach "continue with" tasks to this.
OP here. This is my final solution (which actually solves a lot more than I asked about).
I use the same implementation for Threads in both test and production, but pass in different TaskSchedulers:
public class Threads
{
private readonly TaskScheduler _executeScheduler;
private readonly TaskScheduler _continueScheduler;
public Threads(TaskScheduler executeScheduler, TaskScheduler continueScheduler)
{
_executeScheduler = executeScheduler;
_continueScheduler = continueScheduler;
}
public TaskContinuation<TRet> StartNew<TRet>(Func<TRet> func)
{
var task = Task.Factory.StartNew(func, CancellationToken.None, TaskCreationOptions.None, _executeScheduler);
return new TaskContinuation<TRet>(task, _continueScheduler);
}
}
I wrap the Task in a TaskContinuation class in order to be able to specify TaskScheduler for the ContinueWith() call.
public class TaskContinuation<TRet>
{
private readonly Task<TRet> _task;
private readonly TaskScheduler _scheduler;
public TaskContinuation(Task<TRet> task, TaskScheduler scheduler)
{
_task = task;
_scheduler = scheduler;
}
public void ContinueWith(Action<Task<TRet>> func)
{
_task.ContinueWith(func, _scheduler);
}
}
I create my custom TaskScheduler that dispatches the action on the thread that scheduler was created on:
public class CurrentThreadScheduler : TaskScheduler
{
private readonly Dispatcher _dispatcher;
public CurrentThreadScheduler()
{
_dispatcher = Dispatcher.CurrentDispatcher;
}
protected override void QueueTask(Task task)
{
_dispatcher.BeginInvoke(new Func<bool>(() => TryExecuteTask(task)));
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return true;
}
protected override IEnumerable<Task> GetScheduledTasks()
{
return Enumerable.Empty<Task>();
}
}
Now I can specify the behaviour by passing in different TaskSchedulers to the Threads constructor.
new Threads(TaskScheduler.Default, TaskScheduler.FromCurrentSynchronizationContext()); // Production
new Threads(TaskScheduler.Default, new CurrentThreadScheduler()); // Let the tests use background threads
new Threads(new CurrentThreadScheduler(), new CurrentThreadScheduler()); // No threads, all synchronous
Finally, since the event loop doesn't run automatically in my unit test, I have to execute it manually. Whenever I need to wait for a background operation to complete I execute the following (from the main thread):
DispatcherHelper.DoEvents();
The DispatcherHelper can be found here.
Task scheduler decides whether to run a task on a new thread or on the current thread. There is an option to force running it on a new thread, but none forcing it to run on the current thread.
But there is a method Task.RunSynchronously() which
Runs the Task synchronously on the current TaskScheduler.
More on MSDN.
Also if you are using async/await there is already a similar question on that.