Выполнение задачи по текущему потоку

Можно ли заставить задачу выполнить синхронно, в текущем потоке?

То есть, возможно ли это, например, передав некоторый параметр StartNew(), чтобы сделать этот код:

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

ведут себя так:

ThisShouldBeExecutedSynchronously();

Фон:

У меня есть интерфейс под названием IThreads:

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

Я хотел бы иметь две реализации этого, одну нормальную, которая использует потоки:

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

И тот, который не использует потоки (используется в некоторых сценариях тестирования):

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

Я мог бы позволить версии NoThreading просто вызвать func(), но я хочу вернуть экземпляр Task<TRet>, на котором я могу выполнять такие операции, как ContinueWith().

Ответы

Ответ 1

Планировщик задач решает, следует ли запускать задачу в новом потоке или в текущем потоке. Существует возможность принудительно запускать его в новом потоке, но никто не заставляет его работать в текущем потоке.

Но существует метод Task.RunSynchronously(), который

Выполняет задачу синхронно в текущем TaskScheduler.

Подробнее о MSDN.

Также, если вы используете async/await, там уже есть похожий вопрос.

Ответ 2

Вы можете просто вернуть результат func(), завернутый в Task.

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

Теперь вы можете приложить к ним задачи "продолжить".

Ответ 3

Поскольку вы упоминаете тестирование, вы можете использовать TaskCompletionSource<T>, так как он также позволяет вам устанавливать исключение или задавать задачу как отмененную ( работает в .Net 4 и 4.5):

Верните завершенную задачу с результатом:

var tcs = new TaskCompletionSource<TRet>();
tcs.SetResult(func());
return tcs.Task;

Возвратите неисправную задачу:

var tcs = new TaskCompletionSource<TRet>();
tcs.SetException(new InvalidOperationException());
return tcs.Task;

Вернуть отмененную задачу:

var tcs = new TaskCompletionSource<TRet>();
tcs.SetCanceled();
return tcs.Task;

Ответ 4

Да, вы можете в значительной степени сделать это с помощью настраиваемых планировщиков задач.

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

Ответ 5

OP здесь. Это мое окончательное решение (которое фактически решает намного больше, чем я спрашивал).

Я использую ту же реализацию для Threads как в тестах, так и в производстве, но передаю в разных 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);
    }
}

Я переношу Task в класс TaskContinuation, чтобы иметь возможность указать TaskScheduler для вызова ContinueWith().

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

Я создаю свой пользовательский TaskScheduler, который отправляет действие в поток, на котором был создан планировщик:

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

Теперь я могу указать поведение, передав в другом конструкторе TaskSchedulers конструктору Threads.

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

Наконец, поскольку цикл событий не запускается автоматически в моем unit test, я должен выполнить его вручную. Всякий раз, когда мне нужно дождаться завершения фоновой операции, я выполняю следующее (из основного потока):

DispatcherHelper.DoEvents();

DispatcherHelper можно найти здесь.