C#异步编程由浅入深(一)

1、什么算异步?

  广义来说,两个工做流能同时进行就算异步,例如,CPU与外设之间的工做流就是异步的。在面向服务的系统中,各个子系统之间通讯通常都是异步的,例如,订单系统与支付系统之间的通讯是异步的,又如,在现实生活中,你去馆子吃饭,工做流是这样的,点菜->下单->作你的事->上菜->吃饭,这个也是异步的,具体来说你和厨师之间是异步的,异步是如此重要,因外它表明者高效率(二者或二者以上的工做能够同时进行),但复杂,同步的世界简单,但效率极极低。数据库

2、在编程中的异步

  在编程中,除了同步和异步这两个名词,还多了一个阻塞和非阻塞,其中,阻塞和非阻塞是针对线程的概念,那么同步和异步是针对谁呢?其实不少状况下同步和异步并无具体针对某一事物,因此致使了针对同步阻塞、同步非阻塞、异步阻塞、异步非阻塞这几个概念的模糊不清。而且也确实没有清晰的边界,请看如下例子:编程

public static void DoWorkA()
    {
        Thread thread = new Thread(() => 
        {
            Console.WriteLine("WorkA Done!");
        });
        thread.Start();
    }

    public static void DoWordB()
    {
        Thread thread = new Thread(() =>
        {
            Console.WriteLine("WorkB Done!");
        });
        thread.Start();
    }
    static void Main(string[] args)
    {
        DoWorkA();
        DoWordB();
    }

  假设运行该代码的CPU是单核单线程,那么请问?DoWorkA()、DoWorkB()这两个函数是异步的吗?由于CPU是单核,因此根本不能同时运行两个函数,那么从这个层次来说,他们之间实际上是同步的,可是,现实的状况是咱们通常都认为他们之间是异步的,由于咱们是从代码的执行顺序角度考虑的,而不是从CPU自己的工做流程考虑的。因此要分上下文考虑。再请看下面这个例子:c#

static void Main(string[] args)
    {
        DoWorkA();
        QueryDataBaseSync();//同步查询数据库
        DoWorkB();
    }

  从代码的执行顺序角度考虑,这三个函数执行就是同步的,可是,从CPU的角度来说,数据库查询工做(另外一台机器)和CPU计算工做是异步的,在下文中,没有作特别申明,则都是从代码的执行顺序角度来讨论同步和异步。
  再解释一下阻塞和非阻塞以及相关的知识:网络

  阻塞特指线程由运行状态转换到挂起状态,但CPU并不会阻塞,操做系统会切换另外一个处于就绪状态的线程,并转换成运行状态。致使线程被阻塞的缘由有不少,如:发生系统调用(应用程序调用系统API,若是调用成功,会发生从应用态->内核态->应用态的转换开销),但此时外部条件并无知足,如从Socket内核缓冲区读数据,此时缓冲区尚未数据,则会致使操做系统挂起该线程,切换到另外一个处于就绪态的线程而后给CPU执行,这是主动调用致使的,还有被动致使的,对于如今的分时操做系统,在一个线程时间片到了以后,会发生时钟中断信号,而后由操做系统预先写好的中断函数处理,再按必定策略(如线程优先级)切换至另外一个线程执行,致使线程被动地从运行态转换成挂起状态。
  非阻塞通常指函数调用不会致使执行该函数的线程从运行态转换成挂起状态。多线程

3、原始的异步编程模式之回调函数

  在此以前,咱们先稍微了解下图形界面的工做原理,GUI程序大概能够用如下伪代码表示:异步

While(GetMessage() != 'exit') //从线程消息队列中获取一个消息,线程消息队列由系统维护,例如鼠标移动事件,这个事件由操做系统捕捉,并投递到线程的消息队列中。
{
    msg = TranslateMessage();//转换消息格式
    DispatherMessage(msg);//分发消息到相应的处理函数
}

  其中DispatherMessage根据不一样的消息类型,调用不一样的消息处理函数,例如鼠标移动消息(MouseMove),此时消息处理函数能够根据MouseMove消息中的值,作相应的处理,例如调用绘图相关函数画出鼠标此刻的形状。
  通常来说,咱们称这个循环为消息循环(事件循环、EventLoop),编程模型称为消息驱动模型(事件驱动),在UI程序中,执行这部分代码的线程通常只有一个线程,称为UI线程,为何是单线程,读者能够去思考。
  以上为背景知识。如今,咱们思考,假如在UI线程中执行一个会致使UI线程被阻塞的操做,或者在UI线程执行一个纯CPU计算的工做,会发生什么样的结果?若是执行一个致使UI线程被阻塞的操做,那么这个消息循环就会被迫中止,致使相关的绘图消息不能被相应的消息处理函数处理,表现就是UI界面“假死”,直到UI线程被唤起。若是是纯CPU计算的工做,那么也会致使其余消息不能被及时处理,也会致使界面“假死”现象。如何处理这种状况?写异步代码。
  咱们先用控制台程序模拟这个UI程序,后面以此为基础。async

