【问题标题】:What is correct way to combine long-running tasks with async / await pattern?将长时间运行的任务与异步/等待模式结合起来的正确方法是什么?
【发布时间】:2013-12-14 04:57:18
【问题描述】:

我有一个“高精度”计时器类,我需要能够启动、停止和暂停/恢复。为此,我将我在 Internet 上找到的几个不同示例结合在一起,但我不确定我是否正确使用了带有 asnyc / await 的 Tasks。

这是我的相关代码:

//based on http://haukcode.wordpress.com/2013/01/29/high-precision-timer-in-netc/
public class HighPrecisionTimer : IDisposable
{
    Task _task;
    CancellationTokenSource _cancelSource;

    //based on http://blogs.msdn.com/b/pfxteam/archive/2013/01/13/cooperatively-pausing-async-methods.aspx
    PauseTokenSource _pauseSource;

    Stopwatch _watch;
    Stopwatch Watch { get { return _watch ?? (_watch = Stopwatch.StartNew()); } }

    public bool IsPaused
    {
        get { return _pauseSource != null && _pauseSource.IsPaused; }
        private set
        {
            if (value)
            {
                _pauseSource = new PauseTokenSource();
            }
            else
            {
                _pauseSource.IsPaused = false;
            }
        }
    }

    public bool IsRunning { get { return !IsPaused && _task != null && _task.Status == TaskStatus.Running; } }

    public void Start()
    {
        if (IsPaused)
        {
            IsPaused = false;
        }
        else if (!IsRunning)
        {
            _cancelSource = new CancellationTokenSource();
            _task = new Task(ExecuteAsync, _cancelSource.Token, TaskCreationOptions.LongRunning);
            _task.Start();
        }
    }

    public void Stop()
    {
        if (_cancelSource != null)
        {
            _cancelSource.Cancel();
        }
    }

    public void Pause()
    {
        if (!IsPaused)
        {
            if (_watch != null)
            {
                _watch.Stop();
            }
        }

        IsPaused = !IsPaused;
    }

    async void ExecuteAsync()
    {
        while (!_cancelSource.IsCancellationRequested)
        {
            if (_pauseSource != null && _pauseSource.IsPaused)
            {
                await _pauseSource.Token.WaitWhilePausedAsync();
            }

            // DO CUSTOM TIMER STUFF...
        }

        if (_watch != null)
        {
            _watch.Stop();
            _watch = null;
        }

        _cancelSource = null;
        _pauseSource = null;
    }

    public void Dispose()
    {
        if (IsRunning)
        {
            _cancelSource.Cancel();
        }
    }
}

任何人都可以看看并提供一些关于我是否正确执行此操作的指示吗?

更新

我已经尝试根据下面的 Noseratio 的 cmets 修改我的代码,但我仍然无法弄清楚语法。每次尝试将 ExecuteAsync() 方法传递给 TaskFactory.StartNewTask.Run 都会导致编译错误,如下所示:

“以下方法或属性之间的调用不明确:TaskFactory.StartNew(Action, CancellationToken...) 和 TaskFactory.StartNew(Func, CancellationToken...)”。

最后,有没有一种方法可以指定 LongRunning TaskCreationOption 而无需提供 TaskScheduler?

async **Task** ExecuteAsync()
{
    while (!_cancelSource.IsCancellationRequested)
    {
        if (_pauseSource != null && _pauseSource.IsPaused)
        {
            await _pauseSource.Token.WaitWhilePausedAsync();
        }
        //...
    }
}

public void Start()
{
    //_task = Task.Factory.StartNew(ExecuteAsync, _cancelSource.Token, TaskCreationOptions.LongRunning, null);

    //_task = Task.Factory.StartNew(ExecuteAsync, _cancelSource.Token);

    //_task = Task.Run(ExecuteAsync, _cancelSource.Token);

}

更新 2

我想我已经缩小了范围,但仍然不确定正确的语法。这是否是创建任务的正确方法,以便消费者/调用代码继续执行,任务启动并在新的异步线程上启动?

_task = Task.Run(async () => await ExecuteAsync, _cancelSource.Token);

//**OR**

