【问题标题】:Executing multiple tasks asynchronously in C#在 C# 中异步执行多个任务
【发布时间】:2017-06-30 22:43:16
【问题描述】:

环境

  • Windows 7
  • Visual Studio
  • C#

我想要做什么

我正在尝试构建一个应用来评估公司产品。为了安全起见,下面的描述有所抽象。

这个应用程序所做的是改变产品中的某个参数,看看产品的某个值是如何变化的。所以我需要做两件事。

  1. 每隔一段时间更改一次参数
  2. 以一定的间隔在文本框中显示值

图表是这样的。

这些任务应该一直重复,直到按下取消按钮。

用户界面具有以下控件:

  • button1:开始按钮
  • button2:取消按钮
  • textbox1 : 显示从设备获取的值

这是我写的代码。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    CancellationTokenSource cts = new CancellationTokenSource();

    private async void button1_Click(object sender, EventArgs e)
    {
       
        await Task1();
        await Task2();
 
    }


    private async Task Task1()
    {
        while (!cts.IsCancellationRequested)
        {
            Thread.Sleep(500);
            ChangeParameter(0);
            Thread.Sleep(1000);
            ChangeParameter(10);
            Thread.Sleep(500);
            ChangeParameter(0);
        }
    }

    private void ChangeParameter(double param)
    {
        // change device paremeter
        Console.WriteLine("devicep parameter changed : " + param);
    }

    private async Task Task2()
    {
        while (!cts.IsCancellationRequested)
        {
            Thread.Sleep(100);
            int data = GetDataFromDevice();
            UpdateTextBoxWithData(data);
        }

        cts.Token.ThrowIfCancellationRequested();
    }

    private int GetDataFromDevice()
    {
        //pseudo code
        var rnd = new Random();
        return rnd.Next(100);
    }

    private void UpdateTextBoxWithData(int data)
    {
        textBox1.AppendText(data.ToString() + "\n");
        // debug
        Console.WriteLine("data : " + data);
    }


    private void button2_Click(object sender, EventArgs e)
    {
        cts.Cancel();
    }


}

问题

但是,这段代码有两个问题。

  1. UI 冻结。
  2. Task2 永远不会执行。

第二个问题源自await,因为它一个一个地执行任务。我本可以使用 Task.Run() 但这不允许向 textBox 添加值,因为它与 UI 线程不同。

我该如何解决这些问题?任何帮助将不胜感激。

【问题讨论】:

  • await Task.Delay(500)替换Thread.Sleep(500)
  • 总是为非 UI 任务启动新线程,否则它们在 UI 线程上运行并从 UI 刷新中夺走处理能力。他们可以在运行完成时使用调用提醒 UI。
  • @Nyerguds 不要为非 UI 任务启动新线程。使用 Tasks、async/await、PLINQ 或 Dataflow,让框架调度作业
  • @CallumLinington 不,如果您使用 async/await 和 IProgress,您不需要需要调用
  • @Nyerguds 您正在混合线程和异步/等待。如果 OP 有任何异步代码,await 将确保执行将在异步代码完成后返回到 UI 线程。如果你使用await,又需要使用Invoke,那你就错了

标签: c# asynchronous


【解决方案1】:

首先,async 方法可能是虚幻的,因为它们不会神奇地使您的方法异步。相反,您可以将异步方法视为状态机的设置(请参阅详细说明 here),您可以在其中通过 await 调用安排操作链。

因此,您的异步方法必须尽可能快地执行。不要在这种设置方法中进行任何阻塞操作。如果您有要在异步方法中执行的阻塞操作,请通过await Task.Run(() => MyLongOperation()); 调用安排它。

例如,这将立即返回:

