• C# 委托高级应用----线程——创建无阻塞的异步调用(二)


    了解IAsyncResult

             现在我们已经了解,EndInvoke可以给我们提供传出参数与更新后的ref参数;也可以向我们导出异步函数中的异常信息。例如,我们使用BeginInvoke调用了异步函数Sleep,它开始执行。之后调用EndInvoke,可以获取Sleep何时执行完成。但如果我们在Sleep执行完成20分钟后,才去调用EndInvoke呢?EndInvoke仍然会给我们提供传出值及异步中的异常(假如产生了异常),那么这些信息到底存储在哪里?EndInvoke如何在函数执行如此久之后仍然能够调用这些返回值?答案就在于IAsyncResult对象。EndInvoke每次在执行后,都会调用一个该对象作为参数,它包括以下信息:

    ●  异步函数是否已经完成

    ●  对调用了BeginInvoke方法的委托的引用

    ●  所有的传出参数及它们的值

    ●  所有的ref参数及它们的更新值

    ●  函数的返回值

    ●  异步函数产生的异常

    IAsyncResult看起来空无一物,这是因为它仅仅是一个包含了若干属性的接口;而实际上,它是一个System.Runtime.Remoting.Messaging.AsyncResult对象。

    如果我们在编译器运行期间监视tag的状态,就会发现,AsyncResult对象下包含类型为System.Runtime.Remoting.Messaging.ReturnMessage的对象。点开它,就会发现这个标签中包含的所有的异步函数的执行信息!

    使用Callback委托:好莱坞原则”不要联系我,我会联系你”

    目前为止,我们需要了解如何传递参数、如何捕捉异常;了解我们的异步方法其实是执行在线程池中的某个具体线程对象中。唯一未涉及到的就是如何在异步函数执行完成后得到通知。毕竟,阻塞调用线程等待函数结束的做法始终差强人意。为了实现这个目的,我们必须为BeginInvoke函数提供一个Callback委托。观察一下两个函数:

    class Program
        {
            public delegate string DelegateWithParameters(out int param1, string param2, ref ArrayList param3);
            static private string FuncWithParameters(out int param1, string param2, ref ArrayList param3)
            {
                // 我们在这里改变参数值
                param1 = 200;
                param2 = "hello";
                param3 = new ArrayList();
    
                return "thank you for reading me";
            }
         static   private void CallSleepWithoutOutAndRefParameterWithCallback()
            {
                
            }
    
    
             private static  void CallBack(IAsyncResult tag)
            {
                //  我们的int参数标记了out,因此此处不能定义初始值
                int intOutputValue;
                ArrayList list = null;
    
                //  IAsyncResult实际上就是AsyncResult对象,
                //  取得它也就可以从中取得用于调用函数的委托对象
                AsyncResult result = (AsyncResult)tag;
    
                //  取得委托
                DelegateWithParameters del = (DelegateWithParameters)result.AsyncDelegate;
    
                //  取得委托后,我们需要在其上执行EndInvoke。
                //  这样就可以取得函数中的执行结果。
                string strReturnValue = del.EndInvoke(out intOutputValue, ref list, tag);
    
                Console.WriteLine(intOutputValue);
                Console.WriteLine(list.Count);
                Console.WriteLine(tag);
                Console.WriteLine(strReturnValue);
            }
    
    
            public static void Main(string[] args)
            {
                // 创建几个参数
                string strParam = "Param1";
                int intValue = 100;
                ArrayList list = new ArrayList();
                list.Add("Item1");
    
                // 创建委托对象
                DelegateWithParameters delSleep =
                    new DelegateWithParameters(FuncWithParameters);
    
                delSleep.BeginInvoke(out intValue, strParam, ref list, new AsyncCallback(CallBack), null);
                Console.ReadLine();
            }
        }
        200
        0
        System.Runtime.Remoting.Messaging.AsyncResult
        thank you for reading me

    在这里,我们向BeginInvoke传递了Callback回调函数。这样.NET就可以在FuncWithParameters()执行完后调用Callback函数。在之前,我们已经了解到,必须使用EndInvoke来取得函数的执行结果,注意上面为了使用EndInvoke,我们使用了一些特殊操作来取得delegate对象。

    //  IAsyncResult实际上就是AsyncResult对象,
    //  取得它也就可以从中取得用于调用函数的委托对象
    AsyncResult result = (AsyncResult)tag;
    
    //  取得委托
    DelegateWithParameters del = (DelegateWithParameters)result.AsyncDelegate;

    最后一个问题:回调函数执行在什么线程?

    总而言之,Callback函数(回调函数)是.NET通过我们的委托对象来实现调用的。我们可能会希望得到一个更清晰的画面:回调函数究竟执行在那个线程?为了达到这个目的:我们在函数中加入线程日

    class Program
        {
            public delegate string DelegateWithParameters(out int param1, string param2, ref ArrayList param3);
            static private string FuncWithParameters(out int param1, string param2, ref ArrayList param3)
            {
                //// 记录线程信息
                //Console.WriteLine("In FuncWithParameters: Thread Pool? "
                //    + Thread.CurrentThread.IsThreadPoolThread.ToString() +
                //    " Thread Id: " + Thread.CurrentThread.GetHashCode());
    
                // 挂起秒以模拟线程在这里执行了耗时较长的任务
                Thread.Sleep(4000);
    
                //  我们在这里改变参数值
                param1 = 300;
                param2 = "hello";
                param3 = new ArrayList();
    
                //  这里执行一些耗时较长的工作
              //  Thread.Sleep(3000);
    
                return "thank you for reading me";
            }
            static private void CallSleepWithoutOutAndRefParameterWithCallback()
            {
                
            }
    
             private static  void CallBack(IAsyncResult tag)
            {
                // 回调函数在什么线程执行?
                Console.WriteLine("In Callback: Thread Pool? "
                    + Thread.CurrentThread.IsThreadPoolThread.ToString() +
                    " Thread Id: " + Thread.CurrentThread.GetHashCode());
    
                //  我们的int参数标记了out,因此此处不能定义初始值
                int intOutputValue;
                ArrayList list = null;
    
                //  IAsyncResult实际上就是AsyncResult对象,
                //  取得它也就可以从中取得用于调用函数的委托对象
                AsyncResult result = (AsyncResult)tag;
    
                //  取得委托
                DelegateWithParameters del = (DelegateWithParameters)result.AsyncDelegate;
    
                //  取得委托后,我们需要在其上执行EndInvoke。
                //  这样就可以取得函数中的执行结果。
                string strReturnValue = del.EndInvoke(out intOutputValue, ref list, tag);
    
                string strState = (string)tag.AsyncState;
    
                Console.WriteLine("EndInvoke的传递参数" + strState);
                Console.WriteLine(intOutputValue);
                Console.WriteLine(list.Count);
                Console.WriteLine(strReturnValue);
            }
    
    
            public static void Main(string[] args)
            {
                // 创建几个参数
                string strParam = "Param1";
                int intValue = 100;
                ArrayList list = new ArrayList();
                list.Add("Item1");
    
                // 创建委托对象
                DelegateWithParameters delSleep =
                    new DelegateWithParameters(FuncWithParameters);
                for (int i = 0; i < 2; i++)
                {
                    delSleep.BeginInvoke(out intValue, strParam, ref list, new AsyncCallback(CallBack), "" + i + "");
                }
                Console.ReadLine();
            }
        }
        In Callback: Thread Pool? True Thread Id: 10
        EndInvoke的传递参数第1次
        300
        0
        thank you for reading me
        In Callback: Thread Pool? True Thread Id: 12
        EndInvoke的传递参数第0次
        300
        0
        thank you for reading me

    注意FuncWithParameter函数被连续执行了3次,它们依次被执行在相互独立的线程上,并且这些线程来自于线程池。而他们各自的回调函数也执行在与FuncWithParameter相同的线程中。线程11执行了FuncWithParameter,3秒后,它的回调函数也执行在线程11中,线程12、13也是同样。这样,我们可以认为回调函数实际上是异步函数的一种延续。

    为什么要这样做?也许是因为这样我们就不必过多的耗费线程池中的线程,达到线程复用的效果;通过执行在相同的线程,也可以避免不同的线程间传递上下文环境带来的损耗问题。

    到此为止,我们在Form中执行异步函数,将会得到一个完全不堵塞主线程的异步调用,这就是我们所希望的效果!

    应用场景模拟

    现在我们了解了BeginInvoke、EndInvoke、Callback的使用及特点,如何将他们运用到我们的Win Form程序中,使数据的获取不再阻塞UI线程,实现异步加载数据的效果?我们现在通过一个具体实例来加以说明。

    场景描述:将系统的操作日志从数据库中查询出来,并且加载到前端的ListBox控件中。

    要求:查询数据库的过程是个时间复杂度较高的作业,但我们的窗体在执行查询时,不允许出现”假死”的情况。

    private void button1_Click(object sender, EventArgs e)
    {
        GetLogDelegate getLogDel = new GetLogDelegate(GetLogs);
        
        getLogDel.BeginInvoke(new AsyncCallback(LogTableCallBack),  null);
    }
    
    public delegate DataTable GetLogDelegate();
    
    /// <summary>
    /// 从数据库中获取操作日志,该操作耗费时间较长,
    /// 且返回数据量较大,日志记录可能超过万条。
    /// </summary>
    /// <returns></returns>
    private DataTable GetLogs()
    {
        string sql = "select * from ***";
        DataSet ds = new DataSet();
    
        using (OracleConnection cn = new OracleConnection(connectionString))
        {
            cn.Open();
    
            OracleCommand cmd = new OracleCommand(sql, cn);
    
            OracleDataAdapter adapter = new OracleDataAdapter(cmd);
            adapter.Fill(ds);
        }
    
        return ds.Tables[0];
    }
    
    /// <summary>
    /// 绑定日志到ListBox控件。
    /// </summary>
    /// <param name="tag"></param>
    private void LogTableCallBack(IAsyncResult tag)
    {
        AsyncResult result = (AsyncResult)tag;
        GetLogDelegate del = (GetLogDelegate)result.AsyncDelegate;
    
        DataTable logTable = del.EndInvoke(tag);
    
        if (this.listBox1.InvokeRequired)
        {
            this.listBox1.Invoke(new MethodInvoker(delegate()
            {
                BindLog(logTable);
            }));
        }
        else
        {
            BindLog(logTable);
        }
    }
    
    private void BindLog(DataTable logTable)
    {
        this.listBox1.DataSource = logTable;
    }

    以上代码在获取数据时,将不会带来任何UI线程的阻塞。

    总结:

    写下本文的主要目的在于总结以非阻塞模式调用函数的方法,我们应当了解以下结论;

    ●  Delegate会对BeginInvoke与EndInvoke的调用生成正确的参数,所有的传出参数、返回值与异常都可以在EndInvoke中取得。

    ●  不要忘记BeginInvoke是取自线程池中的线程,要注意防止异步任务的数量超过了线程池的线程上限值。

    ●  CallBack委托表示对与异步任务的回调,它将使我们从阻塞的困扰中彻底解脱。

    ●  截止到目前为止,UI线程在处理异步工作时将不再阻塞,而只有在更新UI具体内容时才会发生阻塞。

  • 相关阅读:
    阿里高级技术专家谈开源DDD框架:COLA4.0,分离架构和组件
    react的setState到底是同步还是异步?
    JMeter入门教程
    手把手教你接口自动化测试 – SoapUI & Groovy
    解读Web应用程序安全性问题的本质
    python实现的json数据以HTTP GET,POST,PUT,DELETE方式页面请求
    Linux常用命令
    PV原语操作详解
    软考计算机网络原理之IP计算问题汇总
    化繁为简了解进程与线程
  • 原文地址:https://www.cnblogs.com/zhangtaotqy/p/8000998.html
Copyright © 2020-2023  润新知