回调函数之理解

前言

回调函数,其实就是 . Net 预置的一个委托,请看定义:

using System.Runtime.InteropServices;

namespace System
{
    //
    // 摘要:
    //     引用在相应异步操作完成时调用的方法。
    //
    // 参数:
    //   ar:
    //     异步操作的结果。
    [ComVisible(true)]
    public delegate void AsyncCallback(IAsyncResult ar);
}

符合这样形式的都可以算回调函数。

那回调函数存在的意义又在哪里呢?比如我们要上传一个比较大的文件,耗时可能比较久,又不能一直在这里等,就开一个异步线程去上传。上传完成后,又希望通知一下我们,这个 “通知” 任务就可以让回调函数去做。

那么问题来了,为什么不直接把通知任务放在异步线程的最后呢?因为有时候,这个通知任务的代码要保密,或者操作起来比较复杂,所以写异步操作的人只需要去调用就可以了。看代码:

using System;
using System.Threading;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            Action<string> action = t => {
                Thread.Sleep(2000);
                Console.WriteLine("Action down,Args:{0},ThreadID:{1}", t ,
                Thread.CurrentThread.ManagedThreadId);          
            };

            action.BeginInvoke("action arg", myCallBack, "callBack arg");

            Console.WriteLine("Main down,ThreadID:{0}",Thread.CurrentThread.ManagedThreadId);

            Console.WriteLine("ThreadID:{0}", Thread.CurrentThread.ManagedThreadId);

            Console.ReadKey();
        }

        static void myCallBack(IAsyncResult asyncResult)
        {
            Console.WriteLine(asyncResult.IsCompleted);
            Thread.Sleep(200);
            Console.WriteLine("CallBack down,Args:{0},ThreadID:{1}", asyncResult.AsyncState,
                               Thread.CurrentThread.ManagedThreadId);
        }
    }
}

执行结果:

显然主线程很快就跑完了,异步 action 执行完后,回调了回调函数完成操作。可以看到,回调函数和 action 是在同一个线程里顺序运行的,注意 “顺序” 两个字,回调函数耗时 200 毫秒,而 action 耗时 3000毫秒,但是依然是 action 先打印出结果。

那么问题来了,既然是顺序执行,就算回调函数的代码要保密,或者操作起来比较复杂,那把回调函数封装成一个方法,放在 action 末尾不就好了,一样调用呀。

这个时候,就关联到异步阻塞的问题了。请看代码:

using System;
using System.Threading;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            Action<string> action = t => {
                Thread.Sleep(2000);
                Console.WriteLine("Action down,Args:{0},ThreadID:{1}", t, 
                Thread.CurrentThread.ManagedThreadId);
            };

            IAsyncResult asyncResult = action.BeginInvoke("action arg", myCallBack, "callBack arg");

            //1,阻止当前线程,直到异步 action 结束
            action.EndInvoke(asyncResult);

            //2,阻止当前线程,直到异步 action 结束。 注意,不阻塞回调哦
            asyncResult.AsyncWaitHandle.WaitOne();

            Console.WriteLine("Main down,ThreadID:{0}", Thread.CurrentThread.ManagedThreadId);

            Console.WriteLine("{0},ThreadID:{1}", asyncResult.IsCompleted, 
                               Thread.CurrentThread.ManagedThreadId);

            Console.ReadKey();
        }

        static void myCallBack(IAsyncResult asyncResult)
        {
            Console.WriteLine(asyncResult.IsCompleted);
            Thread.Sleep(100);
            Console.WriteLine("CallBack down,Args:{0},ThreadID:{1}", asyncResult.AsyncState, 
                               Thread.CurrentThread.ManagedThreadId);
        }
    }
}

执行结果:

执行结果与刚才不同了,主线程被阻塞了,直到 action 异步完成,主线程才接着往下走。

这个时候注意,阻塞并没有阻塞回调函数,这个很关键。这正是很多时候需要的,在确保部分任务完成之后再执行接下来得任务,并且不阻塞异步线程。

上面 1、2 都可以阻塞异步线程,效果一样。但是可以给 WaitOne() 赋予参数,使用其他有参重载,实现阻塞一段时间而不是一直阻塞。

后记

有趣的是,在 .Net 预定义中发现了 AsyncCallback,却并没有发现 SyncCallback ,原来回调函数一定是异步线程才能操作的呀。

想想也是,都同步了,还谈什么多线程,谈什么阻塞,谈什么女朋友…

发表评论