如何使用await在自定义TaskScheduler上运行任务?

时间:2013-03-15 09:20:23

标签: c# .net async-await

我有一些返回Task<T>的方法,我可以随意await。我希望在自定义TaskScheduler上执行这些任务而不是默认任务。

var task = GetTaskAsync ();
await task;

我知道我可以创建一个新的TaskFactory (new CustomScheduler ())并从中StartNew ()执行StartNew (),但是Task会采取行动并创建Task,我已经拥有了TaskCompletionSource {1}}(由TaskScheduler

后面的场景返回)

如何为await指定自己的{{1}}?

6 个答案:

答案 0 :(得分:39)

我认为您真正想要的是Task.Run,但使用自定义调度程序。 StartNew不能直观地使用异步方法; Stephen Toub有一篇关于the differences between Task.Run and TaskFactory.StartNew的精彩博文。

因此,要创建自己的自定义Run,您可以执行以下操作:

private static readonly TaskFactory myTaskFactory = new TaskFactory(
    CancellationToken.None, TaskCreationOptions.DenyChildAttach,
    TaskContinuationOptions.None, new MyTaskScheduler());
private static Task RunOnMyScheduler(Func<Task> func)
{
  return myTaskFactory.StartNew(func).Unwrap();
}
private static Task<T> RunOnMyScheduler<T>(Func<Task<T>> func)
{
  return myTaskFactory.StartNew(func).Unwrap();
}
private static Task RunOnMyScheduler(Action func)
{
  return myTaskFactory.StartNew(func);
}
private static Task<T> RunOnMyScheduler<T>(Func<T> func)
{
  return myTaskFactory.StartNew(func);
}

然后,您可以在自定义调度程序上执行同步异步方法。

答案 1 :(得分:9)

构造TaskCompletionSource<T>.Task时没有任何操作和调度程序 是在第一次致电ContinueWith(...)(来自Asynchronous Programming with the Reactive Framework and the Task Parallel Library — Part 3)时分配的。

值得庆幸的是,您可以通过实现从INotifyCompletion派生自己的类,然后以类似于await SomeTask.ConfigureAwait(false)的模式使用它来配置任务应该在OnCompleted(Action continuation)开始使用的调度程序,从而稍微自定义await行为。 {1}}方法(来自await anything;)。

以下是用法:

    TaskCompletionSource<object> source = new TaskCompletionSource<object>();

    public async Task Foo() {
        // Force await to schedule the task on the supplied scheduler
        await SomeAsyncTask().ConfigureScheduler(scheduler);
    }

    public Task SomeAsyncTask() { return source.Task; }

以下是使用任务扩展方法的ConfigureScheduler的简单实现,其中包含OnCompleted中的重要部分:

public static class TaskExtension {
    public static CustomTaskAwaitable ConfigureScheduler(this Task task, TaskScheduler scheduler) {
        return new CustomTaskAwaitable(task, scheduler);
    }
}

public struct CustomTaskAwaitable {
    CustomTaskAwaiter awaitable;

    public CustomTaskAwaitable(Task task, TaskScheduler scheduler) {
        awaitable = new CustomTaskAwaiter(task, scheduler);
    }

    public CustomTaskAwaiter GetAwaiter() { return awaitable; }

    public struct CustomTaskAwaiter : INotifyCompletion {
        Task task;
        TaskScheduler scheduler;

        public CustomTaskAwaiter(Task task, TaskScheduler scheduler) {
            this.task = task;
            this.scheduler = scheduler;
        }

        public void OnCompleted(Action continuation) {
            // ContinueWith sets the scheduler to use for the continuation action
            task.ContinueWith(x => continuation(), scheduler);
        }

        public bool IsCompleted { get { return task.IsCompleted; } }
        public void GetResult() { }
    }
}

这是一个可以作为控制台应用程序编译的工作示例:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;

