【问题标题】:How to run a Task on a custom TaskScheduler using await?如何使用 await 在自定义 TaskScheduler 上运行任务?
【发布时间】:2013-03-03 22:38:08
【问题描述】:

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

var task = GetTaskAsync ();
await task;

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

如何为await 指定我自己的TaskScheduler

【问题讨论】:

  • 不等待一个热的(也就是已经运行的)任务? TaskCompletionSource 中的任务也不能“运行”,因为它负责通过调用 SetResult 来完成任务。您的意思是如何指定运行 await 延续的调度程序?
  • 任务处于热运行状态,由 CTS 返回。但是在SetResult 和等待之后的继续之间发生了一些调度。如果可能,我需要使用自己的调度程序来控制该调度
  • @StephaneDelcroix 你不能在你的调度器上运行整个方法吗?我认为这将是最好的解决方案。
  • 从您的 cmets 中,我了解到您希望使用特定的调度程序来继续初始任务,而不是初始任务本身,因为它已经很热并且正在运行。对于配置延续,您可能需要查看自定义等待器实现。我认为您应该能够编写类似于“await GetTaskAsync().ContinueIn(MyScheduler)”的内容

标签: c# .net async-await


【解决方案1】:

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

编译从默认情况下在当前 SynchronizationContext 上运行的 await 创建一个延续。所以最好的办法是在调用 await 之前设置 SynchronizationContext

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

编辑: TaskEx 中的 SwitchTo 方法已被删除,因为它太容易被误用。原因见MSDN Forum

