在C#

时间:2016-06-29 08:33:57

标签: c# asynchronous

环境

  • Windows 7
  • Visual Studio
  • C#

我正在尝试做什么

我正在尝试构建一个应用来评估公司产品。为安全起见,下面的描述在某种程度上是抽象的。

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

  1. 以特定间隔更改参数
  2. 以特定间隔显示文本框中的值
  3. 图表是这样的。

    enter image description here

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

    UI具有以下控件:

    • 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. 用户界面冻结。
    2. 任务2永远不会被执行。
    3. 第二个问题源自await,因为它逐个执行任务。我本可以使用Task.Run()但这不允许向textBox添加值,因为它与UI线程不同。

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

3 个答案:

答案 0 :(得分: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,因为评论建议根据某个流程的进度更新您的用户界面。

答案 1 :(得分:2)

此代码存在许多问题:

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

    Maxim Kosov已经清理了代码,并展示了如何正确使用async/await和Task.Run,​​因此我只会发布如何使用IProgress< T>及其实施,{{3} }

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

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

    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。为此目的,有一个ProgressChanged重载。

    该方法与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<int,string>。专门的进度类在生产代码中更合适。

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

答案 2 :(得分:2)

首先,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);