private async Task Task1()
{
    await Task.Run(() =>
    {
        while (!cts.IsCancellationRequested)
        {
            Thread.Sleep(500);
            ChangeParameter(0);
            Thread.Sleep(1000);
            ChangeParameter(10);
            Thread.Sleep(500);
            ChangeParameter(0);
        }
    }
}

小提示:其他人可能会建议使用Task.Delay 而不是Thread.Sleep。我会说只有当它是状态机配置的一部分时才使用Task.Delay。但是,如果延迟打算用作持久操作的一部分,而您不想拆分,则可以简单地留在Thread.Sleep

最后对这部分做一个说明:

private async void button1_Click(object sender, EventArgs e)
{
    await Task1();
    await Task2();
}

这会将您的任务配置为依次执行。如果您想并行执行它们,请这样做:

private async void button1_Click(object sender, EventArgs e)
{
    Task t1 = Task1();
    Task t2 = Task2();
    await Task.WhenAll(new[] { t1, t2 });
}

编辑:长期任务的额外说明:默认情况下,Task.Run 在池线程上执行任务。调度过多的并行和持久的任务可能会导致饥饿,整个应用程序可能会冻结很长时间。因此,对于持久操作,您可能希望使用 Task.Factory.StartNewTaskCreationOptions.LongRunning 选项而不是 Task.Run

// await Task.Run(() => LooongOperation(), token);
await Task.Factory.StartNew(() => LooongOperation(), token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

【讨论】:

  • 使用 Thread.Sleep 是一个的想法,因为它会浪费一个线程池线程很长时间。避免它不仅仅是一个建议。此外,Thread.Sleep 保证 执行是分开的,如果你的意思是线程将被驱逐并且 CPU 被其他代码使用。只有 spinwait 可以防止这种情况发生
  • @PanagiotisKanavos:OP 希望使用具有无限循环的持久任务,因此无论如何都不会释放线程。 spinwait 比 sleep 消耗更多的 CPU 并且不保证单核环境中的上下文变化(除非你使用SpinWait 结构)。
  • 我认为Thread.Sleep 的唯一问题是实际取消只会在按钮单击后 2 秒后才会取消,这可能会导致糟糕的用户体验。我们可以在每次小睡眠后添加IsCancellationRequested 调用,但这不会改变取消将比Task.Delay(token) 晚得多的事实
  • @MaximKosov:是的,这是一个更好的论点。对于用户来说,超过 100 毫秒的延迟已经很明显了。
  • 这与争论无关,这是一个严重的问题。在一个常见的桌面应用程序中,线程泄漏(因为就是这样)可能并不明显。在 Web 应用程序或执行大量异步工作(例如从许多设备读取数据)的应用程序中,这会导致大量 CPU 浪费。这意味着单个服务器最终会服务更少个请求,或者导致意外的应用程序池重新启动。物联网设备最终消耗的电量更多
【解决方案2】:

问题是您没有在任务中使用await,因此它们同步执行

你应该使用这样的东西来保持你的 UI 响应(注意这不是生产代码,我只是展示一个想法):

private void button1_Click(object sender, EventArgs e)
{
    try
    {
        await Task.WhenAll(Task1(cts.Token), Task2(cts.Token));
    }
    catch (TaskCancelledException ex)
    {
    }
}

private async Task Task1(CancellationToken token)
{
    while (true)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(500, token); // pass token to ensure delay canceled exactly when cancel is pressed
        ChangeParameter(0);
        await Task.Delay(1000, token);
        ChangeParameter(10);
        await Task.Delay(500, token);
        ChangeParameter(0);
    }
}

private async Task Task2(CancellationToken token)
{
    while (true)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(100, token);
        int data = await Task.Run(() => GetDataFromDevice()); //assuming this could be long running operation it shouldn't be on ui thread
        UpdateTextBoxWithData(data);
    }
}

基本上,当您需要在背景上运行某些东西时,您应该将其包装在Task.Run() 中,然后将其包装在await 中以获得结果。简单地将async 添加到您的方法不会使此方法异步。

为了使您的代码更清晰,我建议您将GetDataFromDeviceChangeParameter 等方法移至服务层。另外,请查看IProgress,因为 cmets 建议根据某些进程的进度更新您的 UI。