【讨论】:

    【解决方案2】:

    我认为您真正想要的是做一个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);
    }
    

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

    【讨论】:

    • 很抱歉,真正的 Task 工作安排在默认的 ThreadPoolTaskScheduler 上——您的 MyTaskScheduler 仅用于安排内部任务。
    • @springy76:不,这是不正确的。 MyTaskScheduler 将运行func,在异步func 的情况下,任何awaits 将默认在MyTaskScheduler 上恢复。
    • 我以LimitedConcurrencyLevelTaskScheduler 为例,将其修改为后进先出。但是所有任务都以 FIFO 顺序执行并且不受限制,但是TaskScheduler.Current 在任何可能的调试点返回我的自定义调度程序。当与Parallel.ForEach 一起使用或使用myTaskFactory.StartNew() 执行真正的Func&lt;TResult&gt; 时,调度程序按预期工作——但Func&lt;Task&lt;TResult&gt;&gt;Unwrap() 的组合甚至不执行第一个await 之前的代码既没有正确的顺序,也没有有限的并发,说调度程序被完全忽略了。
    • @springy76:这不是预期的行为。我建议您将一个最小的、可重现的示例作为单独的问题发布。
    • @springy76: 好吧,我希望有点奇怪 - 具体来说,发送到线程调度程序的异步方法在运行时“离开”FIFO队列,并且当它们准备好在@之后恢复时987654343@,他们将不得不重新进入 FIFO 队列(最后)。所以行为不会很明显,但它肯定应该使用该任务调度程序运行它们。
    【解决方案3】:

    TaskCompletionSource&lt;T&gt;.Task 是在没有任何操作和调度程序的情况下构造的 在第一次调用ContinueWith(...)(来自Asynchronous Programming with the Reactive Framework and the Task Parallel Library — Part 3)时分配。

    幸运的是,您可以通过实现您自己的派生自 INotifyCompletion 的类,然后以类似于 await SomeTask.ConfigureAwait(false) 的模式使用它来配置任务应在 OnCompleted(Action continuation) 方法中开始使用的调度程序,从而稍微自定义等待行为(来自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);
            }
        }
    }
    

    【讨论】:

    • 虽然可能这样做,但在生产代码中通常不是一个好主意。最初的 CTP 有一个 SwitchTo 操作符,其行为方式大致相同,但由于它鼓励非结构化程序流而被删除,这反过来又导致了复杂性和陷阱。例如,如果您在try 中使用修改后的await,那么您的finally 块必须能够在原始调度程序或修改后的调度程序上执行(并且finally 代码无法切换调度程序,因为await 是不允许的)。
    • @StephenCleary 现在 catch/finally 块可以等待(c# 6),这仍然是个坏主意吗?
    • @Kryptos:是的,这仍然是个坏主意。在catch/finally 中启用awaitSwitchTo 行为没有影响。
    • @StephenCleary 你的意思是使用任何自定义等待者是一个坏主意吗?还是只是这个实现?
    • @Erasmus:就是这个实现。
    【解决方案4】:

    你能适应这个方法调用吗:

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

    【讨论】:

      【解决方案5】:

      无法将丰富的异步功能嵌入到自定义 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));
                  await task;
              }
              finally
              {
                  _semaphore.Release();
              }
          }
      
          protected override bool TryExecuteTaskInline(Task task,
              bool taskWasPreviouslyQueued) => false;
      
          protected override IEnumerable<Task> GetScheduledTasks() { yield break; }
      }
      

      SemaphoreSlim 应确保一次只能运行一个 Task。不幸的是,它不起作用。信号量过早释放,因为在调用QueueTask(task) 中传递的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);
      

      输出:

      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 项完成

      灾难,所有任务一次排队。

      结论:当需要高级异步功能时,自定义 TaskScheduler 类不是可行的方法。

      更新:这是另一个观察结果,关于在环境 SynchronizationContext 存在下的自定义 TaskSchedulers。 await 机制默认捕获当前SynchronizationContext 或当前TaskScheduler,并在捕获的上下文中调用延续 或调度程序。如果两者都存在,则首选当前的SynchronizationContext,而忽略当前的TaskScheduler。下面是此行为的演示,在 WinForms 应用程序中¹:

      private async void Button1_Click(object sender, EventArgs e)
      {
          await Task.Factory.StartNew(async () =>
          {
              MessageBox.Show($"{Thread.CurrentThread.ManagedThreadId}, {TaskScheduler.Current}");
              await Task.Delay(1000);
              MessageBox.Show($"{Thread.CurrentThread.ManagedThreadId}, {TaskScheduler.Current}");
          }, default, TaskCreationOptions.None,
              TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
      }
      

      单击该按钮会依次弹出两条消息,其中包含以下信息:

      1、System.Threading.Tasks.SynchronizationContextTaskScheduler

      1、System.Threading.Tasks.ThreadPoolTask​​Scheduler

      这个实验表明,只有异步委托的第一部分,即第一个 await 之前的部分,在非默认调度程序上被调度。 这种行为进一步限制了自定义 TaskSchedulers 在启用异步/等待的环境中的实际用途。

      ¹ 当调用Application.Run 方法时,Windows 窗体应用程序会自动安装WindowsFormsSynchronizationContext

      【讨论】:

        【解决方案6】:

        遇到同样的问题,尝试使用 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. 首先 - 检查它的 cmets,也许这是有史以来最糟糕的代码!
        2. 没有在 prod 中测试
        3. 没有实现取消令牌和任何其他功能,应该有,但我太懒了。对不起

        【讨论】:

        • 仅供参考,.NET 平台内置了一个LimitedConcurrencyLevelTaskScheduler。它是:var scheduler = new ConcurrentExclusiveSchedulerPair(TaskScheduler.Default, 5).ConcurrentScheduler;
        • @TheodorZoulias,据我所知,LimitedConcurrencyLevelTask​​Scheduler 是在 async/await 之前设计的,所以它不支持正确使用它?
        • 是的,所有TaskSchedulers 在处理异步方法时都是无用的。他们安排在等待的操作之间运行的代码,并且他们完全不知道等待的操作本身。您的ThreadPoolConcurrentScheduler 课程是否以某种方式解决了这个问题?
        • 哦,我明白了。您的 ThreadPoolConcurrentScheduler 不是 TaskScheduler。这是一个安排异步委托Func&lt;Task&gt;,而不是任务的类。我建议将参数Func&lt;Task&gt; task更改为Func&lt;Task&gt; taskFactoryFunc&lt;Task&gt; actionAsync,因为它的当前名称令人困惑。
        • 顺便说一句,如何限制异步操作​​的并发问题已经有很多解决方案,例如herehere。这里的这个问题专门询问如何结合TaskSchedulers 和await,所以你的答案可能不太合适。
        猜你喜欢
        • 2016-06-27
        • 2011-07-02
        • 1970-01-01
        • 1970-01-01
        • 2019-12-22
        • 1970-01-01
        • 2014-03-24
        • 2015-03-16
        • 2013-12-03
        相关资源
        最近更新 更多