namespace Example {
    class Program {
        static TaskCompletionSource<object> source = new TaskCompletionSource<object>();
        static TaskScheduler scheduler = new CustomTaskScheduler();

        static void Main(string[] args) {
            Console.WriteLine("Main Started");
            var task = Foo();
            Console.WriteLine("Main Continue ");
            // Continue Foo() using CustomTaskScheduler
            source.SetResult(null);
            Console.WriteLine("Main Finished");
        }

        public static async Task Foo() {
            Console.WriteLine("Foo Started");
            // Force await to schedule the task on the supplied scheduler
            await SomeAsyncTask().ConfigureScheduler(scheduler);
            Console.WriteLine("Foo Finished");
        }

        public static Task SomeAsyncTask() { return source.Task; }
    }

    public struct CustomTaskAwaitable {
        CustomTaskAwaiter awaitable;

        public CustomTaskAwaitable(Task task, TaskScheduler scheduler) {
            awaitable = new CustomTaskAwaiter(task, scheduler);
        }

        public CustomTaskAwaiter GetAwaiter() { return awaitable; }

        public struct CustomTaskAwaiter : INotifyCompletion {
            Task task;
            TaskScheduler scheduler;

            public CustomTaskAwaiter(Task task, TaskScheduler scheduler) {
                this.task = task;
                this.scheduler = scheduler;
            }

            public void OnCompleted(Action continuation) {
                // ContinueWith sets the scheduler to use for the continuation action
                task.ContinueWith(x => continuation(), scheduler);
            }

            public bool IsCompleted { get { return task.IsCompleted; } }
            public void GetResult() { }
        }
    }

    public static class TaskExtension {
        public static CustomTaskAwaitable ConfigureScheduler(this Task task, TaskScheduler scheduler) {
            return new CustomTaskAwaitable(task, scheduler);
        }
    }

    public class CustomTaskScheduler : TaskScheduler {
        protected override IEnumerable<Task> GetScheduledTasks() { yield break; }
        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return false; }
        protected override void QueueTask(Task task) {
            TryExecuteTask(task);
        }
    }
}

答案 2 :(得分:3)

在评论之后,您似乎想要控制运行await之后代码的调度程序。

编译器默认从当前SynchronizationContext上运行的await创建一个延续。因此,最好的方法是在呼叫等待之前设置SynchronizationContext

有一些方法可以等待特定的上下文。请参阅Jon Skeet的Configure Await,特别是有关SwitchTo的部分,了解有关如何实现此类内容的更多信息。

编辑: TaskEx中的SwitchTo方法已被删除,因为它太容易被滥用。请参阅MSDN Forum了解原因。

答案 3 :(得分:3)

您是否适合此方法调用:

  await Task.Factory.StartNew(
        () => { /* to do what you need */ }, 
        CancellationToken.None, /* you can change as you need */
        TaskCreationOptions.None, /* you can change as you need */
        customScheduler);

答案 4 :(得分:1)

无法将丰富的异步功能嵌入到自定义TaskScheduler中。此类设计时并未考虑async / await。使用自定义TaskScheduler的标准方法是作为Task.Factory.StartNew方法的参数。此方法不了解异步委托。可以提供一个异步委托,但是它将被视为返回某些结果的任何其他委托。要获得异步委托的实际等待结果,必须对返回的任务调用Unwrap()。但这不是问题。问题在于TaskScheduler基础结构未将异步委托视为单个工作单元。它将每个任务分成多个迷你任务(使用每个await作为分隔符),并且每个迷你任务都被单独处理。这严重限制了可以在此类顶部实现的异步功能。例如,下面是一个自定义TaskScheduler,该自定义public class MyTaskScheduler : TaskScheduler { private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); protected async override void QueueTask(Task task) { await _semaphore.WaitAsync(); try { await Task.Run(() => base.TryExecuteTask(task)); } finally { _semaphore.Release(); } } protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) => base.TryExecuteTask(task); protected override IEnumerable<Task> GetScheduledTasks() { yield break; } } 用于一次将提供的任务排队(以限制并发性):