【讨论】:

    【解决方案3】:

    这段代码有很多问题:

    1. async/await 不会自动使代码异步。它允许您等待已经异步操作的结果。如果你想在后台运行一些非异步的东西,你需要使用 Task.Run 或类似的方法来启动一个任务。
    2. await 将执行返回到原始同步上下文。在这种情况下,UI 线程。通过使用 Thread.Sleep,您正在冻结 UI 线程
    3. 您不能从另一个线程更新 UI,这也适用于 Tasks。您可以使用 IProgress 界面来报告进度。很多 BCL 类都使用这个接口,就像 CancellationToken

    Maxim Kosov 已经清理了代码并展示了如何正确使用 async/await 和 Task.Run,​​所以我将发布如何使用 IProgress< T> 及其实现,Progress< T>

    IPProgress 用于通过IProgress< T>.Report 方法发布进度更新。它的默认实现 Progress,在 UI 线程上引发 ProgressChanged 事件和/或调用传递给其构造函数的 Action&lt;T&gt;。具体来说,是在创建类时捕获的同步上下文。

    您可以在构造函数或按钮单击事件中创建进度对象,例如

    private async void button1_Click(object sender, EventArgs e)
    {
        var progress=new Progress<int>(data=>UpdateTextBoxWithData(data));
    
        //...
        //Allow for cancellation of the task itself
        var token=cts.Token;
        await Task.Run(()=>MeasureInBackground(token,progress),token);
    
    }
    
    
    private async Task MeasureInBackground(CancellationToken token,IProgress<int> progress)
    {
        while (!token.IsCancellationRequested)
        {
            await Task.Delay(100,token);
    
            int data = GetDataFromDevice(); 
            progress.Report(data);
        }
    }
    

    请注意,在任务中使用Thread.Sleep 不是一个好主意,因为它会浪费线程池线程什么都不做。最好使用await Task.Delay(),它要求方法的签名更改为async Task。有一个Task.Run(Func) 重载就是为了这个目的。

    该方法与 Maxim Kosov 的代码略有不同,以表明 IProgress 确实可以跨线程进行通信。 IProgress 可以处理复杂的类,因此您可以返回进度百分比和消息,例如:

    private async Task MeasureInBackground(CancellationToken token,IProgress<Tuple<int,string>> progress)
    {
        while(!token.IsCancellationRequested)
        {
            await Task.Delay(100,token);
            int data = GetDataFromDevice(); 
            progress.Report(Tuple.Create(data,"Working"));
        }
        progress.Report(Tuple.Create(-1,"Cancelled!"));
    }
    

    在这里我只是懒惰并返回Tuple&lt;int,string&gt;。专门的进度类在生产代码中会更合适。

    使用 Action 的优点是您不需要管理事件处理程序,并且对象是异步方法的本地对象。清理由 .NET 自己执行。

    如果您的设备 API 提供真正的异步调用,则不需要 Task.Run。这意味着您不必在一个紧密的循环中浪费一个任务,例如:

    private async Task MeasureInBackground(CancellationToken token,IProgress<Tuple<int,string>> progress)
    {
        while(!token.IsCancellationRequested)
        {
            await Task.Delay(100, token);
            int data = await GetDataFromDeviceAsync(); 
            progress.Report(Tuple.Create(data,"Working"));
        }
        progress.Report(Tuple.Create(-1,"Cancelled!"));
    }
    

    大多数驱动程序使用称为完成端口的操作系统功能执行 IO 任务,本质上是在驱动程序完成操作时调用的回调。这样他们就不需要在等待网络、数据库或文件系统响应时阻塞。

    编辑

    在最后一个示例中,不再需要 Task.Run。只需使用await 就足够了:

    await MeasureInBackground(token,progress);
    

    【讨论】:

    • 我不明白为什么await Task.Run(()=&gt;MeasureInBackground(token,progress),token);。由于MeasureInBackgroundasync,初始任务将以MeasureInBackground 中的第一个await 结束。您要么需要使用async lambda,要么只需删除Task.Run 并调用await MeasureInBackground
    • @MaximKosov 不在此示例中。正如我所说,目的是展示IProgress&lt;T&gt; 如何从不同的线程工作。如果你可以在MeasureInBackground 中使用await,你就不需要IProgress&lt; T&gt;second 方法不需要Task.Run,只需await MeasureInBackground(...);
    • 您实际上在await Task.Delay() 的第一个示例中使用了 await :) 这不应该是Thread.Sleep吗?
    • Task.Run 不需要,除非您希望它在后台线程上运行
    • @MaximKosov no, Thread.Sleep 会浪费一个池线程。 await Task.Delay() 释放线程供其他任务使用
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-22
    相关资源
    最近更新 更多