C#中多线程Task详解

C#中多线程Task详解

参考文章:

添加链接描述
添加链接描述
添加链接描述
添加链接描述
添加链接描述
添加链接描述
添加链接描述
添加链接描述
添加链接描述
添加链接描述
添加链接描述
添加链接描述

1.常用多线程创建方式比较

1.1Thread方式

缺点:频繁的创建和消耗比较好资源;提供操作线程的API不是马上响应(线程是操作系统统一管理,收到指令之后,具体还得操作系统真实处理,而操作系统收到指令之后并非马上执行相关指令);

1.2ThreadPool方式

优点:池化线程进行管理,需要使用就从池中获取就行,避免频繁创建和销毁线程;从而可以达到线程的复用;
缺点:提供的API太少,线程等待顺序控制比较弱;从而在一些业务情况下操作不方便;

1.3 Task方式

在ThreadPool的思想进行了封装,继承了ThreadPool的优点;提供了丰富的线程控制API,从而方便了开发;

1.4 Task方式介绍

Task可以简单看作相当于Thead+TheadPool,其性能比直接使用Thread要更好,在工作中更多的是使用Task来处理多线程任务
net4.0在ThreadPool的基础上推出了Task类,微软极力推荐使用Task来执行异步任务,现在C#类库中的异步方法基本都用到了Task;net5.0推出了async/await,让异步编程更为方便。

1.5 什么时候使用多线程

1.可以并发执行
例子:查询数据—可能查询接口、可能查询数据、可能查询缓存,开启3个线程,同时执行。大型项目中需要经常打开关闭数据库,且可能有多个界面同时调用数据库,会耗费大量时间。

例子:查询数据—查询一个结果后,需要使用这个结果作为条件,再进行下一次查询,还需要查询结果继续查询------不能使用多线程

例子:列表数据----可能来自数据库—可能来自缓存—可能来自第三方接口
传统方法:一个一个查询再去判断
多线程:同时启用3个线程查询,等待其中一个线程结束,判断是否找到结果,有一个找到结果,另外2个线程可以不用再管 //task.waitany //continuewhenany

2.Task创建任务的几种方式

2.1 Task task = new Task

		Task task = new Task(() =>
            {
    
    
                Console.WriteLine($"Task 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(2000);
            });
            //开启任务
            task.Start();

new方式实例化一个Task,需要通过Start方法启动

2.2 Task task1 = Task.Run最常用

Task task1 = Task.Run(() =>
   {
    
    
       Console.WriteLine($"Task 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
       Thread.Sleep(2000);
   });

Task.Run(Action action)将任务放在线程池队列,返回并启动一个Task

2.3 Task task2 = Task.Factory.StartNew 实例化工厂

//task静态属性获取Factory
Task task2 = Task.Factory.StartNew(() =>
    {
    
    
        Console.WriteLine($"Task 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
        Thread.Sleep(2000);
    }); 
//TaskFactory 工厂实例化,二者概念不一样
TaskFactory taskFactory1=new TaskFactory();
            taskFactory1.StartNew(() =>
            {
    
    
                Console.WriteLine($"Task 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(2000);
            });

Task.Factory.StartNew(Action action)创建和启动一个Task

2.4 Task有返回值的创建方式

有返回值的创建方式用法与上面3种常见的方式一样,代码示例如下:

static void Main(string[] args)
        {
    
    
            1.new方式实例化一个Task,需要通过Start方法启动
            Task<string> task = new Task<string>(() =>
            {
    
    
                return $"hello, task1的ID为{
      
      Thread.CurrentThread.ManagedThreadId}";
            });
            task.Start();

            2.Task.Factory.StartNew(Func func)创建和启动一个Task
            Task<string> task2 = Task.Factory.StartNew<string>(() =>
            {
    
    
                return $"hello, task2的ID为{
      
      Thread.CurrentThread.ManagedThreadId}";
            });

            3.Task.Run(Func func)将任务放在线程池队列,返回并启动一个Task
            Task<string> task3 = Task.Run<string>(() =>
            {
    
    
                return $"hello, task3的ID为{
      
      Thread.CurrentThread.ManagedThreadId}";
            });

            Console.WriteLine("执行主线程!");
            Console.WriteLine(task.Result);
            Console.WriteLine(task2.Result);
            Console.WriteLine(task3.Result);
            Console.ReadKey();
        }

task.Resut获取结果时会阻塞线程,即如果task没有执行完成,会等待task执行完成获取到Result,然后再执行后边的代码,程序运行结果如下:
在这里插入图片描述

3.委托方式与lambda方式

线程创建好之后,需要用线程执行方法实现想要的功能,可通过委托的方式或labmda方式执行,上述task任务3种创建方式均为lambda,lambda方式与函数方式是一致的,以下多一个简单的对比。
lambda方式代码例子:

private static void ContinueWithTest()
        {
    
    
            Console.WriteLine("start");
            Task<string> task1 = new Task<string>(() =>
            {
    
    
                Console.WriteLine("task0");
                return "task1";
            });
            Task<string> task2 = task1.ContinueWith(t =>
            {
    
    
                //task1执行完后执行task2里面的逻辑,所以
                //一定能在该语句里面拿到task1的返回值即t.Result,且与主线程异步
                Console.WriteLine(t.Result);
                return "task2";
            });
            task1.Start();
            Console.WriteLine("end");
        }

委托方式调用函数代码例子:

private static void ContinueWithTest1()
        {
    
    
            Console.WriteLine("start");
              // Func<string> func = new Func<string>(DoTask1);

             //或Func<string> func=DoTask1
            // Task<string> task1 = new Task<string>(func);写成这样也是与下面要实现的功能一样

            Task<string> task1 = new Task<string>(DoTask1);
            Task<string> task2 = task1.ContinueWith(DoTask2);
            task1.Start();
            Console.WriteLine("end");
        }

        private static string DoTask1()
        {
    
    
            Console.WriteLine("task0");
            return "task1";
        }

        private static string DoTask2(Task<string> t)
        {
    
    
            Console.WriteLine(t.Result);
            return "task2";
        }

上述两种方式执行的结果是一致的

4.Task常用的API

当一个进程中出现多个线程时,线程执行的先后顺序常常是不规律的,例如执行完线程1,会紧接着执行线程6,而不是按顺序执行线程2,即整个执行过程会跳来跳去,这也导致当线程过多时,程序执行变得不可控,产生莫名其妙的BUG,加大了调试代码的难度,同时执行的最终结果可能会偏离预期。因此需要对线程加一些约束。
常用的API函数主要包括:实例方法.Wait()、实例方法.ContinueWith()、Task.WaitAll、Task.WaitAny、Task.Factory.ContinueWhenAll、Task.Factory.ContinueWhenAny等,以下分别对每种API函数详解。

Wait/WaitAny/WaitAll方法返回值为void,这些方法单纯的实现阻塞线程。现在想让所有task执行完毕(或者任一task执行完毕)后,开始执行后续操作,怎么实现呢?这时就可以用到WhenAny/WhenAll方法了,这些方法执行完成返回一个task实例。 task.WhenAll(Task[] tasks) 表示所有的task都执行完毕后再去执行后续的操作, task.WhenAny(Task[] tasks) 表示任一task执行完毕后就开始执行后续操作

先不采用约束,看一下线程执行的常规流程;

static void Main(string[] args)
        {
    
    
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}开启");

            var task1 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task1 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(10000);
            });

            var task2 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task2 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(5000);
            });
            var task3 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task3 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(500);
            });

            var task4 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task4 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(2000);
            });

            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}完成");
            Console.ReadKey();
        }

第一次执行结果如下图1所示:
在这里插入图片描述

第二次执行结果如下图2所示:
在这里插入图片描述
“主线程1开启”、“主线程1完成”是并行执行的,task1、task2、task3、task4任务没有规律,如上图1、图2所示,两次执行的程序代码相同,但线程执行的先后顺序不一致,为了约束线程执行过程,采用API。

4.1 实例方法.wait()

.Wait() 等待执行调用任务完成,然后执行下一步; 即阻塞了主线程;
在常规流程中加入wait方法,如下:

static void Main(string[] args)
        {
    
    
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}开启");

            var task1 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task1 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(10000);
            });

            task1.Wait();

            var task2 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task2 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(5000);
            });
            task2.Wait();

            var task3 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task3 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(500);
            });

            task3.Wait();
            var task4 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task4 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(2000);
            });

            task4.Wait();
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}完成");
            Console.ReadLine();
        }

执行结果如下图3所示,此时线程按代码顺序执行,同时阻塞线程,任务1完成之后,执行任务2、任务3、任务4,异步线程变为同步线程,整个线程执行时间t=10000+5000+500+2000
在这里插入图片描述

4.2 实例方法.ContinueWith()

ContinueWith() 等调用者结束之后才进行调用里面的相关业务,由线程池分配线程进行处理接下来的业务,不阻塞主线程,但却能控制业务之间的先后顺序;
常规流程中加入代码,如下所示:

static void Main(string[] args)
        {
    
    
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}开启");

            var task1 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task1 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(10000);
            });

            var task2 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task2 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(5000);
            });
           
            var task3 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task3 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(500);
            });

            task3.ContinueWith(t =>
            {
    
    
                Console.WriteLine($"Task3 后续执行{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(500);
            });

            var task4 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task4 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(2);
            });
         
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}完成");
            Console.ReadLine();
        }

执行结果如下所示,不论执行多少次,“Task3 后续执行”永远在“Task3 开启线程”之后,即等待“Task3 开启线程”完成之后,才会处理“Task3 后续执行”,并不会阻塞其他线程。
在这里插入图片描述

4.3 静态方法,Task.WaitAll

Task.WaitAll等到所有任务都完成之后,才进行主线程的下一步操作,即阻塞主线程;
代码如下:

static void Main(string[] args)
        {
    
    
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}开启");

            var task1 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task1 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(10000);
            });

            var task2 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task2 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(5000);
            });
           
            var task3 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task3 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(500);
            });

            var task4 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task4 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(2);
            });

            List<Task> list1 = new List<Task> {
    
     task1,task2,task3,task4};
            Task.WaitAll(list1.ToArray());
         
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}完成");
            Console.ReadLine();
        }

执行结果如下图所示,主线程之前的task1、task2、、task3、、task4执行多次顺序会不同,即线程之间是无序的,但是等4个线程即所有任务都完成之后,才会执行“主线程1完成”。
在这里插入图片描述

4.4 静态方法Task.WaitAny

同Task.WaitAll,等待任何一个任务完成就继续向下执行,将上面的代码WaitAll替换为WaitAny
即当task,task2,task3…N任意一个任务都执行完成之后就会往下执行代码,
task.WaitAny等到其中一个任务完成之后,才进行主线程的下一步操作,其中任务没有完成之前也阻塞主线程;

4.4 任务工厂Task.Factory.ContinueWhenAll

当ContinueWhenAll中所有任务都完成时执行回调方法,不阻塞主线程
代码如下:

static void Main(string[] args)
        {
    
    
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}开启");

            var task1 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task1 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(10000);
            });

            var task2 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task2 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(5000);
            });
           
            var task3 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task3 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(500);
            });

            var task4 = Task.Run(() =>
            {
    
    
                Console.WriteLine($"Task4 开启线程{
      
      Thread.CurrentThread.ManagedThreadId}处理业务");
                Thread.Sleep(2);
            });

            List<Task> list1 = new List<Task> {
    
    task1,task2,task3,task4};
            Task.Factory.ContinueWhenAll(list1.ToArray (),tasks =>
            {
    
    
                Console.WriteLine($"任务执行结束");
            });
         
            Console.WriteLine($"主线程{
      
      Thread.CurrentThread.ManagedThreadId}完成");
            Console.ReadLine();
        }

执行结果如下:
在这里插入图片描述
不阻塞主线程, 当封装在所有list中的任务全部执行完成之后,再进行后续的处理,其中后续处理的业务的参数是上一完成任务的列表!!!

4.4 任务工厂Task.Factory.ContinueWhenAny

当参数中的任务有一个完成之后就进行回调,执行下一个任务。
Task.Factory.ContinueWhenAny方法等其中的任务有一项完成之后就立即返回,调用后续业务,不阻塞主线程
实际开发中建议使用ContinueWhenAny、ContinueWhenAll 不阻塞线程,尤其是在UI界面开发中

4.5 Thread.Sleep与Task.Delay的区别

这两个函数,实际工程中也经常用到,都表示延期执行某个功能,但是 Thread.Sleep在延期时间内会阻塞主线程,例如: Thread.Sleep(5000),在UI界面中会卡顿界面5秒,界面无法执行其他操作,原因是Thread属性IsBackground默认为前台线程,Task.Delay(5000)延时期间不会阻塞主线程

5.Task任务取消

线程在执行过程中难免会出错,或者执行相当长一段时间仍未执行完成,这就需要取消线程。
如下例子:首页包含很多信息,数据来自不同渠道,多块信息展示
传统方法:主线程一个一个查询,然后返回
多线程:开启多个线程同时查询,等待所有结果返回 //task.waitall //continuewhenall
如果一个模块的信息获取失败,必须重新获取,数据不能使用,另外获取数据的几个线程还在执行,则另外几个线程是否还有必要执行?需要做线程的取消

1.线程不能从外部取消,只能自己取消自己(对外抛出异常)
2.定义信号量,如果有线程执行出错,通知信号量,如果信号量被改变,则直接取消自己
3.定义布尔变量做信号量,不推荐
4.标准实现:CancellationTokenSource

C# 使用 CancellationTokenSource 终止线程,取消线程不是影响线程内部的执行(线程内部的执行根据系统内存资源自动分配),而是外部对Task的控制,如取消、定时取消。

5.1 任务取消常见方式

如下代码:

class Program
     {
    
     

        static CancellationTokenSource cancelTokenSource = new CancellationTokenSource();

        static void Main(string[] args)
        {
    
    
            Task.Factory.StartNew(MyTask, cancelTokenSource.Token);

            Console.WriteLine("请按回车键(Enter)停止");
            Console.ReadLine();

            cancelTokenSource.Cancel();

            Console.WriteLine("已停止");
            Console.ReadLine();
        }

        static void MyTask()
        {
    
    
            //判断是否取消任务
            while (!cancelTokenSource.IsCancellationRequested)
            {
    
    
                Console.WriteLine(DateTime.Now);
                Thread.Sleep(1000);
            }
        }
    }

执行结果如下图所示,当线程未取消,即未按下“ENTER”键,线程执行委托方法MyTask,代码cancelTokenSource.Cancel();及后面的代码是不会执行的,当按下“enter”键,才会执行后面的代码。
在这里插入图片描述

5.2 延时取消

也可以使用定时取消的方式,当任务超过了我们设定的时间,取消任务

var cancelTokenSource = new CancellationTokenSource(3000);

或者采用如下代码方式:source.CancelAfter(5000);

static void Main(string[] args)
        {
    
    
            CancellationTokenSource source = new CancellationTokenSource();
            //注册任务取消的事件
            source.Token.Register(() =>
            {
    
    
                Console.WriteLine("任务被取消后执行xx操作!");
            });

            int index = 0;
            //开启一个task执行任务
            Task task1 = new Task(() =>
              {
    
    
                  while (!source.IsCancellationRequested)
                  {
    
    
                      Thread.Sleep(1000);
                      Console.WriteLine($"第{
      
      ++index}次执行,线程运行中...");
                  }
              });
            task1.Start();
            //延时取消,效果等同于Thread.Sleep(5000);source.Cancel();
            source.CancelAfter(5000);
            Console.ReadKey();
        }

5.3 任务取消触发回调函数

如前所述在Task的API函数中,当线程执行完毕后,可以调用Continuewith等做某一线程执行完成的后续处理工作,任务取消也可触发此类回调函数。对应方法:source.Token.Register(Action action)注册取消任务触发的回调函数,即当某一任务被取消时,通过Register执行任务取消的后续操作。代码例子如5.2延时取消。

5.4 多个任务共同取消

多个任务共同取消,在有多个CancellationTokenSource需要一起并行管理的时候,比如任意一个任务取消 则取消所有任务。我们不必去一个一个的去关闭,只需要将需要一起并行关闭的CancellationTokenSource组合起来就行了
代码如下:

class Program
    {
    
    
        //声明CancellationTokenSource对象
        static CancellationTokenSource c1 = new CancellationTokenSource();
        static CancellationTokenSource c2 = new CancellationTokenSource();
        static CancellationTokenSource c3 = new CancellationTokenSource();

        //使用多个CancellationTokenSource进行复合管理
        static CancellationTokenSource compositeCancel = CancellationTokenSource.CreateLinkedTokenSource(c1.Token, c2.Token, c3.Token);

        //程序入口
        static void Main(string[] args)
        {
    
    
            Task.Factory.StartNew(MyTask, compositeCancel.Token);

            Console.WriteLine("请按回车键(Enter)停止");
            Console.ReadLine();

            //任意一个 CancellationTokenSource 取消任务,那么所有任务都会被取消。
            c1.Cancel();

            Console.WriteLine("已停止");
            Console.ReadLine();
        }

        //测试方法
        static void MyTask()
        {
    
    
            //判断是否取消任务
            while (!compositeCancel.IsCancellationRequested)
            {
    
    
                Console.WriteLine(DateTime.Now);
                Thread.Sleep(1000);
            }
        }
    }

6.线程同步、异步

6.1 线程同步

常见例子:我们吃饭用手机点菜的时候,多个人同时点菜,在最后结账的时候,如果大家都争着买单,那如果没有同步信息,就会造成多个人都买单成功。这就是线程同步的问题之一。

所谓的同步,即按照代码的顺序执行,也就是用同一个线程来执行所有的操作,或者多个线程按顺序执行,例如***4.1 实例方法.wait()***中的部分,多个线程按顺序执行。
有序性:主要针对程序的执行顺序来说.比如单线程编程中,A();B(); ,必须等待A方法执行完了,B方法才可以执行.再比如,lock(sync){A();},无论多少个线程调用这段代码,A方法在同一个时刻只允许一个线程调用,其它线程必须等待
一致性:主要针对数据来说.我们必须确保对临界区数据的变更不会影响其它线程.比如说,A线程和B线程在某一段时间都对data进行修改,线程之间共享变量可能会造成死锁的现象,为避免出现两个线程同时修改,后者的修改将前者的修改覆盖掉,我们对data的修改进行加锁,这样data一次只会允许一个线程进行修改.也就保证了数据的一致性.

线程安全:多线程执行结果与单线程一致,线程安全
线程不安全:多线程同时修改一个变量;或者一个线程修改,一个线程读取,则可能出现BUG

在多线程问题的处理上,我们是在异步中谋求同步的目的,以确保程序的安全

线程同步的方法常见的主要有锁 SpinLock 、Mutex、Monitor、lock。以下仅介绍最常用的Monitor、lock方法

6.1.1 lock方法

1、lock锁定的是一个引用类型,值类型不能被lock
2、避免lock一个string,因为程序中任何跟锁定的string的字符串是一样的都会被锁定。
模拟售票系统代码如下:

 internal class Program
    {
    
    
        int num = 10;
        void ticket()
        {
    
    
            while (true)        //无线循环
            {
    
    
                lock (this)     //锁定代码快,以便线程同步
                {
    
    
                    if (num > 0)
                    {
    
    
                        Thread.Sleep(1000);
                        Console.WriteLine(Thread.CurrentThread.Name + "------票数" + num--);
                    }
                }
            }
        }

        static void Main(string[] args)
        {
    
    
            Program p=new Program();
            Thread ta = new Thread(p.ticket);
            ta.Name = "线程一";
            Thread tb = new Thread(p.ticket);
            tb.Name = "线程二";
            Thread tc = new Thread(p.ticket);
            tc.Name = "线程三";
            Thread td = new Thread(p.ticket);
            td.Name = "线程四";
            ta.Start();
            tb.Start();
            tc.Start();
            td.Start();
            Console.ReadLine();

        }
    }

执行结果如下图所示:
在这里插入图片描述

说明:4个线程调用的是同一个函数,同一时间,一次只允许一个线程进入,每次执行将票数减一,线程执行的顺序尽管是无序的,但是执行的“票数”都是在上一次的结果上减1。
倘若注释同步代码,即//lock (this) //锁定代码快,以便线程同步,执行结果如下图所示,同一时间,多个线程进入同一函数,线程顺序无序,执行结果也无序,“票数3”同时被“线程四”和“线程一”执行。
现实情况中,当不考虑退票时,票数应该是越卖越少,且不会出现多人购买同一张票都成功的现象。如果不使用线程同步的写法,得不到想要的功能。
在这里插入图片描述

6.1.2 monitor类

Monitor类提供了与lock类似的功能,不过与lock不同的是,它能更好的控制同步块,当调用了Monitor的Enter(Object o)方法时,会获取o的独占权,直到调用Exit(Object o)方法时,才会释放对o的独占权
可以使用 TryEnter() 方法可以给它传送一个超时值,决定等待获得对象锁的最长时间,该方法能在指定的毫秒数内结束线程,这样能避免线程之间的死锁现象。

bool lockTaken=false;
            Monitor.TryEnter(obj, 500, ref lockTaken);
            if(lockTaken){
    
    
                try
                {
    
    
                    //Synchronized part
                }
                finally
                {
    
    
                    Monitor.Exit(obj);
                }
            }else{
    
    
                //don't aquire the lock, excute other parts
            }

6.2 线程异步

异步与同步概念:当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法;当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕,我们称这个方法为异步方法
异步方法的优点:异步线程最大的好处在于非阻塞,即各线程之间执行任务时,互不干扰,当任务完成之后就可立即响应,不需等待其他任务是否执行完成。异步编程中最好用的便是 async和await 。
在C#5.0中出现的 async和await ,让异步编程变得更简单,用同步的写法写异步同步代码的逻辑结构

线程异步与多任务执行关系:异步是同时执行多个任务,而Task则是允许多个任务可以在线程内同时进行,多任务的同时进行,则是由异步来进行,而异步方法,必须为Task

6.2.1 控制台应用async和await

不加await修饰代码如下,即为多线程常规流程执行方式

class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Console.WriteLine("开始");
            WriteAsync();
            Console.WriteLine("结束");
            Console.ReadKey();
        }

        static void WriteAsync()
        {
    
    
            Task.Run(() =>
            {
    
    
                for (int i = 0; i < 10; i++)
                {
    
    
                    Console.WriteLine("T " + i);
                }
            });
            Task.Run(() =>
            {
    
    
                for (int i = 0; i < 10; i++)
                {
    
    
                    Console.WriteLine("S " + i);
                }
            });
        }

执行结果如下:
在这里插入图片描述
或者在这里插入图片描述
如上的执行结果,未阻塞主线程,两次执行结果,S在上或者T在上,这是任务内部无序执行的结果,具体情况与***## 4.Task常用的API***常规流程方式一致

加await修饰代码如下:

 class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Console.WriteLine("开始");
            WriteAsync();
            Console.WriteLine("结束");
            Console.ReadKey();
        }

        static async void WriteAsync()
        {
    
    
            await Task.Run(() =>
            {
    
    
                for (int i = 0; i < 10; i++)
                {
    
    
                    Console.WriteLine("T " + i);
                }
            });
            await Task.Run(() =>
            {
    
    
                for (int i = 0; i < 10; i++)
                {
    
    
                    Console.WriteLine("S " + i);
                }
            });

        }
    }

执行结果如下图所示:
在这里插入图片描述
如上图所示,未阻塞主线程执行,而在task内部,函数执行变得有序起来,不论执行多少次,T永远在S上面,通过异步之间的await进程等待,避免不必要的资源占用和浪费。有点类似于上述API函数中介绍的方法,但是API函数中的方法会阻塞主线程。

6.2.2 Winfrom界面async和await

例子实现功能:当点击button1启动按钮时,运行一个任务,任务结束时要报告是否成功,如果成功button2就显示绿色图标、如果失败就显示红色图标,1秒后图标颜色恢复为白色;任务运行期间启动按钮要不可用,
界面布局如图所示:
在这里插入图片描述

常规代码实现方式:

public partial class Form1 : Form
    {
    
    
        private void btnStart_Click(object sender, EventArgs e)
        {
    
    
            this.btnStart.Enabled = false;

            if(DoSomething())
            {
    
    
                this.picShow.BackColor = Color.Green;
            }
            else
            {
    
    
                this.picShow.BackColor = Color.Red;
            }

            Thread.Sleep(1000);
           
            this.picShow.BackColor = Color.White;
            this.btnStart.Enabled = true;
        }

        private bool DoSomething()
        {
    
    
            Thread.Sleep(5000);
            return true;
        }
    }

运行情况:启动程序,此时程序界面是可以拖动的,点击按钮1,程序界面被阻塞,无法拖动,此时开始执行DoSomething方法,返回结果为true,进入判断执行this.picShow.BackColor = Color.Green,立即执行this.picShow.BackColor = Color.White;,此时无法看到界面按钮2变为绿色,想要功能无法实现。
问题分析:
1、运行期间UI线程阻塞了,用户界面没有响应;
2、根本不能实现需求,点击启动后,程序卡死6秒种,也没有看到颜色变化,因为UI线程已经阻塞,当重新获得句柄时图标已经是白色了。
以下采用多任务实现具体需求,代码如下:

public partial class Form1 : Form
    {
    
    
        public Form1()
        {
    
    
            InitializeComponent();
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
    
    
            this.btnStart.Enabled = false;

            Task.Run(() => 
            {
    
      
                if (DoSomething())
                {
    
    
                    this.Invoke(new Action(() =>
                    {
    
    
                        this.picShow.BackColor = Color.Green;
                    }));                   
                }
                else
                {
    
    
                    this.Invoke(new Action(() =>
                    {
    
    
                        this.picShow.BackColor = Color.Red;
                    }));                   
                }

                Thread.Sleep(1000);

                this.Invoke(new Action(() =>
                {
    
    
                    this.btnStart.Enabled = true;
                    this.picShow.BackColor = Color.White;
                }));               
            });           
        }

        private bool DoSomething()
        {
    
    
            Thread.Sleep(5000);
            return true;
        }
    }

代码特点:将颜色变化功能统一放置task中执行,同一个任务中,代码按顺序执行,执行顺序this.picShow.BackColor = Color.Green;Thread.Sleep(1000);this.picShow.BackColor = Color.White;因此可看到颜色变化。
功能基本可以实现,但存在以下问题:
1、主线程的btnStart_Click方法除了启动一个任务以外,啥事也没干,线程是在后台执行的。
2、由于非UI线程不能访问UI控件,代码里有很多Invoke,比较丑陋;
3、界面逻辑和业务逻辑掺和在一起,使得代码难以理解。

以下采用async和await 异步编程的方式解决上述问题,代码如下:

public partial class Form1 : Form
    {
    
    
        public Form1()
        {
    
    
            InitializeComponent();
        }

        private async void btnStart_ClickAsync(object sender, EventArgs e)
        {
    
    
            this.btnStart.Enabled = false;

            var result = await DoSomethingAsync();
            if(result)
            {
    
    
                this.picShow.BackColor = Color.Green;
            }
            else
            {
    
    
                this.picShow.BackColor = Color.Red;
            }

            await Task.Delay(1000);
            
            this.picShow.BackColor = Color.White;
            this.btnStart.Enabled = true;
        }

        private async Task<bool> DoSomethingAsync()
        {
    
    
            await Task.Run(() =>
            {
    
    
                Thread.Sleep(5000);                
            });
            return true;
        }
    }

代码特点:DoSomethingAsync函数带有返回值,此函数中添加async和await ,代码执行顺序DoSomethingAsyncthis.picShow.BackColor = Color.Greenawait Task.Delay(1000)this.picShow.BackColor = Color.White,和上述功能正常程序一致,但是添加async和await,让程序按照一定顺序执行,即执行完DoSomethingAsync之后,才会执行后续任务。思维模式:以编写同步代码的方式编写异步
为加深理解,将上述代码去掉async和await,改写成如下形式:

public partial class Form1 : Form
    {
    
    
        bool a = false;
        public Form1()
        {
    
    
            InitializeComponent();
        }

        private  void  btnStart_ClickAsync(object sender, EventArgs e)
        {
    
    
            this.btnStart.Enabled = false;
            this.btnStart.Visible = false;

             DoSomethingAsync();
            if (a)
            {
    
    
                this.picShow.BackColor = Color.Green;
            }
            else
            {
    
    
                this.picShow.BackColor = Color.Red;
            }

             Task.Delay(1000);

            this.picShow.BackColor = Color.White;
            this.btnStart.Enabled = true;
            this.btnStart.Visible = true;
        }

        private  void  DoSomethingAsync()
        {
    
    
             Task.Run(() =>
            {
    
    
                Thread.Sleep(5000);
                a = true;
            });            
        }
    }

执行结果:按钮1一直处于可见状态,DoSomethingAsync函数相当于没有执行,按钮2变为红色的瞬间变为白色,是看不到颜色变化的,Task.Delay是在后台运行,整个程序相当于什么也没有执行。
这里如果还不是很清除加与不加async和await的区别,请参考添加链接描述

6.3 异步并发

并行:两队人,进入一个门,每队每次轮流进一个,这称之为并发
并发:两队人,分别进入两个门,各进各的,这称之为并行.

7.线程调试方法

线程是系统后台分配内存空间执行相应任务,当线程创建的越多,后台执行越容易出现莫名其妙的BUG,达不到想要的功能。例如后台执行100个线程,传入共同参数X,所有线程执行完,得到结果Y,而实际想要得到的结果为Z,100个线程在后台执行的先后是无序的,即线程执行会跳来跳去
常用的调试方法打断点是行不通的,因为断点的焦点会在线程之间“反复横跳”,根本无法集中跟踪某一个线程的操作链路,打断点也不是按顺序执行,不可能100个线程每个入口都打上断点,如何确认是在哪一个线程或多个线程执行环节出现问题?除了上述***## 4.task中常用API***函数外,以下简介其他方式调试多线程。
添加链接描述

7.1 多线程界面调试方法

1.以***## 4.Task常用的API***常规流程为例,断点处右击选择条件,如下图所示,条件文本框中可编写调试条件。
在这里插入图片描述
2.debug模式下启动程序,点击“调试-窗体-线程”,如下图所示。
在这里插入图片描述
3.运行后,执行到断点处,如下图所示,进程处会显示当前线程ID。
在这里插入图片描述

7.2 多线程界面调试实操

如上所述,多线程运行时,线程之间无序,常规断点方式也无法进行,最主要的原因在于无法锁定某一个线程,因此我们需要想办法锁定某一个线程,让这个线程一直运行下去,查看锁定的线程是否有问题
简单案例代码如下,for (int i = 0; i < 100; i++)处打断点,

internal class Program
    {
    
    
        static void Main(string[] args)
        {
    
    
            Task[] task = new Task[6];
            for (int i = 0; i < task.Length; i++)
            {
    
    
                task[i] = Task.Run(Do);
            }
            Task.WaitAll(task);
            Console.WriteLine();
            Console.ReadLine();
        }
        private static void Do()
        {
    
    
            int x = Thread.CurrentThread.ManagedThreadId;
            //循环调试
            for (int i = 0; i < 100; i++)
            {
    
    
                Console.WriteLine(i);
            }
        }
    }

具体操作见如下视频:
添加链接描述

8.多线程异常处理

8.1 单线程异常与多线程异常比对

单线程处理:try-catch,catch中进行异常处理,捕捉异常----处理异常

			try
            {
    
    
                {
    
    
                    //捕捉异常
                }
            }
            catch
            {
    
    
                {
    
    
                    throw;
                    //处理异常
                }
            }

多线程处理:无法直接用try-catch包裹

8.2 单线程与多线程案列

界面布局如下图:
在这里插入图片描述
单线程异常处理代码:

private void button1_Click(object sender, EventArgs e)
        {
    
    
            try
            {
    
    
                for (int i = 0; i < 20; i++) 
                {
    
    
                    string k = $"button2_Click{
      
      i}";

                    if (k.Equals("button2_Click8"))
                    {
    
    
                        throw new Exception("k==button2_Click8异常");
                    }
                    else if (k.Equals("button2_Click10"))
                    {
    
    
                        throw new Exception("k==button2_Click10异常");
                    }
                    else if (k.Equals("button2_Click15"))
                    {
    
    
                        throw new Exception("k==button2_Click15异常");
                    }
                }
            }
            catch 
            {
    
    
                throw;
            }
        }

将程序断点打在catch 处,运行时,会进入点内的内容,如下图所示
在这里插入图片描述
多线程异常处理先仿照单线程方式,代码如下:

private void button2_Click(object sender, EventArgs e)
        {
    
    
            try
            {
    
    
                for (int i = 0; i < 20; i++)
                {
    
    
                    string k = $"button2_Click{
      
      i}";
                    Task.Run(() =>
                    {
    
    
                        if (k.Equals("button2_Click2"))
                        {
    
    
                            throw new Exception("k==button2_Click8异常");
                        }
                        else if (k.Equals("button2_Click10"))
                        {
    
    
                            throw new Exception("k==button2_Click10异常");
                        }
                        else if (k.Equals("button2_Click15"))
                        {
    
    
                            throw new Exception("k==button2_Click15异常");
                        }
                    });
                }
            }
            catch
            {
    
    
                throw;
            }
        }

断点分别打在3个异常处及catch处,运行程序如下图所示
在这里插入图片描述
断点可以进入异常处,无法进入catch处,即程序确实有异常发生,被捕捉到了,但是无法处理异常,异常被吞掉了
综上所述,常规单线程处理异常的方式无法在多线程使用,即无法直接用try-catch包裹代码,捕获异常、处理异常

8.3 多线程异常捕捉与处理

那么多线程内部发生的异常如何捕捉?需实现以下2大条件
1.需要做线程的等待,例如task.waitall,即阻塞主线程
2.try-catch包裹
将上述多线程的代码改写成如下形式:

private void button2_Click(object sender, EventArgs e)
        {
    
    
            try
            {
    
    
                List<Task> list1 = new List<Task>();
                for (int i = 0; i < 20; i++)
                {
    
    
                    string k = $"button2_Click{
      
      i}";
                    //线程列表
                    list1.Add(Task.Run(() =>
                    {
    
    
                        if (k.Equals("button2_Click2"))
                        {
    
    
                            throw new Exception("k==button2_Click8异常");
                        }
                        else if (k.Equals("button2_Click10"))
                        {
    
    
                            throw new Exception("k==button2_Click10异常");
                        }
                        else if (k.Equals("button2_Click15"))
                        {
    
    
                            throw new Exception("k==button2_Click15异常");
                        }
                    }));
                }
                Task.WaitAll(list1.ToArray());		//等待线程完成
            }
            catch(Exception ex)
            {
    
    
                throw;
            }
        }

主要增加代码内容为:将所有线程放在线程列表中,添加Task.WaitAll(list1.ToArray());阻塞线程
断点分别打在3个异常处及catch处,异常可进去,也可以捕获。
捕获异常详细情况如下图所示:
在这里插入图片描述
捕获的3个异常恰为发生异常,点击“快速监视”,查看“ex”的类型,如下图所示:
在这里插入图片描述

8.4 多线程中多个异常捕获

上述例子中多线程出现3个异常,只用了一个“try-catch”结构,集中显示异常情况,对于异常查看并不方便,解决上述问题,采用如下方式。
1.一个try可以对应多个catch
2.AggregateException捕获异常,多线程特有异常捕获,AggregateException继承至Exception,Exception子类,AggregateException内部包含多线程集合

上述多线程例子中try-catch部分换成如下情况,
在这里插入图片描述
会弹出3此提示框,告知出现的异常情况,3个异常不仅能够捕获,也可显示。

8.总结

C#多线程最常用的方式就是Task,本文主要针对task做了详细的描述,较全面的总结了多线程使用的方式,文中资料与代码实例参考引用文献较多。文章目的不为赚米,只为学习C#中多线程做笔记,加深对线程理解与使用。

猜你喜欢

转载自blog.csdn.net/m0_48667560/article/details/133364455