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.