Выполнение задачи по текущему потоку
Можно ли заставить задачу выполнить синхронно, в текущем потоке?
То есть, возможно ли это, например, передав некоторый параметр 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
можно найти здесь.