走进异步编程的世界之GUI 中执行异步操作

Posted DotNet

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了走进异步编程的世界之GUI 中执行异步操作相关的知识,希望对你有一定的参考价值。


来源:反骨仔

链接:cnblogs.com/liqingwen/p/5877042.html


这是继后的第三篇。主要介绍在 WinForm 中如何执行异步操作。


一、在 WinForm 程序中执行异步操作


下面通过窗体示例演示以下操作-点击按钮后:


①将按钮禁用,并将标签内容改成:“Doing”(表示执行中);


②线程挂起3秒(模拟耗时操作);


③启用按钮,将标签内容改为:“Complete”(表示执行完成)。


public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

        private void btnDo_Click(object sender, EventArgs e)

        {

            btnDo.Enabled = false;

            lblText.Text = @"Doing";

            Thread.Sleep(3000);

            btnDo.Enabled = true;

            lblText.Text = @"Complete";

        }

    }


可是执行结果却是:



【发现的问题】


①好像没有变成“Doing”?


②并且拖动窗口的时候卡住不动了?


③3秒后突然变到想拖动到的位置?


④同时文本变成“Complete”?

 

【分析】GUI 程序在设计中要求所有的显示变化都必须在主 GUI 线程中完成,如点击事件和移动窗体。Windows 程序时通过 消息来实现,消息放入消息泵管理的消息队列中。点击按钮时,按钮的Click消息放入消息队列。消息泵从队列中移除该消息,并开始处理点击事件的代码,即 btnDo_Click 事件的代码。


btnDo_Click 事件会将触发行为的消息放入队列,但在 btnDo_Click 时间处理程序完全退出前(线程挂起 3 秒退出前),消息都无法执行。


(3 秒后)接着所有行为都发生了,但速度太快肉眼无法分辨才没有发现标签改成“Doing”。


走进异步编程的世界之GUI 中执行异步操作


图1-2 点击事件

走进异步编程的世界之GUI 中执行异步操作


图1-3 点击事件具体执行过程

  

现在我们加入 async/await 特性。


public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

        private async void btnDo_Click(object sender, EventArgs e)

        {

            btnDo.Enabled = false;

            lblText.Text = @"Doing";

            await Task.Delay(3000);

            btnDo.Enabled = true;

            lblText.Text = @"Complete";

        }

    }


走进异步编程的世界之GUI 中执行异步操作


现在,就是原先希望看到的效果。


【分析】btnDo_Click 事件处理程序先将前两条消息压入队列,然后将自己从处理器移出,在3秒后(等待空闲任务完成后 Task.Delay )再将自己压入队列。这样可以保持响应,并保证所有的消息可以在线程挂起的时间内被处理。

 

 1.1 Task.Yield


Task.Yield 方法创建一个立刻返回的 awaitable。等待一个Yield可以让异步方法在执行后续部分的同时返回到调用方法。


可以将其理解为 离开当前消息队列,回到队列末尾,让 CPU 有时间处理其它任务


class Program

    {

        static void Main(string[] args)

        {

            const int num = 1000000;

            var t = DoStuff.Yield1000(num);

            Loop(num / 10);

            Loop(num / 10);

            Loop(num / 10);

            Console.WriteLine($"Sum: {t.Result}");

            Console.Read();

        }


        /// <summary>

        /// 循环

        /// </summary>

        /// <param name="num"></param>

        private static void Loop(int num)

        {

            for (var i = 0; i < num; i++) ;

        }

    }

    internal static class DoStuff

    {

        public static async Task<int> Yield1000(int n)

        {

            var sum = 0;

            for (int i = 0; i < n; i++)

            {

                sum += i;

                if (i % 1000 == 0)

                {

                    await Task.Yield(); //创建异步产生当前上下文的等待任务

                }

            }

            return sum;

        }

    }


走进异步编程的世界之GUI 中执行异步操作


上述代码每执行1000次循环就调用 Task.Yield 方法创建一个等待任务,让处理器有时间处理其它任务。该方法在 GUI 程序中是比较有用的。

 

二、在 WinForm 中使用异步 Lambda 表达式


将刚才的窗口程序的点击事件稍微改动一下。


public partial class Form1 : Form

{

        public Form1()

        {

            InitializeComponent();

            //async (sender, e) 异步表达式

            btnDo.Click += async (sender, e) =>

            {

                Do(false, "Doing");

                await Task.Delay(3000);

                Do(true, "Finished");

            };

        }

        private void Do(bool isEnable, string text)

        {

            btnDo.Enabled = isEnable;

            lblText.Text = text;

        }

    }


还是原来的配方,还是熟悉的味道,还是原来哪个窗口,变的只是内涵。


走进异步编程的世界之GUI 中执行异步操作

 

三、一个完整的 WinForm 程序


现在在原来的基础上添加了进度条,以及取消按钮。