public static string GetMessage()
    {
        return Console.ReadLine();
    }

    public static  string TranslateMessage(string msg)
    {
        return msg;
    }

    public static  void DispatherMessage(string msg)
    {
        switch (msg)
        {
            case "MOUSE_MOVE":
                {
                    OnMOUSE_MOVE(msg);
                    break;
                }
            default:
                break;
        }
    }

    public static void OnMOUSE_MOVE(string msg)
    {
        Console.WriteLine("开始绘制鼠标形状");
    }


    static void Main(string[] args)
    {
        while(true)
        {
            string msg = GetMessage();
            if (msg == "quit") return;
            string m = TranslateMessage(msg);
            DispatherMessage(m);
        }
    }
一、回调函数

  上面那个例子,一但外部有消息到来,根据不一样的消息类型,调用不一样的处理函数,如鼠标移动时产生MOUSE_DOWN消息,相应的消息处理函数就开始从新绘制鼠标的形状,这样一但你鼠标移动,就你会发现屏幕上的鼠标跟着移动了。
  如今假设咱们增长一个消息处理函数,如OnMOUSE_DOWN,这个函数内部进行了一个阻塞的操做,如发起一个HTTP请求,在HTTP请求回复到来前,该UI程序会“假死”,咱们编写异步代码来解决这个问题。异步编程

public static int Http()
    {
        Thread.Sleep(1000);//模拟网络IO延时
        return 1;
    }
    public static void HttpAsync(Action<int> action,Action error)
    {
        //这里咱们用另外一个线程来实现异步IO,因为Http方法内部是经过Sleep来模拟网络IO延时的,这里也只能经过另外一个线程来实现异步IO
        //但记住,多线程是实现异步IO的一个手段而已,它不是必须的,后面会讲到如何经过一个线程来实现异步IO。
        Thread thread = new Thread(() => 
        {
            try
            {
                int res = Http();
                action(res);
            }
            catch
            {
                error();
            }
    
        });

        thread.Start();
    }
    public static void OnMouse_DOWN(string msg)
    {
        HttpAsync(res => 
        {
            Console.WriteLine("请求成功!");
            //使用该结果作一些工做
        }, () => 
        {
            Console.WriteLine("请求发生错误!");
        });
    }

  此时界面再也不“假死”了,咱们看下代码可读性,感受还行,可是,若是再在回调函数里面再发起相似的异步请求呢?(有人可能有疑问,为何还须要发起异步请求,我发同步请求不行吗?这都是在另外一个线程里了。是的,在这个例子里是没问题的,但真实状况是,执行回调函数的代码,通常都会在UI线程,由于取得结果后须要更新相关UI组件上的界面,例如文字,而更新界面的操做都是放在UI线程里的,如何把回调函数放到UI线程上执行,这里不作讨论,在.NET中,这跟同步上下文(Synchronization context)有关,后面会讲到),那么代码会变成这样函数

public static void OnMouse_DOWN(string msg)
    {
        HttpAsync(res => 
        {
            Console.WriteLine("请求成功!");
            //使用该结果作一些工做

            HttpAsync(r1 => 
            {
                //使用该结果作一些工做

                HttpAsync(r2 => 
                {
                    //使用该结果作一些工做
                }, () => 
                {

                });
            }, () => 
            {

            });
        }, () => 
        {
            Console.WriteLine("请求发生错误!");
        });
    }

  写过JS的同窗可能很清楚,这叫作“回调地狱”,如何解决这个问题?JS中有Promise,而C#中有Task,咱们先用Task来写这一段代码,而后本身实现一个与Task功能差很少的简单的类库。oop

public static Task<int> HttpAsync()
    {
        return Task.Run(() => 
        {
            return Http();
        });
    }


    public static void OnMouse_DOWN(string msg)
    {
        HttpAsync()
            .ContinueWith(t => 
            {
                if(t.Status == TaskStatus.Faulted)
                {

                }else if(t.Status == TaskStatus.RanToCompletion)
                {
                    //作一些工做
                }
            })
            .ContinueWith(t => 
            {
                if (t.Status == TaskStatus.Faulted)
                {

                }
                else if (t.Status == TaskStatus.RanToCompletion)
                {
                    //作一些工做
                }
            })
            .ContinueWith(t => 
            {
                if (t.Status == TaskStatus.Faulted)
                {

                }
                else if (t.Status == TaskStatus.RanToCompletion)
                {
                    //作一些工做
                }
            });
    }

  是否是感受清爽了许多?这是编写异步代码的第一个跃进。下篇将会介绍,如何本身实现一个简单的Task。后面还会提到C#中async/await的本质做用,async/await是怎么跟Task联系起来的,怎么把本身写的Task库与async/await连结起来,以及一个线程如何实现异步IO。   以为有收获的不妨点个赞,有支持才有动力写出更好的文章。

相关文章
相关标签/搜索