_task = Task.Factory.StartNew(async () => await ExecuteAsync, _cancelSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

【问题讨论】:

  • async () => await ExecuteAsync lambda 传递给Task.Factory.StartNew 并不能解决我在#1 中描述的问题。从本质上讲,它没有任何改变。查看我的答案的更新以获取正确语法和逻辑的示例。
  • 实际上,既然您已经将ExecuteAsync 的返回类型更改为Task,将async () => await ExecuteAsync lambda 传递给Task.Factory.StartNew 就可以了,但它是多余的。只需传递ExecuteAsync 并在Task.Factory.StartNew 返回的Task<Task> 对象上执行task.Unwrap

标签: c# xamarin.ios task-parallel-library async-await long-running-processes


【解决方案1】:

这里有几点:

  • async void 方法仅适用于异步事件处理程序 (more info)。你的async void ExecuteAsync() 立即返回(只要代码流到达其中的await _pauseSource)。本质上,您的_task 在那之后处于完成状态,而ExecuteAsync 的其余部分将在未观察到的情况下执行(因为它是void)。它甚至可能根本不会继续执行,这取决于您的主线程(以及进程)何时终止。

  • 鉴于此,您应该将其设为async Task ExecuteAsync(),并使用Task.RunTask.Factory.StartNew 而不是new Task 来启动它。因为您希望任务的操作方法为async,所以您将在这里处理嵌套任务,即Task<Task>Task.Run 会自动为您解包。更多信息可以在herehere找到。

  • PauseTokenSource 采用以下方法(按设计,AFAIU):代码的消费者端(调用Pause)实际上只请求暂停,但不同步。它将在Pause 之后继续执行,即使生产者端可能尚未达到等待状态,即await _pauseSource.Token.WaitWhilePausedAsync()。这对于您的应用程序逻辑可能没问题,但您应该意识到这一点。更多信息here

[UPDATE] 下面是使用Factory.StartNew 的正确语法。注意Task<Task>task.Unwrap。另请注意Stop 中的_task.Wait(),它用于确保Stop 返回时任务已完成(以类似于Thread.Join 的方式)。此外,TaskScheduler.Default 用于指示Factory.StartNew 使用线程池调度程序。如果您从另一个任务中创建 HighPrecisionTimer 对象,这一点很重要,该任务又是在具有非默认同步上下文的线程上创建的,例如一个 UI 线程(更多信息 herehere)。

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication
{
    public class HighPrecisionTimer
    {
        Task _task;
        CancellationTokenSource _cancelSource;

        public void Start()
        {
            _cancelSource = new CancellationTokenSource();

            Task<Task> task = Task.Factory.StartNew(
                function: ExecuteAsync, 
                cancellationToken: _cancelSource.Token, 
                creationOptions: TaskCreationOptions.LongRunning, 
                scheduler: TaskScheduler.Default);

            _task = task.Unwrap();
        }

        public void Stop()
        {
            _cancelSource.Cancel(); // request the cancellation

            _task.Wait(); // wait for the task to complete
        }

        async Task ExecuteAsync()
        {
            Console.WriteLine("Enter ExecuteAsync");
            while (!_cancelSource.IsCancellationRequested)
            {
                await Task.Delay(42); // for testing

                // DO CUSTOM TIMER STUFF...
            }
            Console.WriteLine("Exit ExecuteAsync");
        }
    }

    class Program
    {
        public static void Main()
        {
            var highPrecisionTimer = new HighPrecisionTimer();

            Console.WriteLine("Start timer");
            highPrecisionTimer.Start();

            Thread.Sleep(2000);

            Console.WriteLine("Stop timer");
            highPrecisionTimer.Stop();

            Console.WriteLine("Press Enter to exit...");
            Console.ReadLine();
        }
    }
}

【讨论】:

  • @JoshuaBarker,我已经用一些代码更新了我的答案,显示了Task.Factory.StartNew 的正确语法,以及更多的想法。一切都应该适用于 Mono 运行时环境。我相信他们非常严格地遵循 .NET 规范。
  • 所以,我最终创建了两个变量,一个用于Task,另一个用于Task。有趣的注释,但是当我在调试窗口中检查这两个变量时,它的“外部”任务包装器的状态为“正在运行”,而内部任务始终是“WaitingForActiviation”......所以现在调用暂停是有效的,因为我可以暂停/恢复我正在运行的线程,但是 iOS 或 MonoTouch 中的一些奇怪的东西会导致该线程从后台线程 (#5) 移动到主 UI 线程 (#1)...向前两步,向后一步.. .
  • 好的...感谢您的澄清。看起来这可能是 Xamarin 的任务实现中的一个错误(有关更多详细信息,请参阅 forums.xamarin.com/discussion/10858/…bugzilla.xamarin.com/show_bug.cgi?id=16548)。希望他们尽快解决这个问题。同时,感谢您的所有帮助!
  • _task.Wait()Stop()。这对我帮助很大!
  • 请注意,这可能无法按预期工作。在这个例子中,唯一被调用为LongRunningTask 是由Task.Factory.StartNew 创建的。这确实会导致默认调度程序在其自己的线程上调度该特定任务,但它不会在ExecuteAsync 上安排await,它只是调用它。所以发生的事情是ExecuteAsync 在该线程上运行,直到它遇到它的第一个await,此时线程返回执行父任务并返回。当ExecuteAsync 恢复时,它会在线程池线程上恢复,从而否定LongRunning 标志。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-05-07
  • 1970-01-01
  • 1970-01-01
  • 2015-04-10
  • 2014-09-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多