public partial class Form1 : Form

    {

        private CancellationTokenSource _source;

        private CancellationToken _token;


        public Form1()

        {

            InitializeComponent();

        }

        /// <summary>

        /// Do 按钮事件

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        private async void btnDo_Click(object sender, EventArgs e)

        {

            btnDo.Enabled = false;

            _source = new CancellationTokenSource();

            _token = _source.Token;

            var completedPercent = 0; //完成百分比

            const int time = 10; //循环次数

            const int timePercent = 100 / time; //进度条每次增加的进度值

            for (var i = 0; i < time; i++)

            {

                if (_token.IsCancellationRequested)

                {

                    break;

                }

                try

                {

                    await Task.Delay(500, _token);

                    completedPercent = (i + 1) * timePercent;

                }

                catch (Exception)

                {

                    completedPercent = i * timePercent;

                }

                finally

                {

                    progressBar.Value = completedPercent;

                }

            }


            var msg = _token.IsCancellationRequested ? $"进度为:{completedPercent}% 已被取消!" : $"已经完成";

            MessageBox.Show(msg, @"信息");

            progressBar.Value = 0;

            InitTool();

        }


        /// <summary>

        /// 初始化窗体的工具控件

        /// </summary>

        private void InitTool()

        {

            progressBar.Value = 0;

            btnDo.Enabled = true;

            btnCancel.Enabled = true;

        }

        /// <summary>

        /// 取消事件

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        private void btnCancel_Click(object sender, EventArgs e)

        {

            if (btnDo.Enabled) return;

            btnCancel.Enabled = false;

            _source.Cancel();

        }

    }


走进异步编程的世界之GUI 中执行异步操作


四、另一种异步方式 - BackgroundWorker 类


与 async/await 不同的是,你有时候可能需要一个额外的线程,在后台持续完成某项任务,并不时与主线程通信,这时就需要用到 BackgroundWorker 类。主要用于 GUI 程序。


书中的千言万语不及一个简单的示例。


public partial class Form2 : Form

{

        private readonly BackgroundWorker _worker = new BackgroundWorker();

        public Form2()

        {

            InitializeComponent();

            //设置 BackgroundWorker 属性

            _worker.WorkerReportsProgress = true;   //能否报告进度更新

            _worker.WorkerSupportsCancellation = true;  //是否支持异步取消

            //连接 BackgroundWorker 对象的处理程序

            _worker.DoWork += _worker_DoWork;   //开始执行后台操作时触发,即调用 BackgroundWorker.RunWorkerAsync 时触发

            _worker.ProgressChanged += _worker_ProgressChanged; //调用 BackgroundWorker.ReportProgress(System.Int32) 时触发

            _worker.RunWorkerCompleted += _worker_RunWorkerCompleted;   //当后台操作已完成、被取消或引发异常时触发

        }

        /// <summary>

        /// 当后台操作已完成、被取消或引发异常时发生

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        private void _worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

        {

            MessageBox.Show(e.Cancelled ? $@"进程已被取消:{progressBar.Value}%" : $@"进程执行完成:{progressBar.Value}%");

            progressBar.Value = 0;

        }

        /// <summary>

        /// 调用 BackgroundWorker.ReportProgress(System.Int32) 时发生

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        private void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e)

        {

            progressBar.Value = e.ProgressPercentage;   //异步任务的进度百分比

        }

        /// <summary>

        /// 开始执行后台操作触发,即调用 BackgroundWorker.RunWorkerAsync 时发生

        /// </summary>

        /// <param name="sender"></param>

        /// <param name="e"></param>

        private static void _worker_DoWork(object sender, DoWorkEventArgs e)

        {

            var worker = sender as BackgroundWorker;

            if (worker == null)

            {

                return;

            }

            for (var i = 0; i < 10; i++)

            {

                //判断程序是否已请求取消后台操作

                if (worker.CancellationPending)

                {

                    e.Cancel = true;

                    break;

                }

                worker.ReportProgress((i + 1) * 10);    //触发 BackgroundWorker.ProgressChanged 事件

                Thread.Sleep(250);  //线程挂起 250 毫秒

            }

        }

        private void btnDo_Click(object sender, EventArgs e)

        {

            //判断 BackgroundWorker 是否正在执行异步操作

            if (!_worker.IsBusy)

            {

                _worker.RunWorkerAsync();   //开始执行后台操作

            }

        }

        private void btnCancel_Click(object sender, EventArgs e)

        {

            _worker.CancelAsync();  //请求取消挂起的后台操作

        }

    }



关注「DotNet」 

看更多精选 .Net 技术文章

↓↓↓



以上是关于走进异步编程的世界之GUI 中执行异步操作的主要内容,如果未能解决你的问题,请参考以下文章

[C#] 走进异步编程的世界 - 剖析异步方法(下)

C#_异步编程走进异步编程的世界 - 剖析异步方法(下)

走进异步编程的世界:剖析异步方法(上)

走进异步编程的世界 - 开始接触 async/await

转走进异步编程的世界 - 开始接触 async/await

走进异步编程的世界接触 :async/await