简体   繁体   English

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

[英]How to run a Task on a custom TaskScheduler using await?

I have some methods returning Task<T> on which I can await at will.我有一些返回Task<T>方法,我可以随意await I'd like to have those Tasks executed on a custom TaskScheduler instead of the default one.我希望在自定义TaskScheduler而不是默认任务上执行这些任务。

var task = GetTaskAsync ();
await task;

I know I can create a new TaskFactory (new CustomScheduler ()) and do a StartNew () from it, but StartNew () takes an action and create the Task , and I already have the Task (returned behind the scenes by a TaskCompletionSource )我知道我可以创建一个新的TaskFactory (new CustomScheduler ())并从中执行StartNew () ,但是StartNew ()执行操作并创建Task ,并且我已经有了Task (由TaskCompletionSource在幕后返回)

How can I specify my own TaskScheduler for await ?如何为await指定我自己的TaskScheduler

I think what you really want is to do a Task.Run , but with a custom scheduler.我认为你真正想要的是做一个Task.Run ,但使用自定义调度程序。 StartNew doesn't work intuitively with asynchronous methods; StartNew不适用于异步方法; Stephen Toub has a great blog post about the differences between Task.Run and TaskFactory.StartNew . Stephen Toub 有一篇很棒的博客文章,介绍Task.RunTaskFactory.StartNew之间的差异

So, to create your own custom Run , you can do something like this:因此,要创建自己的自定义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);
}

Then you can execute synchronous or asynchronous methods on your custom scheduler.然后,您可以在自定义调度程序上执行同步异步方法。

The TaskCompletionSource<T>.Task is constructed without any action and the scheduler is assigned on the first call to ContinueWith(...) (from Asynchronous Programming with the Reactive Framework and the Task Parallel Library — Part 3 ). TaskCompletionSource<T>.Task是在没有任何操作的情况下构建的,并且调度程序在第一次调用ContinueWith(...)被分配(来自使用响应式框架和任务并行库的异步编程 - 第 3 部分)。

Thankfully you can customize the await behavior slightly by implementing your own class deriving from INotifyCompletion and then using it in a pattern similar to await SomeTask.ConfigureAwait(false) to configure the scheduler that the task should start using in the OnCompleted(Action continuation) method (from await anything; ).值得庆幸的是,您可以通过实现从INotifyCompletion派生的自己的类,然后以类似于await SomeTask.ConfigureAwait(false)的模式使用它来稍微自定义 await 行为,以配置任务应在OnCompleted(Action continuation)方法中开始使用的调度程序(来自等待任何东西; )。

Here is the usage:这是用法:

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

Here is a simple implementation of ConfigureScheduler using a Task extension method with the important part in OnCompleted :这是使用 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() { }
    }
}

Here's a working sample that will compile as a console application:这是一个可以编译为控制台应用程序的工作示例:

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

There is no way to embed rich async features into a custom TaskScheduler .无法将丰富的异步功能嵌入到自定义TaskScheduler This class was not designed with async / await in mind.这个类的设计没有考虑到async / await The standard way to use a custom TaskScheduler is as an argument to the Task.Factory.StartNew method.使用自定义TaskScheduler的标准方法是作为Task.Factory.StartNew方法的参数。 This method does not understand async delegates.此方法不理解异步委托。 It is possible to provide an async delegate, but it is treated as any other delegate that returns some result.可以提供异步委托,但它被视为返回某些结果的任何其他委托。 To get the actual awaited result of the async delegate one must call Unwrap() to the task returned.要获得异步委托的实际等待结果,必须对返回的任务调用Unwrap()

This is not the problem though.不过这不是问题。 The problem is that the TaskScheduler infrastructure does not treat the async delegate as a single unit of work.问题在于TaskScheduler基础结构不会将异步委托视为单个工作单元。 Each task is split into multiple mini-tasks (using every await as a separator), and each mini-task is processed individually.每个任务被拆分成多个小任务(使用每个await作为分隔符),每个小任务单独处理。 This severely restricts the asynchronous functionality that can be implemented on top of this class.这严重限制了可以在此类之上实现的异步功能。 As an example here is a custom TaskScheduler that is intended to queue the supplied tasks one at a time (to limit the concurrency in other words):作为一个例子,这里是一个自定义的TaskScheduler ,它旨在一次将提供的任务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; }
}

The SemaphoreSlim should ensure that only one Task would run at a time. SemaphoreSlim应该确保一次只运行一个Task Unfortunately it doesn't work.不幸的是它不起作用。 The semaphore is released prematurely, because the Task passed in the call QueueTask(task) is not the task that represents the whole work of the async delegate, but only the part until the first await .信号量过早释放,因为调用QueueTask(task)传递的Task并不是代表 async 委托的全部工作的任务,而只是第一个await之前的部分。 The other parts are passed to the TryExecuteTaskInline method.其他部分传递给TryExecuteTaskInline方法。 There is no way to correlate these task-parts, because no identifier or other mechanism is provided.无法关联这些任务部分,因为没有提供标识符或其他机制。 Here is what happens in practice:以下是实践中发生的事情:

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

Output:输出:

05:29:58.346 Item 1 Started 05:29:58.346 项目 1 开始
05:29:58.358 Item 2 Started 05:29:58.358 第 2 项开始
05:29:58.358 Item 3 Started 05:29:58.358 第 3 项开始
05:29:58.358 Item 4 Started 05:29:58.358 第 4 项开始
05:29:58.358 Item 5 Started 05:29:58.358 第 5 项开始
05:29:59.358 Item 1 Finished 05:29:59.358 第 1 项完成
05:29:59.374 Item 5 Finished 05:29:59.374 第 5 项完成
05:29:59.374 Item 4 Finished 05:29:59.374 第 4 项完成
05:29:59.374 Item 2 Finished 05:29:59.374 第 2 项完成
05:29:59.374 Item 3 Finished 05:29:59.374 第 3 项完成

Disaster, all tasks are queued at once.灾难,所有任务同时排队。

Conclusion: Customizing the TaskScheduler class is not the way to go when advanced async features are required.结论:当需要高级异步功能时,自定义TaskScheduler类不是TaskScheduler的方法。

Update: Here is another observation, regarding custom TaskScheduler s in the presence of an ambient SynchronizationContext .更新:这是另一个观察,关于在存在环境SynchronizationContext的情况下自定义TaskScheduler The await mechanism by default captures the current SynchronizationContext , or the current TaskScheduler , and invokes the continuation on either the captured context or the scheduler.默认情况下, await机制会捕获当前SynchronizationContext或当前TaskScheduler ,并在捕获的上下文或调度程序上调用延续。 If both are present, the current SynchronizationContext is preferred, and the current TaskScheduler is ignored.如果两者都存在,则首选当前SynchronizationContext ,并忽略当前TaskScheduler Below is a demonstration of this behavior, in a WinForms application¹:下面是这种行为的演示,在 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();
}

Clicking the button causes two messages to popup sequentially, with this information:单击该按钮会依次弹出两条消息,其中包含以下信息:

1, System.Threading.Tasks.SynchronizationContextTaskScheduler 1、System.Threading.Tasks.SynchronizationContextTaskScheduler

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

This experiment shows that only the first part of the asynchronous delegate, the part before the first await , was scheduled on the non-default scheduler.这个实验表明,只有异步委托的第一部分,即第一个await之前的部分,被安排在非默认调度程序上。 This behavior limits even further the practical usefulness of custom TaskScheduler s in an async/await-enabled environment.这种行为进一步限制了自定义TaskScheduler在启用 async/await 的环境中的实际用途。

¹ Windows Forms applications have a WindowsFormsSynchronizationContext installed automatically, when the Application.Run method is called. ¹ Windows 窗体应用程序会在调用Application.Run方法时自动安装WindowsFormsSynchronizationContext

After the comments it looks like you want to control the scheduler on which the code after the await is run.在评论之后,您似乎想要控制等待后运行代码的调度程序。

The compile creates a continuation from the await that runs on the current SynchronizationContext by default.默认情况下,编译从在当前 SynchronizationContext 上运行的 await 创建一个延续。 So your best shot is to set up the SynchronizationContext before calling await.所以你最好的办法是在调用 await 之前设置SynchronizationContext

There are some ways to await a specific context.有一些方法可以等待特定的上下文。 See Configure Await from Jon Skeet, especially the part about SwitchTo, for more information on how to implement something like this.请参阅 Jon Skeet 的Configure Await,尤其是有关 SwitchTo 的部分,以获取有关如何实现此类内容的更多信息。

EDIT: The SwitchTo method from TaskEx has been removed, as it was too easy to misuse.编辑:来自 TaskEx 的 SwitchTo 方法已被删除,因为它太容易被误用。 See the MSDN Forum for reasons.有关原因,请参阅MSDN 论坛

Can you fit for this method call:你能适应这个方法调用吗:

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

Faced with same issue, tried to use LimitedConcurrencyLevelTaskScheduler , but it does not support async tasks.面对同样的问题,尝试使用LimitedConcurrencyLevelTask​​Scheduler ,但它不支持异步任务。 So...所以...

Just wrote my own small simple Scheduler, that allow to run async Tasks based on global ThreadPool (and Task.Run method) with ability to limit current max degree of parallelism.刚刚编写了我自己的小型简单调度程序,它允许运行基于全局 ThreadPool(和 Task.Run 方法)的异步任务,并具有限制当前最大并行度的能力。 It is enough for my exact purposes, maybe will also help you, guys.这对于我的确切目的来说已经足够了,也许也会对你们有所帮助,伙计们。

Main demo code (console app, dotnet core 3.1) :主要演示代码(控制台应用程序,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);


    }

Supporting code (place it in the same place) :支持代码(放在同一个地方):

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

Scheduler :调度程序:

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

Few notes :几个注意事项:

  1. First - check comments to it, maybe it is the worst code ever!首先 - 检查对它的评论,也许这是有史以来最糟糕的代码!
  2. Did not test in prod没有在产品中测试
  3. Did not implement cancellation tokens and any other functionality, that should be there, but i'm too lazy.没有实现取消令牌和任何其他功能,应该在那里,但我太懒了。 Sorry对不起

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM