C#之多线程和异步编程

C#之多线程和异步编程

1.进程、线程计算机的概念

    • 进程:一个程序运行时,占用的全部计算资源的总和
    • 线程:
      • 线程本质上不是一个计算机的硬件功能,而是操作系统提供的一种逻辑功能
      • 程序执行流的最小单位
      • 任何操作都是有线程完成的
      • 线程是依托于进程存在的,一个进程可以包含多个线程
      • 线程也可以有自己的计算资源
    • 多线程:多个执行流同时运行
      • CPU太快了,分时间片:上下文进行切换(加载环境--计算--保存环境);微观角度,一个核同一时刻只能执行一个线程;宏观角度,多线程并发
      • 多CPU多核,可以独立工作;4核8线程--核是物理的核,线程是指虚拟核

2.同步和异步

    • 同步:完成计算之后,再进入下一行
    • 异步:不会等待方法的完成,会直接进入下一行(非阻塞)

3.异步和多线程的联系和区别

    • 相同点
      • 异步和多线程两者都可以达到避免调用线程使程序发生阻塞的目的,来提高软件的响应性和流畅度
    • 区别
      • 多线程是多个Thread并发,需要开辟新的线程
      • 异步是硬件式的异步,没有开辟新的线程,简单来说就是当CPU发送完操作命令后,就不再等着命令的执行结束,而是可以去执行别的任务,当上述任务结束时,会触发一个回调,告诉CPU任务执行完毕了,可以接着执行了
      • 异步更强调等待完成,多线程更强调并行
      • 异步方法通常使用回调来进行处理

4.异步多线程无序

    • 启动无序,执行时间不确定,结束也无序,一定不要通过等几毫秒的形式来控制启动/执行时间/结束
    • 可以通过回调/状态等待/信号量来进行控制

5.前台线程和后台线程

    • 默认情况下,手动创建的线程属于前台线程
    • 只要有前台线程在运行,那么应用程序就会一直处于活跃状态
    • 只要后台线程则应用程序不会处于活跃状态,一旦所有的前台线程停止,那么应用程序就停止了,任何的后台线程也会突然终止
    • C#中可以通过IsBackground属性判断线程是否属于后台线程,若程序的前台线程全部执行完后,则后台线程执行栈中的finally块也就不会被执行了
    • 注意:线程的前台、后台状态与它的优先级无关(所分配的执行时间);若应用程序无法正常退出,则有一个常见的原因是还有活跃的前台线程

6.线程安全

    • 状态的保存:
      • Local本地状态,CLR为每一个线程分配自己的内存栈(Stack),使本地变量保持独立
      • 如果多个线程引用到同一个对象的实例,则就共享了数据
      • 被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段(field),也会被共享
      • 静态字段(field)也会在线程间共享数据。此时会存在线程安全的问题
    • 在读取和写入共享数据的时候,通过使用一个互斥锁(exclusive lock),可以避免同时修改共享数据
    • C#使用lock语句来加锁
      • 当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,直到锁变成可用状态
      • 在多线程上下文中,以这种方式避免不确定的代码来使得线程安全
      • 注意:Lock也会引起一些问题,例如死锁
    • 异常处理
      • 创建线程时在作用范围内的try/catch/finally块,在线程开始执行后就与线程无关了,需要将捕获放置在线程所执行的代码块中
      • 在WPF、WinForm里,可以订阅全局异常处理事件:
        • Application.DispatcherUnhandledException
        • Application.ThreadException
        • 在通过消息循环调用的程序任何部分发生未处理的异常(相当于应用程序处于活跃状态时在主线程上运行的所有代码)后,将触发这些异常
        • 但是在非UI线程上的未处理异常,并不会触发它
      • 任何线程有任何未处理的异常都会触发的事件:
        • AppDomain.CurrentDomain.UnhandledException
      • 富客户端应用通常依赖于集中的异常处理事件来处理UI线程上未捕获的异常
        • WPF中的Application.DispatcherUnhandledException
        • ASP.NET Core中定制的ExceptionFilterAttribute也是相同的效果

7.线程优先级

    • 线程的优先级(Thread的Priority属性)决定了相对于操作系统中其它活跃线程所占的执行时间
    • 提升线程优先级:
      • 提升线程优先级的时候需要特别注意,因为它可能“饿死”其它线程
      • 如果想让某线程的优先级比其它进程中的线程优先级高,那就必须提升线程的优先级
      • 对于需要大量计算的应用程序(尤其是有UI的应用程序),提高线程优先级可能会使其他线程“饿死”,从而降低整个计算机的速度

8.信号

    • 如果需要让某个线程一直处于等待的状态,直至接收到其它线程发来的通知(信号)
    • 最简单的信号结构就是ManualResetEvent
      • 调用其WaitOne方法会阻塞当前的线程,直到另一个线程通过调用set方法来开启信号
      • 调用完Set之后,信号就会处于“打开”的状态,并且可以通过调用Reset方法将其再次关闭
    var signal = new ManualResetEvent(false);
     new Thread(() => {
        Console.WriteLine("Waiting for signal ...");
        signal.WaitOne();//阻塞当前线程
        signal.Dispose();
        Console.WriteLine("Go signal! ...");
     }).Start();
     Thread.Sleep(3000);
     signal.Set();//打开了信号

9.客户端应用程序中的线程

    • 在WPF,WinForm,UWP等类型的程序,如果在主线程执行非常耗时的操作,就会导致整个应用程序无响应(假死)。其主线程在处理计算任务的同时,还需要处理消息循环(渲染,鼠标键盘事件等工作)
    • 针对耗时的操作,主要流行的做法是启用一个worker线程,执行完计算任务后,再更新到UI
    • 客户端应用的的线程模型通常是:
      • UI元素和控件只能从创建它们的线程来访问(通常是主UI线程)
      • 如果需要从worker线程更新UI,则必须把请求交给UI线程,实现方式包括:
        • WPF,在元素的Dispatcher对象上调用BeginInvoke或Invoke
        • WinForm,调用空间的BeginInvoke或Invoke
        • UWP,调用Dispatcher对象上的RunAsync或Invoke
      • BeginInvoke和RunAsync通过将委托排队到UI线程的消息队列来执行工作
      • Invoke执行相同的操作,但随后会进行阻塞,直到UI线程读取并处理消息
        • Invoke可以从方法中获取返回值
        • 如果不需要返回值,则BeginInvoke和RunAsync更好,不会阻塞UI,也不会引入死锁的可能性
      • 同步上下文(Synchronization Contexts)
        • 在System.ComponentModel下有一个抽象类:SynchronizationContext,它使得Thread Marshaling得到泛化
        • 针对移动,桌面(WPF、UWP、WinForms)等富客户端应用的API,它们都定义和实例化了SynchronizationContext的子类
          • 可以通过静态属性 SynchronizationContext.Current来获得(当运行在UI线程时)
          • 捕获Post就相当于调用Dispatcher或Control上面的BeginInvoke方法,而send方法,等价于Invoke方法
    public partial class Form1 : Form
        SynchronizationContext _uiSyncContext;
        public Form1()
            InitializeComponent();
            _uiSyncContext = SynchronizationContext.Current;
            new Thread(work).Start();//开启一个线程
        void work()
            Thread.Sleep(5000);//模拟耗时操作
            updateMessage("改变了");
        void updateMessage(string message)
            _uiSyncContext.Post((state) => label1.Text = message, null);//通过异步同步上下文

10.线程池

    • 当开始一个线程的时候,将花费几百微秒来组织类似以下的内容:
      • 一个新的局部变量栈(Stack)
    • 线程池就可以节约上述这种开销:
      • 通过预先创建一个可循环使用的线程池
    • 线程池对于高效的并行编程和细粒度并发是极为重要的
    • 线程池允许在不被线程启动的开销淹没的情况下运行短期操作
    • 使用线程池需要注意的:
      • 不可以设置线程池的Name
      • 线程池线程都是后台线程
      • 阻塞线程池线程可能会使性能降级
      • 可以更改线程池线程的优先级,但是当它释放回线程池的时候,其优先级将还原
      • 可以通过Thread.CurrentThread.IsThreadPoolThread属性来判断是否执行在线程池上
    • 以下内容使用了线程池:
      • WCF,Remoting,ASP.NET,ASMX Web Services应用服务器
      • System.Timers.Timer,System.Threading.Timer
      • 并行编程结构
      • BackgroundWorker类
      • 异步委托
    • 最简单的、显示的在线程池上运行代码的方式为使用Task.Run
Task.Run(() => { Console.WriteLine("Hello World"); });

11.异步编程之Task类

异步编程的原则是将长时间运行的函数写成异步。在传统的做法中是将长时间运行的函数写成同步,然后从新的线程或Task中进行调用,按需进行并发,而异步编程是从长时间运行函数的内部启动并发,其优势是:计算并发可不使用线程来实现,从而提高可扩展性和执行效率;可以减少富客户端的worker线程的代码,从而简化了线程安全性。
Task是一个相对高级的抽象:其代表一个并发操作(concurrent)。该操作可能由Thread支持,或者不由Thread支持。
Task是可以组合的(可使用Continuation把它们连接起来) Task可以使用线程池来减少启动延迟;使用TaskCompletionSource,Task可以利用回调的方式,在等待I/O绑定操作时完全避免线程。
  • Task默认使用线程池,也就是后台线程:
    • 当主线程结束时,你创建的所有tasks都会结束
    • Task.Run会返回一个Task对象,可以使用它来监控其过程
      • 调用task的wait方法会进行阻塞直到操作完成
    • 在线程池中运行的Task,非常适合短时间运行的计算类工作
    • 针对长时间运行的任务或阻塞操作,可以不采用线程池
  • Task的返回值
    • Task有一个泛型的子类Task,其会发出一个返回值
    • 使用Func委托或兼容的Lambda表达式来调用Task.Run就可以得到Task
    • 如果task没有完成,则访问Result属性会阻塞该线程直到task完成操作
  • Task异常
    • task里面抛出了一个未处理的异常,那么该异常就会重新被抛给:
      • 调用了wait()的地方
      • 访问Task的Result属性的地方
    • 如果没有抛出异常,通过Task的IsFaulted和IsCanceled属性也可以检测出Task是否发生了故障
      • 如果两个属性都返回false,那么没有错误发生
      • 如果IsCanceled为true,那么就说明一个OperationCanceledexception被Task抛出
      • 如果IsFaulted为true,那么就说明另一个类型的异常被抛出,其Exception属性也将指明该错误
  • Task的链式调用(Continuation、ContinueWith)
    • Continuation通常是通过回调的方式来实现,当操作一结束,就开始执行
    • 在Task上调用GetAwaiter会返回一个awaiter对象
    • 可以将Continuation附加到已经结束的task上面,则此时Continuation将会立即被执行
    • 如果任务发生异常,则在Continuation中调用awaiter.GetResult()时,异常会重新被抛出
    • 调用awaiter.GetResult()(没有返回值同样)相比Result属性的好处是,如果task发生故障,则异常会直接被抛出
    • 如果出现同步上下文,则Continuation会重新回到UI线程中(没有则会继续运行在task线程上),假如不希望回到UI线程中,则可以通过ConfigureAwait方法来避免,此时还是会回到task线程上
    • 其另一种附加Continuation的方式就是调用task的ContinueWith方法
    • ContinueWith本身返回一个task,可以用来附加更多的Continuatio
    • Task的另一种执行异步方法的方式为使用TaskCompletionSource来创建Task
      • TaskCompletionSource可以在任意操作中指定开始
      • 提供一个“从属”的Task来指示操作何时结束或发生故障
    //线程池运行任务
    Task task = Task.Run(() => {
        Thread.Sleep(3000);
        Console.WriteLine("Foo");
    Console.WriteLine(task.IsCompleted);//false
    task.Wait();//阻塞直到task完成操作
    Console.WriteLine(task.IsCompleted);//true
    //非线程池运行任务
    Task task2 = Task.Factory.StartNew(() => {
        Thread.Sleep(3000);
        Console.WriteLine("Foo");
    },TaskCreationOptions.LongRunning);
     //Task返回值
    Task<int> task3 = Task.Run(() => {
        Console.WriteLine("Foo");
        return 1;
    int result = task3.Result;//如果task没有完成,那么会阻塞
    Console.WriteLine(result);
    //Task的Continuation
    Task<int> task4 = Task.Run(() =>
        Thread.Sleep(3000);
        return 1;
    var awaiter = task4.GetAwaiter();
    awaiter.OnCompleted(() =>
        int result4 = awaiter.GetResult();
        Console.WriteLine(result4);
    //Task的Continuation,不返回同步上下文
    Task<int> task5 = Task.Run(() =>
        Thread.Sleep(3000);
        return 1;
    var awaiter5 = task5.ConfigureAwait(false).GetAwaiter();
    awaiter.OnCompleted(() =>
        int result5 = awaiter.GetResult();
        Console.WriteLine(result5);
    //Task的ContinueWith
    Task<int> task6 = Task.Run(() =>
        Thread.Sleep(3000);
        return 1;
    task6.ContinueWith(t =>
        int result6 = t.Result;
        Console.WriteLine(result6);
        Thread.Sleep(3000);
        return 2;
    }).ContinueWith(t2 =>
        int result6 = t2.Result;
        Console.WriteLine(result6);
    //异步函数执行,并发10次
    static void Main(string[] args)
        Task.Run(() => DisplayPrimeCounts());
        Console.ReadLine();
    static void DisplayPrimeCounts()
        //并发10次
        for (int i = 0;i < 10;i++)
            var awaiter = GetPrimesCountAsync(i).GetAwaiter();
            awaiter.OnCompleted(() =>
                Console.WriteLine(awaiter.GetResult());
    static Task<int> GetPrimesCountAsync(int m)
    {//异步函数
        return Task.Run(() =>
            Thread.Sleep(1000);
            return m;
  • async和await关键字可以按照写同步代码的方式来构建简洁且结构相同的异步代码
    • await关键字简化了附加的Continuation过程
    • async修饰符会让编译器把await当作关键字而不是标识符(c#5以前可能会使用await作为标识符)
    • async修饰符只能应用于方法(包括lambda表达式)
      • 该方法可以返回void、Task、Task
    • async修饰符对方法的签名或public元数据没有影响(和unsafe一样),它只会影响方法的内部
      • 在接口内使用async是没有意义的
      • 使用async来重载非async的方法是合法的(只要方法的签名一致)
    • 使用了async修饰符的方法就是“异步函数”
    • 在异步方法内,await表达式可以替换任何表达式,除了lock表达式和unsafe上下文
    • await表达式之后在以下线程上执行:
      • await之后,编译器依赖于Continuation来继续执行
      • 如果在富客户端应用的UI线程上,同步上下文会保证后续是在原线程上执行
      • 否则,就会在task结束的线程上继续执行
    • 匿名方法(包括Lambda表达式),通过使用async也可以变成异步方法,调用方式也一样
    • 附加的event handler的时候也可以使用异步Lambda表达式
  • 对于在循环中多次调用的异步方法,通过调用ConfigureAwait方法,可以避免重复弹回到UI消息循环中所带来的开销
    //async和await
    async void DisplayPrimeCounts()
        var result = await GetPrimesCountAsync(i);
        Console.WriteLine(result);
    static Task<int> GetPrimesCountAsync(int m)
    { //异步函数
        return Task.Run(() =>
            Thread.Sleep(1000);
            return m;
    //async在匿名函数中的使用
    async void Test()
        Func<Task> unnamed = async () =>
            await Task.Delay(1000);
            Console.WriteLine("Foo");
        await NamedMethod();
        await unnamed();
    static async Task NamedMethod()
        await Task.Delay(1000);
        Console.WriteLine("Foo");
  • 对于异步方法,可以利用CancellationToken来取消异步操作
  • 在异步方法中可以使用IProgress和Progress来报告完成的进度
  • Task组合器
    • Task.WhenAny,当一组Task中的任何一个Task完成时,Task.WhenAny就会返回完成的Task。如果当一组Task中已经完成某一个任务,则没完成的Task在后续中发生异常,则异常不会被检测到,除非对后续的所有Task进行awwit
    • Task.WhenAll,当一组Task中的所有Task都完成时,Task.WhenAll就会返回完成的Task。与WhenAny不同的是,如果一组Task中的某一个Task出现异常,则无需等待后续的Task,其错误也会被检测到
    //取消异步操作
    void Test1()
        var cancelSource = new CancellationTokenSource();//实例化一个CancellationTokenSource
        Task foo = Foo(cancelSource.Token);
        Thread.Sleep(1000);
        cancelSource.Cancel();//取消异步操作
    async Task Foo(CancellationToken cancellationToken)
        for (int i = 0;i < 10;i++)
            Console.WriteLine(i);
            await Task.Delay(1000);
            cancellationToken.ThrowIfCancellationRequested();//当得到取消操作的时候,将其抛出停止异步执行
    //Task组合器
    async Task<int> Delay1() { await Task.Delay(1000);return 1; }
    async Task<int> Delay2() { await Task.Delay(1000); return 2; }
    async Task<int> Delay3() { await Task.Delay(1000); return 3; }
    Task<int> t1 = Delay1();
    Task<int> t2 = Delay2();
    Task<int> t3 = Delay3();
    Task.WhenAny(new Task<int>[] { t1, t2, t3 }).ContinueWith(t =>
    {//当其中一个Task执行完后,返回完成的Task结果
        Console.WriteLine(t.Result.Result);
    Task.WhenAll(new Task<int>[] { t1, t2, t3 }).ContinueWith(t =>
    {//当所有Task全部执行完后,返回所有Task的结果