Execute task on current thread

2019-03-23 03:08发布

问题:

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().

回答1:

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.



回答2:

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.



回答3:

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;


回答4:

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.



回答5:

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());
}