zoukankan      html  css  js  c++  java
  • 委托的Invoke 和 BeginInvoke

    通过一个委托来进行同步方法的异步调用,也是.net提供的异步调用机制之一。但是Delegate.BeginInvoke方法是从ThreadPool取出一个线程来执行这个方法,以获得异步执行效果的。也就是说,如果采用这种方式提交多个异步委托,那么这些调用的顺序无法得到保证。而且由于是使用线程池里面的线程来完成任务,使用频繁,会对系统的性能造成影响。

    Delegate.BeginInvoke也是讲一个委托方法封送到其它线程,从而通过异步机制执行一个方法。调用者线程则可以在完成封送以后去继续它的工作。但是这个方法封送到的最终执行线程是运行库从ThreadPool里面选取的一个线程。

    三、同步和异步

    同步和异步是对方法执行顺序的描述。

    同步:等待上一行完成计算之后,才会进入下一行。

    例如:请同事吃饭,同事说很忙,然后就等着同事忙完,然后一起去吃饭。

    异步:不会等待方法的完成,会直接进入下一行,是非阻塞的。

    例如:请同事吃饭,同事说很忙,那同事先忙,自己去吃饭,同事忙完了他自己去吃饭。

    下面通过一个例子讲解同步和异步的区别

    1、新建一个winform程序,上面有两个按钮,一个同步方法、一个异步方法,在属性里面把输出类型改成控制台应用程序,这样可以看到输出结果,代码如

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    
    namespace MyAsyncThreadDemo
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            /// <summary>
            /// 异步方法
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void btnAsync_Click(object sender, EventArgs e)
            {
                Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}");
                Action<string> action = this.DoSomethingLong;
                // 调用委托(同步调用)
                action.Invoke("btnAsync_Click_1");
                // 异步调用委托
                action.BeginInvoke("btnAsync_Click_2",null,null);
                Console.WriteLine($"***************btnAsync_Click End    {Thread.CurrentThread.ManagedThreadId}");
            }
    
            /// <summary>
            /// 同步方法
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void btnSync_Click(object sender, EventArgs e)
            {
                Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
                int j = 3;
                int k = 5;
                int m = j + k;
                for (int i = 0; i < 5; i++)
                {
                    string name = string.Format($"btnSync_Click_{i}");
                    this.DoSomethingLong(name);
                }
            }
    
    
            private void DoSomethingLong(string name)
            {
                Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
                long lResult = 0;
                for (int i = 0; i < 1000000000; i++)
                {
                    lResult += i;
                }
                Console.WriteLine($"****************DoSomethingLong {name}   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
            }
        }
    }

    2、启动程序,点击同步,结果如下:

    从上面的截图中能够很清晰的看出:同步方法是等待上一行代码执行完毕之后才会执行下一行代码。

    点击异步,结果如下:

    从上面的截图中看出:当执行到action.BeginInvoke("btnAsync_Click_2",null,null);这句代码的时候,程序并没有等待这段代码执行完就执行了下面的End,没有阻塞程序的执行。

    在刚才的测试中,如果点击同步,这时winform界面不能拖到,界面卡住了,是因为主线程(即UI线程)在忙于计算。

    点击异步的时候,界面不会卡住,这是因为主线程已经结束,计算任务交给子线程去做。

    在仔细检查上面两个截图,可以看出异步的执行速度比同步执行速度要快。同步方法执行完将近16秒,异步方法执行完将近6秒。

    在看下面的一个例子,修改异步的方法,也和同步方法一样执行循环,修改后的代码如下:

    private void btnAsync_Click(object sender, EventArgs e)
    {
          Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}");
          //Action<string> action = this.DoSomethingLong;
          //// 调用委托(同步调用)
          //action.Invoke("btnAsync_Click_1");
          //// 异步调用委托
          //action.BeginInvoke("btnAsync_Click_2",null,null);
          Action<string> action = this.DoSomethingLong;
          for (int i = 0; i < 5; i++)
          {
               //Thread.Sleep(5);
               string name = string.Format($"btnAsync_Click_{i}");
               action.BeginInvoke(name, null, null);
          }
          Console.WriteLine($"***************btnAsync_Click End    {Thread.CurrentThread.ManagedThreadId}");
    }

    结果如下:

    从截图中能够看出:同步方法执行是有序的,异步方法执行是无序的。异步方法无序包括启动无序和结束无序。启动无序是因为同一时刻向操作系统申请线程,操作系统收到申请以后,返回执行的顺序是无序的,所以启动是无序的。结束无序是因为虽然线程执行的是同样的操作,但是每个线程的耗时是不同的,所以结束的时候不一定是先启动的线程就先结束。从上面同步方法中可以清晰的看出:btnSync_Click_0执行时间耗时不到3秒,而btnSync_Click_1执行时间耗时超过了3秒。可以想象体育比赛中的跑步,每位运动员听到发令枪起跑的顺序不同,每位运动员花费的时间不同,最终到达终点的顺序也不同。

    总结一下同步方法和异步方法的区别:

    1、同步方法由于主线程忙于计算,所以会卡住界面。

          异步方法由于主线程执行完了,其他计算任务交给子线程去执行,所以不会卡住界面,用户体验性好。

    2、同步方法由于只有一个线程在计算,所以执行速度慢。

          异步方法由多个线程并发运算,所以执行速度快,但并不是线性增长的(资源可能不够)。多线程也不是越多越好,只有多个独立的任务同时运行,才能加快速度。

    3、同步方法是有序的。

          异步多线程是无序的:启动无序,执行时间不确定,所以结束也是无序的。一定不要通过等待几毫秒的形式来控制线程启动/执行时间/结束。

    四、回调

    先来看看异步多线程无序的例子:

    在界面上新增一个按钮,实现代码如下:

    private void btnAsyncAdvanced_Click(object sender, EventArgs e)
    {
          Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
          Action<string> action = this.DoSomethingLong;
          action.BeginInvoke("btnAsyncAdvanced_Click", null, null);
          // 需求:异步多线程执行完之后再打印出下面这句
          Console.WriteLine($"到这里计算已经完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
          Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    }

    运行结果:

    从上面的截图中看出,最终的效果并不是我们想要的效果,而且打印输出的还是主线程。

    既然异步多线程是无序的,那我们有没有什么办法可以解决无序的问题呢?办法当然是有的,那就是使用回调,.NET框架已经帮我们实现了回调:

    BeginInvoke的第二个参数就是一个回调,那么AsyncCallback究竟是什么呢?F12查看AsyncCallback的定义:

    发现AsyncCallback就是一个委托,参数类型是IAsyncResult,明白了AsyncCallback是什么以后,将上面的代码进行如下的改造:

    private void btnAsyncAdvanced_Click(object sender, EventArgs e)
    {       
        Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
        Action<string> action = this.DoSomethingLong;
        // 定义一个回调
        AsyncCallback callback = p => 
        {
           Console.WriteLine($"到这里计算已经完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
        };
        // 回调作为参数
        action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);          
        Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
     }

    运行结果:

     

    上面的截图中可以看出,这就是我们想要的效果,而且打印是子线程输出的,但是程序究竟是怎么实现的呢?我们可以进行如下的猜想:

    程序执行到BeginInvoke的时候,会申请一个基于线程池的线程,这个线程会完成委托的执行(在这里就是执行DoSomethingLong()方法),在委托执行完以后,这个线程又会去执行callback回调的委托,执行callback委托需要一个IAsyncResult类型的参数,这个IAsyncResult类型的参数是如何来的呢?鼠标右键放到BeginInvoke上面,查看返回值:

    发现BeginInvoke的返回值就是IAsyncResult类型的。那么这个返回值是不是就是callback委托的参数呢?将代码进行如下的修改:

    private void btnAsyncAdvanced_Click(object sender, EventArgs e)
    {
                // 需求:异步多线程执行完之后再打印出下面这句
                Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
                Action<string> action = this.DoSomethingLong;
                // 无序的
                //action.BeginInvoke("btnAsyncAdvanced_Click", null, null);
    
                IAsyncResult asyncResult = null;
                // 定义一个回调
                AsyncCallback callback = p =>
                {
                    // 比较两个变量是否是同一个
                    Console.WriteLine(object.ReferenceEquals(p,asyncResult));
                    Console.WriteLine($"到这里计算已经完成了。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
                };
                // 回调作为参数
                asyncResult= action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);           
                Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    }

    结果:

    这里可以看出BeginInvoke的返回值就是callback委托的参数。

    现在我们可以使用回调解决异步多线程无序的问题了。

    2、获取委托异步调用的返回值

    使用EndInvoke可以获取委托异步调用的返回值,请看下面的例子:

    private void btnAsyncReturnVlaue_Click(object sender, EventArgs e)
    {
           // 定义一个无参数、int类型返回值的委托
           Func<int> func = () =>
           {
                 Thread.Sleep(2000);
                 return DateTime.Now.Day;
           };
           // 输出委托同步调用的返回值
           Console.WriteLine($"func.Invoke()={func.Invoke()}");
           // 委托的异步调用
           IAsyncResult asyncResult = func.BeginInvoke(p => 
           {
                Console.WriteLine(p.AsyncState);
           },"异步调用返回值");
           // 输出委托异步调用的返回值
           Console.WriteLine($"func.EndInvoke(asyncResult)={func.EndInvoke(asyncResult)}");
    }

    结果:

  • 相关阅读:
    《构建高性能web站点》阅读笔记(三)
    哈希表的C实现(一)
    《大规模web服务开发技术》阅读笔记
    CentOS搭建python开发环境
    Instagram的技术探索(2)
    CentOS5.5编译安装gvim7.3 失败记录
    由于启动用户实例的进程时出错,导致无法生成 SQL Server 的用户实例。该连接将关闭
    wp开发中解决gb2312的编码问题
    WP8 SDK 视图设计器 未将对象设置到对象的实例解决方法
    Oracle误删表的恢复
  • 原文地址:https://www.cnblogs.com/hanguoshun/p/12860015.html
Copyright © 2011-2022 走看看