SemaphoreSlim

Task应该确保一次只能运行一个Task。不幸的是,它不起作用。信号量过早释放,因为在调用QueueTask(task)中传递的await不是代表异步委托的全部工作的任务,而是代表直到第一个TryExecuteTaskInline为止的部分。其他部分将传递给var taskScheduler = new MyTaskScheduler(); var tasks = Enumerable.Range(1, 5).Select(n => Task.Factory.StartNew(async () => { Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Item {n} Started"); await Task.Delay(1000); Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Item {n} Finished"); }, default, TaskCreationOptions.None, taskScheduler)) .Select(t => t.Unwrap()) .ToArray(); Task.WaitAll(tasks); 方法。由于没有提供标识符或其他机制,因此无法关联这些任务部分。这是实际发生的情况:

TaskScheduler

输出:

  

05:29:58.346项目1已开始
  05:29:58.358项目2已开始
  05:29:58.358项目3已开始
  05:29:58.358项目4已开始
  05:29:58.358项目5已开始
  05:29:59.358项目1已完成
  05:29:59.374项目5已完成
  05:29:59.374项目4已完成
  05:29:59.374项目2已完成
  05:29:59.374项目3已完成

灾难,所有任务都立即排队。

结论:在需要高级异步功能时,自定义{{1}}类是不可行的。

答案 5 :(得分:-1)

面对同样的问题,尝试使用LimitedConcurrencyLevelTask​​Scheduler,但它不支持异步任务。所以...

刚刚编写了我自己的小型简单调度程序,它允许运行基于全局 ThreadPool(和 Task.Run 方法)的异步任务,并具有限制当前最大并行度的能力。这对于我的确切目的来说已经足够了,也许也会帮助你们,伙计们。

主要演示代码(控制台应用程序,dotnet core 3.1):

    static async Task Main(string[] args)
    {

        //5 tasks to run per time
        int concurrentLimit = 5;
        var scheduler = new ThreadPoolConcurrentScheduler(concurrentLimit);

        //catch all errors in separate event handler
        scheduler.OnError += Scheduler_OnError;

        // just monitor "live" state and output to console
        RunTaskStateMonitor(scheduler);

        // simulate adding new tasks "on the fly"
        SimulateAddingTasksInParallel(scheduler);

        Console.WriteLine("start adding 50 tasks");

        //add 50 tasks
        for (var i = 1; i <= 50; i++)
        {
            scheduler.StartNew(myAsyncTask);
        }

        Console.WriteLine("50 tasks added to scheduler");

        Thread.Sleep(1000000);


    }

