26

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 make this code:

Task.Factory.StartNew(() => ThisShouldBeExecutedSynchronously());

behave like this:

ThisShouldBeExecutedSynchronously();

Background:

I have an interface called IThreads:

public interface IThreads
{
    Task<TRet> StartNew<TRet>(Func<TRet> func);
}

I would like to have two implemenetations of this, one normal that uses threads:

public class Threads : IThreads
{
    public Task<TRet> StartNew<TRet>(Func<TRet> func)
    {
        return Task.Factory.StartNew(func);
    }
}

And one that does not use threads (used in some testing scenarios):

public class NoThreading : IThreads
{
    public Task<TRet> StartNew<TRet>(Func<TRet> func)
    {
        // What do I write here?
    }
}

I could let the NoThreading version just call func(), but I want to return an instance of Task<TRet> on which I can perform operations such as ContinueWith().

Torbjörn Kalin
  • 1,976
  • 1
  • 22
  • 31
  • What testing scenarios is the NoThreading implementation used? It seems strange that an implementation of IThreads exists that does not in fact have anything to do with threading. – Todd Bowles Nov 19 '13 at 08:18
  • @Todd Bowles: I dislike so much when instead of answering question people say "Huh, why are you asking this? you are not supposed to ask this." – Simple Fellow Nov 05 '15 at 09:53
  • @SimpleFellow I understand where you are coming from, but an important part of providing help is to understand the problem and the context. In this case, IThreads feels like a leaky abstraction, whose non-threaded implementation will be confusing for a future developer. – Todd Bowles Jan 07 '16 at 23:18
  • @ToddBowles Would it be better if I change the name of the interface `IThreads` to e.g. `ITasks`? In comparison, `Task.Factory.StartNew()` does not necessarily start a new thread. So with a better name the abstraction leak would be gone and the question is valid, right? – Torbjörn Kalin Jan 08 '16 at 06:19
  • @Todd Bowles "I understand where you are coming from" -- So where am I coming from? – Simple Fellow Jan 11 '16 at 10:27

5 Answers5

13

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.

dcastro
  • 66,540
  • 21
  • 145
  • 155
  • Nice solution. Unfortunately for me is that it requires .NET 4.5. – Torbjörn Kalin Nov 19 '13 at 08:33
  • @TorbjörnKalin You can do the same thing on .Net 4.0, it's just more verbose. Have a look at `TaskCompletionSource` (also explained in Eren's answer). – svick Nov 19 '13 at 10:35
8

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.

Community
  • 1
  • 1
Ondrej Janacek
  • 12,486
  • 14
  • 59
  • 93
  • 6
    Unfortunately, `RunSynchronously` does not always run the task synchronously. It has [similar corner cases as `ExecuteSynchronously`](http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx). – Stephen Cleary Nov 19 '13 at 12:01
6

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;
Eren Ersönmez
  • 38,383
  • 7
  • 71
  • 92
5

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.

Community
  • 1
  • 1
Torbjörn Kalin
  • 1,976
  • 1
  • 22
  • 31
4

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());
}
Sriram Sakthivel
  • 72,067
  • 7
  • 111
  • 189
  • This actually ended up being my solution, creating my own TaskScheduler. With the other solutions, I ended up with other problems, such as not being able to run `ContinueWith()` on the main thread. This one worked all the way. – Torbjörn Kalin Nov 19 '13 at 11:59
  • @TorbjörnKalin Oh.. Why can't you use `ContinueWith` on main thread? – Sriram Sakthivel Nov 19 '13 at 12:02
  • I get an error when using `TaskScheduler.FromCurrentSynchronizationContext()`. Found a solution for that [here](http://stackoverflow.com/questions/8245926/the-current-synchronizationcontext-may-not-be-used-as-a-taskscheduler), but when using it the `ContinueWith()` call ended up on a different thread. Probably something I did wrong, but I got tired of trying... – Torbjörn Kalin Nov 19 '13 at 12:16