• c#异步编程基础



    一、线程(Thread)

    1.1 什么是线程Thread

    • 线程是一个可执行路径,它可以独立于其他线程执行。
    • 每个线程都在操作系统的进程(Process)内执行,而操作系统进程提供了程序运行的独立环境。
    • 单线程应用:一个进程中只有一个线程
    • 多线程应用:一个进程中有多个线程,多个线程会共享当前的执行环境(如内存)
    //开辟了一个的新的线程Thread
    Thread t = new Thread(WryteY);
    t.Start();
    //主线程在做的一些事
    for(int i=0; i<1000; i++)
    {
        Console.Write("x");
    }
    
    void WryteY(object obj)
    {
        for(int i=0; i < 1000; i++)
        {
            Console.Write("y");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上例讲解:
    在单核计算机上,操作系统必须为每个线程分配"时间片"来模拟并发,从而导致重复的x和y。
    在多核或多处理器计算上,这两个线程可以真正地并行执行(也可能受到计算机其他活动进程的竞争)

    1.2 线程的一些属性

    • 线程一旦开始执行,IsAlive就是true,线程结束就变成了false。
    • 线程结束的条件就是:线程构造函数传入的委托结束了执行。
    • 线程一旦结束,就无法重启。
    • 每个线程都有一个Name属性,通常用于调试。线程Name只能设置一次,以后更改会执出异常
    • 静态的Threac.CurrentThread属性,会返回当前执行的线程。
    
    // 为main线程起个名字
    Thread.CurrentThread.Name = "Main Thread...";
    //开辟了一个的新的线程Thread
    Thread t = new Thread(WryteY);
    t.Name = "Y_Thread...";
    t.Start();
    Console.WriteLine(Thread.CurrentThread.Name);
    //主线程在做的一些事
    for(int i=0; i<1000; i++)
    {
        Console.Write("x");
    }
    
    void WryteY(object obj)
    {
        Console.WriteLine(Thread.CurrentThread.Name);
        for(int i=0; i < 1000; i++)
        {
            Console.Write("y");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1.3 Thread.Join() & Thread.Sleep()

    • 调用Join方法,就可以等待另一个线程结束。
      Demo1:
    
    Thread t = new Thread(Go);
    t.Start();
    t.Join();//会等着线程t执行完毕才会接着下面执行
    Console.WriteLine("Thread t has ended!");
    
    void Go(object obj)
    {
        for(int i = 0; i < 1000; i++)
        {
            Console.Write("y");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Demo2:

    Thread.sleep()被用来暂停当前线程的执行,会通知线程调度器把当前线程在指定的时间周期内置为wait状态。当wait时间结束,线程状态重新变为Runnable并等待CPU的再次调度执行。所以线程sleep的实际时间取决于线程调度器,而这是由操作系统来完成的。
    一个进程在运行态时调用sleep(),进入等待态,睡眠结束以后,并不是直接回到运行态,而是进入就绪队列,要等到其他进程放弃时间片后才能重新进入运行态。所以sleep(1000),在1000ms以后,线程不一定会被唤醒。sleep(0)可以看成一个运行态的进程产生一个中断,由运行态直接转入就绪态。这样做是给其他就绪态进程使用时间片的机会。总之,还是操作系统中运行态、就绪态和等待态相互转化的问题。

    Thread thread1 = null;
    Thread thread2 = null;
    
    thread1 = new Thread(ThreadProc);
    
    thread1.Name = "Thread1";
    thread1.Start();
    
    thread2 = new Thread(ThreadProc);
    thread2.Name = "Thread2";
    thread2.Start();
    
    void ThreadProc(object obj)
    {
        Console.WriteLine($"Current thread: {Thread.CurrentThread.Name}");
        // 注意ThreadState
        if(Thread.CurrentThread.Name != "Thread1" && thread2.ThreadState!=ThreadState.Unstarted)
            thread2.Join(); //如果先进来的是thread1,等待thread2执行完毕,去执行thread2的流程
        Thread.Sleep(1000); //如果先进来的是thread1,那么此时休眠的是thread2
        Console.WriteLine($"Current thread: {Thread.CurrentThread.Name}");
        Console.WriteLine($"Thread1:{thread1.ThreadState}");
        Console.WriteLine($"Thread2:{thread2.ThreadState}");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    Demo3: join(超时参数毫秒)

    Thread thread1 = null;
    Thread thread2 = null;
    
    thread1 = new Thread(ThreadProc);
    
    thread1.Name = "Thread1";
    thread1.Start();
    
    thread2 = new Thread(ThreadProc);
    thread2.Name = "Thread2";
    thread2.Start();
    
    void ThreadProc(object obj)
    {
        Console.WriteLine($"Current thread: {Thread.CurrentThread.Name}");
        // 注意ThreadState
        if(Thread.CurrentThread.Name != "Thread1" && thread2.ThreadState!=ThreadState.Unstarted)
            if(thread2.Join(4000)) //thread2执行没超过2s则走下面
                Console.WriteLine("Thread2 has termminated.");
           else
                // 起时已过,Thread1将恢复
                Console.WriteLine("The timeout has elapsed and Thread1 will resume");
        Thread.Sleep(1000); 
        Console.WriteLine($"Current thread: {Thread.CurrentThread.Name}");
        Console.WriteLine($"Thread1:{thread1.ThreadState}");
        Console.WriteLine($"Thread2:{thread2.ThreadState}");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    总结:

    • 调用Join的时候,可以设置一个超时,用毫秒或者TimeSpan都可以,如果返回true,那就是线程结束了,如果超时了,就返回false。
    • Thread.Sleep()方法会暂停当前的线程,并等一段时间。
    • Thread.Sleep(0)这样调用会导致线程立即放弃本身当前的时间片,自动将CPU移交给其他线程。
    • Thread.Yield()做同样的事情,但是它只会反执行交给同一处理器上的其他线程。
    • 当等待Sleep或Join的时候,线程处于阻塞状态。

    1.4 阻塞 blocking

    如果线程的执行由于某种原因导致暂停,那么就认为该线程被阻塞了。如在Sleep或者通过Join等待其他线程结束。
    被阻塞的线程会立即将其处理器的时间片释放,直到满足阻塞条件为止
    可以通过ThreadState这个属性来判断线程是否处理被阻塞的状态:
    bool blocked = (someThread.ThreadState & ThreadState.WaitSleepJoin) !=0;

    ThreadState是一个flags enum,它大部分的枚举值都没有什么有,常用的几个如下:

    • Unstarted:尚未线程调用Start()方法
    • Running:线程已启动且尚未停止
    • WaitSleepJoin:线程被阻止,这可能是调用Sleep()或Join()等。

    1.4.1 解除阻塞

    当遇到下列四种情况的时候,就会解除阻塞

    • 阻塞条件被满足
    • 操作超时(如果设置超时的话)
    • 通过Thread.Interrupt()进行打断
    • 通过Thread.Abort()进行中止

    1.4.2 上下文切换

    当线程阻塞或解除,操作系统将执行上下文切换。这会产生少量开销,通常为1或2微秒

    I/O-bound vs Compute-bound(或Cpu-Bound)

    • 一个花费大部分时间等待某事发生的操作称为I/O-bound
    • I/O绑定操作通常涉及输入或输出,但这不是硬性要求,Thread.Sleep()也被视为I/O bound 也就是CPU干等着,什么事都不干。
    • 一个花费大部分时间执行CPU密集型工作的操作称为Compute-bound

    1.5 线程安全

    本地 vs 共享的状态
    Local vs Shared State
    Local本地独立

    • CLR为每个线程分配自己的内存栈(Stack),以便使本地变量保持独立。

    Shared共享

    • 如果多个线程都引用到同一个对象的实例,那么它们就共享了数据。
    • 被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段(field),所以也会被共享。
    • 静态字段(field) 也会在线程间共享数据。

    Shared共享的数据在多个线程中被使用会出现线程安全问题。

    锁定与线程安全

    • 在读取和写入共享数据的时候,通过使用一个互斥锁,就可以解决线程安全问题。
    • c#使用lock语句来加锁
    • 当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型的对象),一个线程会等待或阻塞,直到锁变成可用状态。
    • Lock不是线程安全的万金油,很容易忘记对字段加锁,Lock也会引起一些问题如死锁
    
    class ThreadSafe
    {
        static bool _done;
        static readonly object _locker = new object();
    
        static void Main()
        {
            new Thread(Go).Start();
            Go();
        }
        static void Go()
        {
            lock (_locker)
            {
                if (!_done)
                {
                    Console.WriteLine("Done");
                    _done = true;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    1.6 向线程传递数据

    如果你想往线程的启动方法里传递参数,最简单的方式是使用lambda表,在里面使用参数调用方法。

    
    class Program
    {
        static void Main()
        {
            Thread t = new Thread(() => Print("Hello from t!"));
            t.Start();
        }
    
        static void Print(string message)
        {
            Console.WriteLine(message);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    整个逻辑都放在lambda表达式中

    
    class Program
    {
        static void Main()
        {
            new Thread(() =>
            {
                Console.WriteLine("I'm running on anoter thread!");
                Console.WriteLine("This is so easy!");
            }).Start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    使用Lambda表达式可以很简单的给Thread传递参数,但是线程开始后,可以不能小修改了被捕获的变量,可能出现问题如下例

    class Program
    {
        static void Main()
        {
            for(int i = 0; i < 10; i++)
            {
                new Thread(()=>Console.Write(i)).Start();
            }
        }
        
        /*
         * i在循环的整个生命周期内指向的同一个同存地址
         */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    下例解决方案

    class Program
    {
        static void Main()
        {
            for(int i = 0; i < 10; i++)
            {
                int temp = i;
                new Thread(()=>Console.Write(temp)).Start();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    1.7 异常处理

    • 在try/cat/finally块中开辟了新线程,新线程中发生异常不能被捕获到
    • 任何线程有任何未处理的异常都会触发AppDomain.CurrentDomain.UnhandledException
    
    class Program
    {
        static void Main()
        {
            new Thread(Go).Start();
        }
        static void Go() //将异常放在逻辑中
        {
            try
            {
                throw null;
            }catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    1.8 前台和后台线程

    • 默认情况下,你手动创建的线程就是前台线程
    • 只要有前台线程在运行,那么应用程序就会一直处于活动状态。
    • 一旦所有的前台线程停止,那么应用程序就停止了,任何的后台线程也会突然终止。
    • 可以通过IsBackground属性判断线程是否是后台线程

    1.9 线程优先级

    • 线程的优先级(Thread的Priority属性)决定了相对于操作系统中其他活跃线程所占的执行时间。
    • 优先级分为 enum ThreadPriority{Lowest, BelowNormal, Normal, AboveNormal, Highest}

    提升线程优先级

    • 提升线程优先级的时候需特别注意,因为它可能饿死其他线程
    • 如果想让某线程(Thread)的优先级比其他进程(Process)中的线程(Thread)高,那就必须提升进程(Process)的优先级
    • 使用System.Dignostics下的Process类
    • using (Process p = Process.GetCurrentProcess())
    • p.PriorityClass = ProcessPriorityClass.High;
      
      • 1
    • 可以很好地用于只做少量工作且需要罗低延迟的非UI进程
    • 对于需要大量计算的应用程序(尤其是有UI的应用程序),提高进程优先级可能会使其他进程饿死,从而降低整个计算机的速度。

    1.10 信号

    • 有时,你需要让某线程一直处于等待的状态,直到接收到其他线程发来的通知。这就叫做signaling(发送信号)
    • 最简单的信号结构就是ManualResetEvent,调用它上面的WaitOne方法会阻塞当前的线程,直到另一个线程通过调用Set()方法来开启信号。
    class Program
    {
        static void Main()
        {
            var signal = new ManualResetEvent(false);
            new Thread(() =>
            {
                Console.WriteLine("Waiting for signal......");
                signal.WaitOne();//阻塞,等另一个线程开启信号
                signal.Dispose();
                Console.WriteLine("Got Signal!");
            }).Start();
            Thread.Sleep(1000); //线程暂停一秒
            signal.Set(); //发送信号
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.11 线程池(Thread Pool)

    • 当开始一个线程的时候,将花费几百微秒来组织类似以下的内容:一个新的局部变量栈(stack)
    • 线程池就可以节省这种开销
    • 不可以设置池线程的Name
    • 池线程都是后台线程
    • 阻塞池线程可使性能降级
    • 可能通过Thread.CurrentThread.IsThreadPoolThread属性来判断是否执行在池线程上

    1.12 Task

    Thread的问题

    • 线程(Thread)是用来创建并发的一种低级别工具
    • 虽然开始线程的时候可以方便的传入数据,但是当Join的时候,很难从线程获得返回值
    • 无法告诉线程在结束的时候开始做另外的工作,你必须进行Join操作

    Task Class

    • Task是一个相对高级的抽象,它代表了一个并发操作,该操作可能由Thread支持或不由Thread支持
    • Task是可组合的,Tasks可以使用线程池来减少启动延迟

    Task Run

    • Task类在System.Threading.Tasks命名空间下,开始一个Task最简单的办法就是Task.Run
    • Task默认使用线程池,也就是后台线程,当主线程结束时,你创建的所有tasks都会结束。
    
    class Program
    {
        static void Main()
        {
            // 创建了一个任务,默认使用的是线程池
            // 主线程结束,所有后台线程都将关闭
            Task.Run(() => Console.WriteLine("Foo"));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    Wait 等待

    • 调用task的Wait方法会进行阻塞直到操作完成
    • 相当于调用thread上的join方法
    • wait也可以让你指定一个超时时间和一个取消令牌来提前结束等待。
    class Program
    {
        static void Main()
        {
            Task task = Task.Run(() =>
            {
                Thread.Sleep(3000);
                Console.WriteLine("Foo");
            });
            Console.WriteLine(task.IsCompleted); //false
            task.Wait(); // 阻塞直至task完成操作
            Console.WriteLine(task.IsCompleted); //true
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    长时间运行的任务(Long-running tasks)

    • 默认情况下,CLR在线程池中运行Task,这非常适合短时间运行的Compute-Bound类工作
    • 针对长时间运行的任务或者阻塞操作,你可以不采用线程池
    • 如果同时运行多个long-running tasks(尤其是其中有处于阻塞状态的),那么性能将会受很大影响,这时有比TaskCreationOptions.LongRunning更好的办法
    • 如果任务是IO-BOUND,TaskCompleionSource和异步函数可以让你用回调代替线程来实现并发
    • 如果任务是Compute-Bound,生产者、消费者队列允许你对任务的并发性进行限流,避免把其他线程和进程饿死。

    1.13 Task的返回值

    • Task有一个泛型子类叫做Task,它允许发出一个返回值
    • 使用Func委托或兼容的Lambda表达式调用Taks.Run就可以得到Task
    • 随后,可能通过Result属性来获得返回的结果
    • 如果这个task还没有完成操作,访问Result属性会阻塞该线程直到该task完成操作
    • Task可以看作是一种所谓的"未来/许诺"(future、promise),在它里面包裹着一个Result,在稍后的时候就会变得可用
    • 在CTP版的时候,Task实际上叫做Future
    class Program
    {
        static void Main()
        {
            Task<int> task = Task.Run(() =>
            {
                Console.WriteLine("Foo");
                return 3;
            });
            int result = task.Result; // 如果task没有完成就阻塞
            Console.WriteLine(result);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1.14 Task的异常

    • 与Thread不一样,Task可以很方便的传播异常
    • 如果你的task里面抛出了一个未处理的异常,那么该异常就会重新被抛出给
    • 调用了wait()的地方
    • 访问了Task的Result属性的地方
    • CLR将异常包裹在AggregateException里,以便在并行编程场景中发挥很的作用。
    
    class Program
    {
        static void Main()
        {
            Task task = Task.Run(() => { throw null; });
            try
            {
                task.Wait();
            }catch(AggregateException aex)
            {
                if(aex.InnerException is NullReferenceException)
                {
                    Console.WriteLine("Null");
                }
                else
                {
                    throw;
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 无需重新抛出异常,通过Task的IsFaulted和IsCanceled属性也可以检测出Task是否发生了故障
    • 如果两个属性都返回false,那么就没有错误发生
    • 如果isCanceled为true,那就说明一个OperationCanceledException为该Task抛出了。
    • 如果IsFaulted为true,那就说明另一个类型的异常被抛出了,而Exception属性也将指明错误。

    异常与“自治”的Task

    • 自治的,“设置完就不管了”的Task,就是指不通过调用Wait()方法、Result属性或continuation进行会合的任务
    • 针对自治的Task,需要像Thread一样,显式的处理异常,避免发生悄无声息的故障
    • 自治Task上未处理的异常称为未观察到的异常。

    未观察到的异常

    • 可以通过全局TaskScheduler.UnobservedTaskException来订阅未观察到的异常

    1.15 Coninuation(继续、延续)

    • 一个Continuation会对Task说:“当你结束的时候,继续再做点其他的事”
    • Continuation通常是通过回调的方式实现的
    • 当操作一结束,就开始执行
    • 在task上调用GetAwaiter会返回一个awaiter对象
    • 它的OnCompleted方法会告诉之前的task,当你结束、发生故障的时候要执行委托
    • 可以将Continuation附加到已经结束的task上面,此时Continuation将会被安排立即执行。

    awaiter

    • 任何可以暴露下列两个方法和一个属性的对象就是awaiter
    • onCompleted
    • GetResult
    • 一个叫做IsCompleted的bool属性
    • 没有接口或者父类来统一这些成员
    • 其中OnCompleted是INotifyCompletion的一部分

    如果发生故障

    • 如果之前的任务发生故障,那么当Continuation代码调用awaiter.GetResult()的时候,异常会被重新抛出。
    • 无需调用GetResult,我们可以直接访问task的Result属性。
    • 但调用GetResult的好处是,如果task发生故障,那么异常会被直接的抛出,而不是包裹在AggregateException里面,这样的话catch块就简洁很多了。

    非泛型task

    • 针对非泛型的task,GetResult()方法有一个void返回值,它就是用来重新抛出异常。
    
    class Program
    {
        static void Main()
        {
            Task<int> primeNumberTask = Task.Run(() =>
                Enumerable.Range(2, 3000000).Count(n =>
                    Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
            var awaiter = primeNumberTask.GetAwaiter();
            // 你执行完了我立即执行回调
            awaiter.OnCompleted(() =>
            {
                // 有问题,异常会抛出,不直接拿结果就是为了如果有异常可以捕获
                int result = awaiter.GetResult();
                Console.WriteLine(result);
            });
            Console.ReadLine();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    1.16 TaskCompletionSource

    • Task.Run创建Task
    • 另一种方式就是用TaskCompletionSource来创建Task
    • TaskCompletionSource让你在稍后开始和结束的任意操作中创建Task
    • 它会为你提供一个可手动执行的从属Task,指示操作何时结束或发生故障
    • 它对IO-Bound类工作比较理想
    • 可以获得所有Task的好处(传播值、异常、Continuation等)
    • 不需要在操作时阻塞线程

    使用TaskCompletionSource

    • 初始化一个实例即可
    • 它有一个Task属性可返回一个Task
    • 该Task完全由TaskCompletionSource对象控制
    • 调用任意一个方法都会给Task发信号:完成、故障、取消
    • 这些方法只能调用一次,如果再次调用:
    • SetXXX会抛出异常
    • TryXxx会返回false
    public class TaskCompletionSource<TResult>
    {
        public void SetResult(TResult result);
        public void SetException(Exception exception);
        public void SetCanceled();
        ......
        public bool TrySetResult(TResult result);
        public bool TrySetException(Exception exception);
        public bool TrySetCanceled();
        public bool TrySetCanceled(CancellationToken cancellationToken);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    
    class Program
    {
        static void Main()
        {
            var tcs = new TaskCompletionSource<int>();
    
            new Thread(() =>
            {
                Thread.Sleep(5000);
                tcs.SetResult(42);
            })
            {
                IsBackground = true
            }.Start();
    
            Task<int> task = tcs.Task;
            Console.WriteLine(task.Result);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    1.17 同步异步

    同步、异步

    首先我们得跳出我们的固有思维,我们生活中常说的同步就是一起执行,但是计算机中同步却是另外的概念!

    举个例子,你家里只有一个洗手间,但是你跟你爸都想上厕所,怎么办?只能一个一个来,没有问题,问题

    在于你在等你爸从洗手间出来的这段时间里,你是站在门口一直等还是去干其他的?那么同步跟异步的概念就可以这样解释:

    同步:你在门口一直等,你爸用完了你进去,要是你妈也来了,也是站在后面等,就像排队挂号一样。把人当做线程,把洗手间当做资源,

    同一时间多个线程申请使用一个资源,要是在后面排队就是线程同步。

    那异步呢?很简单,你申请了想要用洗手间,但是你去看电视了,这个就叫异步,等有人告诉你洗手间空出来了,你再决定要不要去,当然你肯定得去,

    只是你可以自己决定什么时候去。一个线程同一时间访问多个资源,便称之为异步

    1.18 异步函数

    async 和 await关键字可以让你写出和同步代码一样简洁且结构相同的异步代码
    await关键字简化了附加continuation的过程
    其结构如下:
    var result = await expression;
    statement(s);
    它的作用相当于:
    var awaiter = expression.GetAwaiter();
    awaiter.OnCompleted(()=>
    {
    var result = awaiter.GetResult();
    statement(s);
    }
    );

    
    class Program
    {
        static async Task Main()
        {
            // 方法内有await,方法必须加上async
            // async方法无返回值必须加上Task
            await DisplayPrimesCountAsync();
        }
    
        // async无返回值要加上Task
        static async Task DisplayPrimesCountAsync()
        {
            int result = await GetPrimesCountAsync(2, 1000000);
            Console.WriteLine(result);
        }
    
        // Task泛型中的类型就是返回值的类型
        static Task<int> GetPrimesCountAsync(int start, int count)
        {
            return Task.Run(() =>
            ParallelEnumerable.Range(start, count).Count(n =>
                Enumerable.Range(2, (int)Math.Sqrt(n) - 1).All(i => n % i > 0)));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    async修饰符

    • async修饰符会让编译器把await当作关键字而不是标识符
    • async修饰符只能应用于方法(包括lambda表达式)
    • 该方法可以返回void、Task、Task
    • async修饰符对方法的签名或public元数据没有影响,它只会影响方法内部
    • 在接口内使用async是没有意义的
    • 使用async画重载非async的方法是合法的(只要方法签名一致)
    • 使用了async修饰符的方法就是"异步函数"

    异步方法如何执行

    • 遇到await表达式,执行(正常情况下)会返回调用者
    • 就像iterator里面的yield return
    • 在返回前,运行时会附加一个continuation到await的task
    • 为保证task结束时,执行会跳回原方法,从停止的地方继续执行
    • 如果发生故障,那么异常会被重新抛出
    • 如果一切正常,那么它的返回值就会赋给await表达式

    如下两个方法基本一样:

    void DisplayPrimesCount()
    {
        var awaiter = GetPrimesCountAsync(2, 1000000).GetAwaiter();
        // 获得延续体继续执行,以获取到当前线程的环境
        awaiter.OnCompleted(()=>
        {
            int result = awaiter.GetResult();
            Console.Writeline(result);
        });
    }
    
    static async Task DisplayPrimesCountAsync()
    {
        // await会获得一个延续体,回来获得当前线程的环境
        int result = await GetPrimesCountAsync(2, 1000000);
        Console.Writeline(result)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    捕获本地状态

    • await 表达式的最牛之处就是它几乎可以出现在任何地方
    • 特别的,在异步方法内,await表达式可以替换任何表达式
    • 除lock表达式和unsafe上下文
    async void DisplayPrimeCounts()
    {
        for (int i=0; i<10; i++)
            // GetPrimesCountAsync是一个异步方法
            // await每次调用完异步方法都回加到这里来而且能捕获到本地变量i
            Console.WriteLine(await GetPrimesCountAsync(i*1000000+2, 1000000);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    await之后在那个线程上执行

    • 在await表达式之后,编译器依赖continuation(通过awaiter模式)来继续执行
    • 如果在富客户端应用的UI线程上,同步上下文会保证后续是在原线程上执行
    • 否则,就会在task结束的线程上继续执行

    1.19 编写异步函数

    对于任何异步函数,你可以使用Task替代void作为返回类型,让该方法成为更有效的异步(可以进行await)

    class Program
    {
        static async Task Main()
        {
            await PrintAnswerToLife();
        }
    
        static async Task PrintAnswerToLife()
        {
            await Task.Delay(1000);
            int answer = 21 * 2;
            Console.WriteLine(answer);
        }
       
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    并不需要在方体中显式的返回Task。编译器会生成一个Task(当方法完成或发生异常时),这使得创建异步的调用链非常方便。

    
    class Program
    {
        static async Task Main()
        {
            await Go();
        }
    
        static async Task Go()
        {
            await PrintAnswerToLife();
            Console.WriteLine("Done");
        }
    
        static async Task PrintAnswerToLife()
        {
            await Task.Delay(1000);
            int answer = 21 * 2;
            Console.WriteLine(answer);
        }
       
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    编译器会对返回Task的异步函数进行扩展,使其成为当发送信号或发生故障时使用TaskCompletionSource来创建Task代码。
    因此,当返回Task的异步方法结束的时候,执行就会跳回到对它进行await的地方。(通过continuation)

    下面这段代码

    static async Task PrintAnswerToLife()
        {
            await Task.Delay(1000);
            int answer = 21 * 2;
            Console.WriteLine(answer);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    编译器会给我们翻译成:

     static Task PrintAnswerToLife()
        {
            var tcs = new TaskCompletionSource<Object>();
            var awaiter = Task.Delay(1000).GetAwaiter();
            awaiter.OnCompleted(async () =>
            {
                try
                {
                    awaiter.GetResult();
                    int answer = 21 * 2;
                    Console.WriteLine(answer);
                    tcs.SetResult(null);
                }
                catch (Exception ex)
                {
                    tcs.SetException(ex);
                }
            });
            return tcs.Task;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    返回Task
    如果方法体返回TResult,那么异步方法就可以返回Task
    其原理就是给TaskCompletion发送信号带有值,而不是null

    
    class Program
    {
        static async Task Main()
        {
            int result = await GetAnswerToLife();
            Console.WriteLine(result);
        }
        static async Task<int> GetAnswerToLife()
        {
            await Task.Delay(5000);
            int answer = 21 * 2;
            return answer;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    异步Lambda表达式
    匿名方法(包括Lambda表达式),通过使用asyn也可以变成异步方法

    
    class Program
    {
        static async Task Main()
        {
            Func<Task> unnamed = async () =>
            {
                await Task.Delay(1000);
                Console.WriteLine("unnamed");
            };
            await NamedMethod();
            await unnamed();
        }
        static async Task NamedMethod()
        {
            await Task.Delay(1000);
            Console.WriteLine("NamedMethod");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    异步Lambda表达式也可以返回Task

    
    class Program
    {
        static async Task Main()
        {
            Func<Task<int>> unnamed = async () =>
            {
                await Task.Delay(1000);
                return 123;
            };
            int result =await unnamed();
            Console.WriteLine(result);
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    1.20 异步中的同步上下文

    1.21 优化同步完成

    编写完全没有await的异步方法也是合法的,但是编译器会发出警告
    async Task Foo() {return “abc”;}
    另外一种可以达到相同结果的方式是:使用Task.FromResult,它会返回一个已经设置好信号的Task
    Task Foo() { return Task.FromResult(“abc”);}

    1.22 TAP

    TAB Task-Based Asynchronous Pattern 基于Task的异步模式(TAP)

  • 相关阅读:
    [BSidesCF 2019]Kookie
    es分布式全文搜索引擎介绍、下载和安装、索引操作、文档操作
    算法刷题第一天:二分查找
    prometheus学习5altermanager
    gRPC 四模式之 服务器端流RPC模式
    乐观锁与悲观锁
    Zookeeper集群 + Kafka集群
    如何找到联盟营销人员:招募合适会员的10个方法
    解决Oracle SQL语句性能问题——SQL语句改写(分析函数、with as、union及or)
    消息队列中间件的选型与比较
  • 原文地址:https://blog.csdn.net/weixin_42710036/article/details/127941541