支持代码(放在同一个地方):

    private static void Scheduler_OnError(Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }

    private static int currentTaskFinished = 0;

    //your sample of async task
    static async Task myAsyncTask()
    {
        Console.WriteLine("task started ");

        using (HttpClient httpClient = new HttpClient())
        {
            //just make http request to ... wikipedia!
            //sorry, Jimmy Wales! assume,guys, you will not DDOS wiki :)
            var uri = new Uri("https://wikipedia.org/");
            var response = await httpClient.GetAsync(uri);
            string result = await response.Content.ReadAsStringAsync();
            if (string.IsNullOrEmpty(result))
                Console.WriteLine("error, await is not working");
            else
                Console.WriteLine($"task result : site length is {result.Length}");
        }
        //or simulate it using by sync sleep
        //Thread.Sleep(1000);
        //and for tesing exception : 
        //throw new Exception("my custom error");
        Console.WriteLine("task finished ");

        //just incrementing total ran tasks to output in console
        Interlocked.Increment(ref currentTaskFinished);
    }

    static void SimulateAddingTasksInParallel(ThreadPoolConcurrentScheduler taskScheduler)
    {
        int runCount = 0;
        Task.Factory.StartNew(() =>
        {
            while (true)
            {
                runCount++;

                if (runCount > 5)
                    break;

                //every 10 sec 5 times
                Thread.Sleep(10000);

                //adding new 5 tasks from outer task
                Console.WriteLine("start adding new 5 tasks!");
                for (var i = 1; i <= 5; i++)
                {
                    taskScheduler.StartNew(myAsyncTask);
                }

                Console.WriteLine("new 5 tasks added!");
            }
        }, TaskCreationOptions.LongRunning);
    }

    static void RunTaskStateMonitor(ThreadPoolConcurrentScheduler taskScheduler)
    {
        int prev = -1;
        int prevQueueSize = -1;
        int prevFinished = -1;
        Task.Factory.StartNew(() =>
            {
                while (true)
                {
                    // getting current thread count in working state
                    var currCount = taskScheduler.GetCurrentWorkingThreadCount();
                    // getting inner queue state
                    var queueSize = taskScheduler.GetQueueTaskCount();

                    //just output overall state if something changed
                    if (prev != currCount || queueSize != prevQueueSize || prevFinished != currentTaskFinished)
                    {
                        Console.WriteLine($"Monitor : running tasks:{currCount}, queueLength:{queueSize}. total Finished tasks : " + currentTaskFinished);
                        prev = currCount;
                        prevQueueSize = queueSize;
                        prevFinished = currentTaskFinished;
                    }

                    // check it every 10 ms
                    Thread.Sleep(10);
                }
            }
            , TaskCreationOptions.LongRunning);
    }

调度程序:

public class ThreadPoolConcurrentScheduler
{
    private readonly int _limitParallelThreadsCount;
    private int _threadInProgressCount = 0;

    public delegate void onErrorDelegate(Exception ex);
    public event onErrorDelegate OnError;

    private ConcurrentQueue<Func<Task>> _taskQueue;
    private readonly object _queueLocker = new object();


    public ThreadPoolConcurrentScheduler(int limitParallelThreadsCount)
    {
        //set maximum parallel tasks to run
        _limitParallelThreadsCount = limitParallelThreadsCount;
        // thread-safe queue to store tasks
        _taskQueue = new ConcurrentQueue<Func<Task>>();
    }

    //main method to start async task
    public void StartNew(Func<Task> task)
    {
        lock (_queueLocker)
        {
            // checking limit
            if (_threadInProgressCount >= _limitParallelThreadsCount)
            {
                //waiting new "free" threads in queue
                _scheduleTask(task);
            }
            else
            {
                _startNewTask(task);
            }
        }
    }

    private void _startNewTask(Func<Task> task)
    {
        Interlocked.Increment(ref _threadInProgressCount);
        Task.Run(async () =>
        {
            try
            {
                await task();
            }
            catch (Exception e)
            {
                //Console.WriteLine(e);
                OnError?.Invoke(e);
            }
        }).ContinueWith(_onTaskEnded);
    }

    //will be called on task end
    private void _onTaskEnded(Task task)
    {
        lock (_queueLocker)
        {
            Interlocked.Decrement(ref _threadInProgressCount);
            //queue has more priority, so if thread is free - let's check queue first
            if (!_taskQueue.IsEmpty)
            {
                if (_taskQueue.TryDequeue(out var result))
                {
                    _startNewTask(result);
                }
            }
        }
    }

    private void _scheduleTask(Func<Task> task)
    {
        _taskQueue.Enqueue(task);
    }

    //returning in progress task count 
    public int GetCurrentWorkingThreadCount()
    {
        return _threadInProgressCount;
    }

    //return number of tasks waiting to run
    public int GetQueueTaskCount()
    {
        lock (_queueLocker) return _taskQueue.Count;
    }
}

一些注意事项:

  1. 首先 - 检查对它的评论,也许这是有史以来最糟糕的代码!
  2. 未在产品中进行测试
  3. 没有实现取消令牌和任何其他应该存在的功能,但我太懒了。对不起