C#多线程之Thread,ThreadPool,Task,Parallel

news2024/10/7 18:32:27

总目录

文章目录

  • 总目录
  • 前言
  • 一、多线程以及与之相关概念
    • 1.基本概念
      • 1)进程
      • 2)线程
      • 3)多线程
    • 2.同步、异步
      • 1)同步方法
      • 2)异步方法
  • 二、Thread
    • 1.线程的使用
      • 1)创建并开启线程
      • 2)线程的属性设置&方法调用
    • 2.WinForm中跨线程操作主线程UI
      • 1)案例
      • 2)Control.Invoke和Control.BeginInvoke
    • 3.其他知识点(应用场景不多,可略过)
      • 1) Thread的扩展使用
      • 2)数据槽,内存栅栏
  • 三、ThreadPool
    • 1.使用须知
    • 2.创建并开启一个线程
    • 3.ThreadPool的属性和方法
    • 4.实例(比较Thread 和ThreadPool)
    • 5 System.Threading.Timer
  • 四、Task【重点】
    • 1.Task开启线程的方式
    • 2.Task在WinForm中跨线程操作UI
    • 3.Task线程等待
    • 4.线程返回值
    • 5.TaskCreationOptions&TaskContinuationOptions枚举类
    • 6.延迟执行
    • 7.多线程异常捕获
    • 8.取消线程
      • 1)使用变量的方式,取消线程
      • 2)使用CancellationTokenSource取消线程
      • 3)source.Token.Register
      • 4)CreateLinkedTokenSource组合取消
      • 5)监控取消
    • 9.解决中间变量问题
  • 五、Parallel
    • 1.Parallel介绍
    • 2.Parallel.Invoke()
    • 3.Parallel.For()
    • 4.Parallel.ForEach()
  • 总结


前言

在日常开发中,多线程是避不开的痛,用吧,总容易出问题,不用吧,程序性能又会及其慢,如何做才能避开线程的坑,更好的使用多线程呢?本文将会全面的介绍C#多线程的知识,让你对多线程有更深的认识。


一、多线程以及与之相关概念

1.基本概念

1)进程

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

简单理解:当一个程序运行的时候,他就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。

2)线程

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel
thread),而把用户线程(user thread)称为线程。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。
但同一进程中的多个线程有各自的调用栈(call stack)、寄存器环境(register context)、线程本地存储(thread-local storage)。
通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。

简单理解:一个进程包含多个线程,线程是进程中一个单一顺序的控制流。

3)多线程

(1)概念

是指从软件或者硬件上实现多个线程并发执行的技术。

(2)原理

实现多线程是采用一种并发执行机制,其原理:简单地说就是把一个处理器划分为若干个短的时间片,每个时间片依次轮流
执行处理各个应用程序,由于一个时间片很短,相对于一个应用程序来说,就好像是处理器在为自己单独服务一样,从而达到多个应用程序在同时进行的效果。

无论是过去还是现在,世界上大多数计算机仍然采用的是冯·诺依曼结构,这种结构的特点就是顺序处理,一个处理器在同个时刻只能处理一件事情。Windows95/NT采用一种全新的任务调度策略,它把一个进程划分为多个线程,每个线程轮流占用CPU的运算时间,操作系统不断地把线程挂起、唤醒、再挂起、再唤程,如此反复,由于现在CPU的速度比较快,给人的感觉是多个线程在同时执行,就好像有多个CPU存在于计算机中一样。

即是说,如果把CPU比作一个员工的话,他自始至终就是同一时刻只能处理一件事情,而为了实现同时处理多个任务的时候,他将按照一定的调度干一会儿任务A,然后干一会儿任务B,在干一会儿任务C,由于他干事情的速度很快,给人的感觉就是多个任务在同时进行。

(3)优点(资源换性能)
1、提高CPU的利用率
2、提高程序运行效率
3、可以加快应用程序的响应,对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作置于一个新的线程,就可以避免这种尴尬的情况

(4)缺点
1、如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换
2、更多的线程需要更多的内存空间
3、线程虽好,但是控制不好,将会导致很多的Bug,因此要小心使用
4、线程的中止需要考虑其对程序运行的影响
5、多个线程是共享进程资源的,因此需要解决竞用共享资源的问题

如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到竞用共享资源的问题。
举个例子来说,两个线程都需要将信息同时发送给一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源

(5)多线程适用场景
1、当主线程执行耗时的任务时,会导致用户界面卡死,导致用户体验差,这种可另开一个线程处理耗时任务,保证主线程正常运行
2、在一些等待的任务实现上如用户输入、文件读写和网络收发数据(请求接口)等
3、利用多线程拆分复杂运算,提高计算速度

(6)多线程不适用场景
单线程就能很好解决的时候,就不要为了适用多线程而使用多线程

(7)故事案例理解
1、如果将工厂比作系统,生产线比作进程,那么员工就是线程,
2、 假定这家特殊的工厂营收是有上限(就像CPU等硬件设备的处理上限)
3 、生产线上有不同的员工做着不同的工序(这就像多线程同步完成多个任务一样)
4 、同一个工序(比如用扳手拧螺丝),可能多个员工完成(表示多个线程也可能执行同一个任务)
5 、现在 生产线上拧螺丝的扳手只有一把(平常这个工序不太忙),但是有2-3员工要用,那么需要用的人,就必须等到上一个人使用完才行(表示线程需要处理一些共享资源竞用的问题)
6 、后面生产线接了个大单,生产线给每个工序都增加了员工并且开始加班,刚开始少量增加还可以,越到后面员工越多,给工厂加大了管理的成本以及工资开销,由于营收有上限,导致不久后工厂就撑不住了,那么只有裁员或者减少每个员工的工资,导致每个员工干活的积极性也降低了,加上不断加班,工作效率也降低了。(表示多线程过多就会占用过多的系统资源,导致程序性能降低)

2.同步、异步

1)同步方法

线性执行,从上往下依次执行,同步方法执行慢,消耗的计算机资源少。
案例:同步方法

        public static void SyncMethod(string str)
        {
            Console.WriteLine($"--------同步方法{str}--------");
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(200);
                Console.WriteLine($"同步:{i}");
            }
        }

//Main中调用
	   SyncMethod("一");
       SyncMethod("二");
       Console.WriteLine("--------主线程--------");
       Console.ReadLine();

结果输出:按照调用顺序和业务顺序依次输出
在这里插入图片描述

2)异步方法

线程和线程之间,不再线型执行,多个线程总的耗时少,执行快,消耗的计算机资源多,各线程执行是无序的。

案例:(这里主要理解同步异步,先不纠结线程的使用,后面就会讲到)

        //异步方法
        public static void AsyncMethod(string str)
        {
            Console.WriteLine($"--------异步方法{str}--------");
            Task.Run(()=> {
                for (int i = 0; i < 10; i++)
                {
                    Thread.Sleep(200);
                    Console.WriteLine($"异步{str}{i}");
                }
            });
        }
        //Main中调用
	   SyncMethod("一");
       SyncMethod("二");
       Console.WriteLine("--------主线程--------");
       Console.ReadLine();

输出结果:并没有按照调用顺序输出结果,而是异步交错的输出结果
在这里插入图片描述
以上案例是为了加深对同步和异步方法的理解

二、Thread

Thread是.Net最早的多线程处理方式,它出现在.Net1.0时代,虽然现在已逐渐被微软所抛弃,微软强烈推荐使用Task,但从多线程完整性的角度上来说,我们有必要了解下早期多线程的是怎么处理的,以便体会.Net体系中多线程处理方式的进化。

关于线程调试工具WinDbg的使用,感兴趣的可参考:官方文档,WinDbg介绍

1.线程的使用

1)创建并开启线程

            //【1】创建一个线程
            Thread thread= new Thread(new ThreadStart(()=> 
            {
                Console.WriteLine("创建一个线程!");
            }));
            //【2】开启线程
            thread.Start();
             //【简写】
            new Thread(()=> { Console.WriteLine("创建一个线程!"); }).Start();

由上可知创建需要使用Thread,然后调用Start()方法启动

           public delegate void ThreadStart();

另外ThreadStart的本质是委托,因此可以直接使用lambda表达式替换,因此就有了简写的方式

2)线程的属性设置&方法调用

(1)设置前台后台线程
前台线程:界面关闭,线程会等待执行完才结束
后台线程:界面关闭,线程也就随之消失

            Thread thread = new Thread(() => { Console.WriteLine("创建一个线程!"); });
            thread.IsBackground = true;

(2)调用方法,设置线程的停止等待

            Thread thread = new Thread(() => { Console.WriteLine("创建一个线程!"); });
            thread.Start();

            #挂起/暂停一个线程,已弃用
            thread.Suspend();
            #恢复挂机的线程,已弃用
            thread.Resume();
            #停止线程,通过抛异常的方式停止
            thread.Abort();
            #中断线程
            thread.Interrupt();
			#重启 停止的线程
            Thread.ResetAbort();

            if (thread.ThreadState!=ThreadState.Stopped)
            {
                #休息100ms,【此时间内不消耗计算机资源】
                Thread.Sleep(100);
            }
			#让主线程等待,直到当前thread线程执行完毕
            thread.Join();
            //让主线程等100ms,100ms后不管当前线程是否执行完毕,都继续执行后续的操作
            thread.Join(100);

(3)设置线程的优先级

            Thread thread1 = new Thread(() => { Console.WriteLine("创建一个线程1!"); });
            Thread thread2 = new Thread(() => { Console.WriteLine("创建一个线程2!"); });

            thread1.Priority = ThreadPriority.Highest;
            thread2.Priority = ThreadPriority.Lowest;

            thread1.Start();
            thread2.Start();

通过 ThreadPriority设置线程的优先级,设置好后,系统会根据设置的优先级来执行

线程执行完就会变为dead状态 不会占用系统资源

2.WinForm中跨线程操作主线程UI

1)案例

实现:时间一个按钮,让一个Label像时钟一样展示时间
在这里插入图片描述
(1)子线程中访问UI线程并修改UI
在这里插入图片描述
(2)使用控件的Invoke方法跨线程操作

        private void button1_Click(object sender, EventArgs e)
        {
            Thread thread = new Thread(() => {
                while (true)
                {                   
                    this.lb_time.Invoke(new Action(()=> 
                    {
                        this.lb_time.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                    }));
                    Thread.Sleep(800);
                }
            });
            thread.IsBackground = true;
            thread.Start();
        }

通过以上代码即可实现跨线程访问UI而不会卡界面

2)Control.Invoke和Control.BeginInvoke

Control.Invoke (Delegate method) :在拥有此控件的基础窗口句柄的线程上执行指定的委托。
Control.BeginInvoke(Delegate method) :在创建控件的基础句柄所在线程上异步执行指定委托。

(1) 为啥上面案例中使用Invoke后不报错?
Control的Invoke和BeginInvoke都执行在主线程即UI线程上
上面的案例之所以可以做到不报错,因为子线程中通过使用控件的Invoke方法将UI上的改动委托给UI主线程自己去操作,就不涉及到跨线程的问题了。

(2)Control.Invoke和Control.BeginInvoke的区别
在这里插入图片描述
通过上图更够更深刻的明白,Invoke就是在UI线程上同步执行一段代码,
BeginInvoke就是UI线程上异步执行一段代码

(3)Invoke(Delegate method) 解析
有Invoke中的Delegate可知需要传入一个委托类型,由于Delegate 本身是abstract 禁止实例化的,因此只能传入其派生类,那么所有的委托类型都可以传入,但是不能直接传入一个lambda表达式,虽然任何 Lambda 表达式都可以转换为委托类型。
如:

        private void Form2_Load(object sender, EventArgs e)
        {
            this.lb_time.Text = "1";
            //直接使用lambda会报错
            //this.lb_time.BeginInvoke(()=> { this.lb_time.Text = "2"; });

            //可以使用,Action派生自Delegate
            this.lb_time.BeginInvoke(new Action(()=> { this.lb_time.Text += "2"; }));
            //可以使用,ThreadStart派生自Delegate
            this.lb_time.BeginInvoke(new ThreadStart(()=> { this.lb_time.Text += "3"; }));
            //可以使用,CustomDelegate和所有自定义delegate都派生自Delegate
            this.lb_time.BeginInvoke(new CustomDelegate(()=> { this.lb_time.Text += "4"; }));
        }

        public delegate void CustomDelegate();
        //这些委托的本质是类,但是lambda表达式的本质是匿名方法,
        //虽有语法糖可将匿名方法直接赋予一个委托,但委托类型和lambda表达式两者本质上是不同的

3.其他知识点(应用场景不多,可略过)

1) Thread的扩展使用

(1) 实现多个委托在中多线程按照顺序执行

        static void Main(string[] args)
        {
            Action action = new Action(()=> { Console.WriteLine("执行方法1"); });
            Func<string> func = () => 
            {
                Console.WriteLine("执行方法2");
                return "我是方法2返回的msg";
            };
            CallBackThread(action,func);

            Console.ReadLine();
        }

        public static void CallBackThread(Action action,Func<string> func)
        {
            Thread thread = new Thread(()=> {
                action.Invoke();
                string msg= func.Invoke();
                Console.WriteLine($"输出消息:{msg}");
            });
            thread.Start();
        }

结果:按照线程中的调用顺序去执行
在这里插入图片描述
(2) 实现获取多线程委托的结果
获取子线程中的值并且返回,需要用到Join,主线程会等待,不推荐这种做法,还是抱着学习的态度了解一下
(1)初始实现

        public static long GetUserID()
        {
            long result = 0;
            Thread thread = new Thread(()=> {
                result = DoSomething();//假定这里是访问接口获取用户Id是个很耗时的操作
            });
            thread.Start();
            thread.Join();//这里回卡住UI,因为主线程在等待
            return result;
        }
        //模拟耗时操作
        public static long DoSomething()
        {
            long iResult = 0;
            for (int i = 0; i < 1_000_000_000; i++)
            {
                iResult += i;
            }
            return iResult;
        }

(2)包装实现

        //获取子线程中值,并返回
        public static Func<T> CallBackFunc<T>(Func<T> func)
        {
            T t = default(T);
            Thread thread = new Thread(()=> { t = func.Invoke(); });
            thread.Start();

            return new Func<T>(() =>
            {
                thread.Join();//等待thread执行完成;
                return t;
            });
        }

传入一个有返回值的并且耗时的委托,然后返回一个有返回值的委托

        static void Main(string[] args)
        {
            Func<long> func = () =>
            {
                return DoSomething();
            };
            //这一步不会阻塞界面
            Func<long> func1 = CallBackFunc<long>(func);
            Console.WriteLine("子线程准备开启");
            long iResult = func1.Invoke();
            Console.WriteLine($"子线程完成:{iResult}");

            Console.ReadLine();
        }

2)数据槽,内存栅栏

这块儿内容的应用场景更少,感兴趣的可以,推荐查看数据槽,内存栅栏

三、ThreadPool

线程池是用来保存线程的一个容器,在程序创建线程来执行任务的时候线程池才会初始化一个线程,线程在执行完毕之后并不会被销毁,而是被挂起等待下一个任务的到来被激活执行任务,当线程池里的线程不够用的时候会新实例化一个线程,来执行,线程池里的线程会被反复利用。

1.使用须知

1、线程池中的所有线程都是后台线程。如果进程的所有前台线程都结束了,所有的后台线程 就会停止。不能把入池的线程改为前台线程。
2、不能给入池的线程设置优先级或名称。
3、入池的线程只能用于时间较短的任务。如果线程要一直运行(如Word的拼写检查器线程), 就应使用Thread类创建一个线程.
4、线程池里的线程会被反复利用,不需要程序员对线程的数量管控,提高性能,防止滥用

2.创建并开启一个线程

        static void Main(string[] args)
        {
            ThreadPoolTest();
            Console.WriteLine("主线程");
            Console.ReadLine();
        }
        public static void ThreadPoolTest()
        {
            //ThreadPool使用QueueUserWorkItem开启一个线程
            //QueueUserWorkItem方法,将方法排入队列以便执行。 此方法在 有线程池线程 变得可用时执行。
            //QueueUserWorkItem(WaitCallback callBack)
            //public delegate void WaitCallback(object state);
            //由此可知WaitCallback是一个有object类型参数且无返回值的委托。
            //QueueUserWorkItem(WaitCallback callBack, object state)
            //state即WaitCallback中需要的参数, 不推荐这么使用,存在拆箱装箱的转换问题,影响性能。
            //即state的值会传入CallBack这个委托中,供使用

            ThreadPool.QueueUserWorkItem((obj) => { Console.WriteLine("张三"); });

            ThreadPool.QueueUserWorkItem((obj) => { Console.WriteLine(obj); }, "我是鲤籽鲲");
        }

3.ThreadPool的属性和方法

int maxWorkerThreads, minWorkerThreads, maxCompletionPortThreads, minCompletionPortThreads;
//获取线程中的工作线程数的最大值,和线程池中异步I/O线程的数目
ThreadPool.GetMaxThreads(out maxWorkerThreads,out maxCompletionPortThreads);
//获取线程中的工作线程数的最小值,和线程池中异步I/O线程的数目
ThreadPool.GetMinThreads(out minWorkerThreads, out minCompletionPortThreads);

//设置当前进程的最大/最大 工作线程数和IO线程数,不推荐使用,配置不当,影响性能
ThreadPool.SetMaxThreads(15,12);
ThreadPool.SetMinThreads(5,5);

Console.WriteLine($"当前进程--最大工作线程数:{maxWorkerThreads}最大的IO线程数:{maxCompletionPortThreads}");
Console.WriteLine($"当前进程--最小工作线程数:{minWorkerThreads}最小的IO线程数:{minCompletionPortThreads}");

4.实例(比较Thread 和ThreadPool)

        public static void ThreadPoolTest()
        {
            for (int i = 1; i <=10; i++)
            {
                ThreadPool.QueueUserWorkItem((obj)=> 
                {
                    Console.WriteLine($"ThreadPool子线程Id:{Thread.CurrentThread.ManagedThreadId}");
                    DoSomething();
                });
                Thread.Sleep(100);
               
            }
        }

        public static void ThreadTest()
        {
            for (int i = 1; i <= 10; i++)
            {
                new Thread(()=> 
                {
                    Console.WriteLine($"Thread子线程Id:{Thread.CurrentThread.ManagedThreadId}");
                    DoSomething();
                }).Start();
                Thread.Sleep(100);
            }
        }

Main调用输出结果:
在这里插入图片描述
通过输出结果的ThreadId可知,Thread会按照你的业务安排需要多少线程开启多少线程,而ThreadPool,在开启一定数量的线程后,如果发现现有的线程数量足以应对,那么就不会新开启多余的线程。

5 System.Threading.Timer

是一种简单的、轻量级计时器,它使用回调方法而不是使用事件,并由线程池线程提供支持。
可以使用System.Threading.Timer做一些桌面应用显示时钟等小功能
(1) Timer 主要使用的的构造函数

1 public Timer(TimerCallback callback, object state, int dueTime, int period);

2 public Timer(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period);

参数意义
callback一个委托,表示要执行的方法
state表示回调方法callback中,要使用的对象或null
dueTime表示实例后,多久后启动计时器
period表示计时间隔

使用Timer 需要注意:一定要声明成全局变量以保持对Timer的引用,否则会被垃圾回收

(2)实例:

//首先声明Timer变量,一定要声明称全局变量以保持对Timer的引用,不要在方法内部申明局部变量,
//否则会被垃圾回收~
private System.Threading.Timer timer;
//实例化
timer = new System.Threading.Timer((obj)=> 
{
      Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); 
},null,0,1000);
timer.Dispose();//作用:注销(停止)计时器

//还可以通过使用Change方法停止计时器,把 dueTime 参数置为-1就可以停止定时器。


四、Task【重点】

Task是微软在.Net 4.0时代推出来的,也是微软极力推荐的一种多线程的处理方式,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool。

1.Task开启线程的方式

public static void TaskTest()
{
    //【1】new Task().Start()
    Task task1 = new Task(() => { Console.WriteLine("Start开启一个子线程"); });
    task1.Start();
    //【1】实例化Task的一个重载,可传入委托需要用到的参数
    Task task1_1 = new Task((inStr) => { Console.WriteLine($"Start开启一个子线程,入参{inStr}"); },"入参AAA");
    task1_1.Start();
    //通过AsyncState 获取创建子线程时委托传入的参数值
    string result = task1_1.AsyncState.ToString();
    Console.WriteLine($"输出委托的传入参数:{result}");

    //【2】Task.Run()
    Task task2 = Task.Run(() => { Console.WriteLine("Run开启一个子线程"); });
    //【2】Task.Run<TResult>表示一个可以返回值的异步操作
    Task<long> task2_1 = Task.Run<long>(() =>
    {
        return DoSomethingLongTime();
    });
    long rst = task2_1.Result;//获取子线程返回的值

    //【3】Task.Factory.StartNew
    Task task3 = Task.Factory.StartNew(() => { Console.WriteLine("Factory.StartNew开启一个子线程"); });
    //【3】Task.Factory.StartNew<TResult>
    Task<long> task3_1 = Task.Factory.StartNew<long>(() =>
    {
        return DoSomethingLongTime();
    });
    long rst2 = task3_1.Result;
    //另外还可以
    TaskFactory task3_2=new TaskFactory();
    task3_2.StartNew(()=> { Console.WriteLine("TaskFactory开启一个子线程");});

    //【4】new Task().RunSynchronously()同步执行,上述三种均是异步
    Task task4 = new Task(() => { Console.WriteLine("Factory.StartNew开启一个子线程111"); });
    task4.RunSynchronously();
    Task task4_1 = new Task(() => { Console.WriteLine("Factory.StartNew开启一个子线程222"); });
    task4_1.RunSynchronously();
    Task task4_2 = new Task(() => { Console.WriteLine("Factory.StartNew开启一个子线程333"); });
    task4_2.RunSynchronously();
    Task task4_3 = new Task(() => { Console.WriteLine("Factory.StartNew开启一个子线程444"); });
    task4_3.RunSynchronously();
    //以上线程中的方法将会依次执行,不再异步执行

    //【4】new Task().RunSynchronously()同步执行且带返回值的Task,(就当没开子线程)
    Task<long> task4_4 = new Task<long>(() =>
    {
        return DoSomethingLongTime();
    });
    task4_4.RunSynchronously();
    long result = task4_4.Result;

}
//耗时操作
public static long DoSomethingLongTime()
{
    long iResult = 0;
    for (int i = 0; i < 1_000_000_000; i++)
    {
        iResult += i;
    }
    return iResult;
}

new Task().Start()开启还有一个重载的方法public void Start(TaskScheduler scheduler);
可以这样用:new Task().Start(TaskScheduler.FromCurrentSynchronizationContext())还没搞明白这个用法

2.Task在WinForm中跨线程操作UI

同Thread是一样的使用invoke

        private void button_Click(object sender, EventArgs e)
        {
            Task task = new Task(() =>
            {
                //耗时操作
                long res = DoSomething();
                while (true)
                {
                    this.lb_time.Invoke(new Action(() =>
                    {
                        this.lb_time.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                    }));
                    Thread.Sleep(100);
                }
            });
            task.Start();
        }

        public long DoSomething()
        {
            long iResult = 0;
            for (int i = 0; i < 1_000_000_000; i++)
            {
                iResult += i;
            }
            return iResult;
        }

3.Task线程等待

(1)task.Wait()
task.Wait()是Task实例方法,使用时会阻塞主线程,因为Wait()会让task内部的任务执行完后,主线程才往后执行。

        static void Main(string[] args)
        {
            Console.WriteLine("----主线程--Start-----");
            TaskTest();
            Console.WriteLine("----主线程--End-----");
            Console.ReadLine();
        }

        public static void TaskTest()
        {
            Task task1 = new Task(()=> 
            {
                DoSomethingLongTime();
            });
            task1.Start();
            //【1】内部执行完方可往下执行
            task1.Wait();
            //【2】指定等待的毫秒数,到指定毫秒后,不管是否执行完,都往下执行,相当于说好我要卡你主线程多久
            task1.Wait(1000);
            //作用同【2】,不过使用TimeSpan可以指定时分秒等更多的单位,更加的灵活
            task1.Wait(TimeSpan.FromMilliseconds(1000));
        }
        //耗时任务
        public static void DoSomethingLongTime()
        {
            long iResult = 0;
            for (int i = 0; i < 1_000_000_000; i++)
            {
                iResult += i;
            }
            Console.WriteLine("耗时任务完成!");
            //return iResult;
        }

在这里插入图片描述
从输出结果可知,如果没有使用wait,那么主线程将会正常执行,使用后主线程将会等待task中的任务执行完,才执行自己的内容。

(2)Task.WaitAny()&Task.WaitAll()
均是Task静态方法,存在多个子线程的时候,
当使用WaitAny可实现【任意一个】task完成后,就可以往后执行,同样会卡主线程。比如上位机查询报警点,任意一个报警点报警都必须触发报警。
当使用WaitAll的时候,就需要等到taskList中的所有任务完成才会往后执行,会卡主线程。对应案例的表现就是所有的报警点报警才会触发报警

        static void Main(string[] args)
        {
            Console.WriteLine("----主线程--Start-----");
            TaskTest();
            Console.WriteLine("----主线程--End-----");
            Console.ReadLine();
        }

        public static void TaskTest()
        {
            List<Task> taskList = new List<Task>();
            Task task1 = Task.Run(() => { GetAlarmInfo("站点1"); });
            Task task2 = Task.Run(() => { GetAlarmInfo("站点2"); });
            Task task3 = Task.Run(() => { GetAlarmInfo("站点3"); });
            taskList.Add(task1);
            taskList.Add(task2);
            taskList.Add(task3);
            //Task.WaitAny需要传入一个Task的数组
            Task.WaitAny(taskList.ToArray());
            Console.WriteLine("有报警点查询到报警信息,需要报警");
			
			//WaitAll等待所有的任务完成,才往后执行
            //Task.WaitAll(taskList.ToArray());
            //Console.WriteLine("所有报警点都查询到报警信息,紧急报警");

        }

        public static void GetAlarmInfo(string name)
        {
            Console.WriteLine($"Start--开始查询报警【{name}】的数据");
            DoSomethingLongTime();//模拟查询任务
            Console.WriteLine($"End--查询到报警【{name}】的数据");
        }

        public static void DoSomethingLongTime()
        {
            long iResult = 0;
            for (int i = 0; i < 1_000_000_000; i++)
            {
                iResult += i;
            }
        }

输出结果:
在这里插入图片描述
在这里插入图片描述
(3)Task.WhenAny()&Task.WhenAll()&ContinueWith()
1、Task.WhenAny()&Task.WhenAll()均是Task静态方法

2、Task.WhenAny()+ContinueWith() 可以实现,当传入的多个线程中任意一个线程执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)

3、Task.WhenAll()+ContinueWith() 可以实现,当传入的多个线程都执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)

4、案例

        static void Main(string[] args)
        {
            Console.WriteLine("----主线程--Start-----");
            //TaskTest(1);
            TaskTest(2);
            Console.WriteLine("----主线程--End-----");
            Console.ReadLine();
        }

        public static void TaskTest(int flag)
        {
            List<Task> taskList = new List<Task>();
            TaskFactory factory = new TaskFactory();
            taskList.Add(factory.StartNew(() => { GetAlarmInfo("站点1"); }));
            taskList.Add(factory.StartNew(() => { GetAlarmInfo("站点2"); }));
            taskList.Add(factory.StartNew(() => { GetAlarmInfo("站点3"); }));
            if (flag == 1)
            {
                //当传入的多个线程中任意一个线程执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
                Task.WhenAny(taskList.ToArray()).ContinueWith((continuationAction) =>
                {
                    Console.WriteLine($"【{flag}】-开始紧急报警");
                    DoSomethingLongTime();
                    Console.WriteLine($"【{flag}】-紧急报警完毕");

                });
            }
            else
            {
                //当传入的多个线程都执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
                Task.WhenAll(taskList.ToArray()).ContinueWith((continuationAction) =>
                {
                    Console.WriteLine($"【{flag}】-开始紧急报警");
                    DoSomethingLongTime();
                    Console.WriteLine($"【{flag}】-紧急报警完毕");

                });
            }
        }

        public static void GetAlarmInfo(string name)
        {
            Console.WriteLine($"Start--开始查询报警【{name}】的数据");
            DoSomethingLongTime();//模拟查询任务
            Console.WriteLine($"End--查询到报警【{name}】的数据");
        }

        public static void DoSomethingLongTime()
        {
            long iResult = 0;
            for (int i = 0; i < 1_000_000_000; i++)
            {
                iResult += i;
            }
        }

输出结果:效果就是都不卡主线程
在这里插入图片描述
5、分析WhenAny,WhenAll,ContinueWith

分析项结果
Wait与WhenWait的就是需要阻塞主线程的,使用Wait就是子线程和主线程说:“你要等我干完我的事情,你再干”。使用When就是子线程和主线程说:“你干你的事情,不用管我,我什么时候还不知道,我自己处理自己的事情”
WaitAny与WhenAny使用WaitAny就是有多个子线程和主线程说:“你先等着,我们中任何一个事情干完了,你再开始做你的事情”。使用WhenAny就是多个子线程和主线程说:“你干你的事情,不用管我们,我们中有人干完事情,我们自己安排后续的事情”
WaitAll与WhenAll使用WaitAll就是子线程和主线程说:“你需要等我们都把事情干完了,你再开始做你的事情”。使用WhenAny就是和主线程说:“你干你的事情,不用管我,我们自己做完事情自己安排”
ContinueWith使用ContinueWith就是子线程安排后续自己要做的事情
#表示提供的任务之一已完成的任务。 返回任务的结果是完成的任务。
public static Task<Task> WhenAny(params Task[] tasks)
#返回结果:表示所有提供的任务的完成情况的任务。
public static Task WhenAll(params Task[] tasks);
#ContinueWith返回的一个新的延续 System.Threading.Tasks.Task
##入参:在运行时,委托将作为一个参数传递给完成的任务
public Task ContinueWith(Action<Task<TResult>> continuationAction);

Task.WhenAny(taskList.ToArray()).ContinueWith()
完成任务列表中的一个任务的任务后面使用ContinueWith再延续一个任务
Task.WhenAll(taskList.ToArray()).ContinueWith()
完成所有任务的任务后面使用ContinueWith再延续一个任务

(4)ContinueWhenAny()&ContinueWhenAll()
1、ContinueWhenAny 的作用等于 WhenAny+ContinueWith
2、ContinueWhenAll 的作用等于 WhenAll+ContinueWith

            if (flag == 1)
            {
                //当传入的多个线程中任意一个线程执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
                Task.Factory.ContinueWhenAny(taskList.ToArray(), ((continuationAction) =>
                 {
                     Console.WriteLine($"【{flag}】-开始紧急报警");
                     DoSomethingLongTime();
                     Console.WriteLine($"【{flag}】-紧急报警完毕");

                 }));
            }
            else
            {
                //当传入的多个线程都执行完成后,继续执行执行ContinueWith中的任务(不卡主线程,因为新开了一个线程)
                factory.ContinueWhenAll(taskList.ToArray(),((continuationAction) =>
                {
                    Console.WriteLine($"【{flag}】-开始紧急报警");
                    DoSomethingLongTime();
                    Console.WriteLine($"【{flag}】-紧急报警完毕");

                }));
            }

只需将上面讲WhenAll和WhenAny中的两行代码进行替换即可
3 、可通过Task.Factory.ContinueWhenAny()静态方法调用
也可通过factory.ContinueWhenAll()实例方法调用

public Task ContinueWhenAny(Task[] tasks, Action<Task> continuationAction);

public Task ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction);

第一个参数均是传入一个任务列表,第二个参数表示将要延续执行的委托,
委托的参数则是第一个参数:任务列表

4.线程返回值

(1)主线程中获取返回值,会阻塞主线程

        private void TaskMethod1()
        {
            //这样子在主线程内获取返回值,会导致主线程阻塞
            Task<long> task2_1 = Task.Run<long>(() =>
            {
                return DoSomethingLongTime1();
            });
            long rst = task2_1.Result;//获取子线程返回的值
        }

(2)使用ContinueWith获取返回值,并做后续操作,不会阻塞线程,并且还可以连环使用返回值

        private void TaskMethod2()
        {
            //使用ContinueWith会另外开一个线程不会卡顿
            Task<long> task2_1 = Task.Run<long>(() =>
            {
                //Console.WriteLine(task2_1.Result.ToString());
                //第一个任务的返回值,是无法在自己内部直接获取的
                return DoSomethingLongTime1();
            });
            task2_1.ContinueWith(t =>
            {
                // 这里的 t 是一个带返回值的Task
                // 通过 t.Result 可以取出上一个任务的返回值,然后再行使用
                Console.WriteLine(t.Result.ToString());
                return 2 + t.Result;
            }).ContinueWith(s=> 
            {
                //这里没有return ,系统自动识别是没有返回值的Task
                Console.WriteLine(s.Result.ToString());
            }) ;
        }

(3)使用ContinueWhenAny和ContinueWhenAny获取返回值并作不同的操作,不会阻塞主线程

        private void TaskMethod3()
        {
            List<Task<long>> tasks = new List<Task<long>>();
            TaskFactory factory = new TaskFactory();
            tasks.Add(factory.StartNew<long>(() => { return DoSomethingLongTime2(1); }));
            tasks.Add(factory.StartNew<long>(() => { return DoSomethingLongTime2(2); }));
            tasks.Add(factory.StartNew<long>(() => { return DoSomethingLongTime2(3); }));
            tasks.Add(factory.StartNew<long>(() => { return DoSomethingLongTime2(4); }));
            //在使用ContinueWhenAny获取第一个返回的数据
            factory.ContinueWhenAny(tasks.ToArray(), (t) =>
             {
                 Console.WriteLine($"第一个返回的结果:{t.Result}");
             });
            //使用ContinueWhenAll执行完所有任务后,找出最大值,最小值和结算总和
            factory.ContinueWhenAll(tasks.ToArray(),(t)=> 
            {
                long sumNum = 0;
                long maxNum = t.Max(x => x.Result);
                Console.WriteLine($"返回的最大值:{maxNum}");
                long minNum = t.Min(x => x.Result);
                Console.WriteLine($"返回的最小值:{minNum}");
                long sum = t.Sum(x => x.Result);
                Console.WriteLine($"返回的总和方式1:{sum}");
                foreach (var item in t)
                {
                    sumNum += item.Result;
                };
                Console.WriteLine($"返回的总和方式2:{sumNum}");
            });
        }

        private long DoSomethingLongTime2(int id)
        {
            Console.WriteLine($"Start--耗时任务【{id}】开始");

            long iResult = 0;
            for (int i = 0; i < 1_000_000_000; i++)
            {
                iResult += i;
            }
            iResult += id;//这里只是为了后面对比大小使用
            Console.WriteLine($"End--耗时任务【{id}】结束");
            return iResult;
        }

输出结果:
在这里插入图片描述
从以上的案例就可Task在任务的延续上是完胜Thread和ThreadPool

5.TaskCreationOptions&TaskContinuationOptions枚举类

(1)TaskCreationOptions
主要是可以指定任务的运行时的相关设置,创建任务时即可设置,一般默认足够使用,感兴趣的可以自行敲代码尝试一下

private void TaskMethod4()
{           
    Task task = new Task(() =>
    {
        //【None默认情况】默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行,异步运行
        Task task1 = new Task(() =>
        {
            DoSomethingLongTime2(1);
        }, TaskCreationOptions.None);
        task1.Start();

        //【AttachedToParent】指定将任务附加到任务层次结构中的某个父级,将父任务和子任务同步
        Task task2 = new Task(() =>
        {
            DoSomethingLongTime2(2);
        }, TaskCreationOptions.AttachedToParent);
        task2.Start();

        //【LongRunning】指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件
        //提示,过度订阅可能是合理的
        Task task3 = new Task(() =>
        {
            DoSomethingLongTime2(3);
        }, TaskCreationOptions.LongRunning);
        task3.Start();

        //防止环境计划程序被视为已创建任务的当前计划程序。 
        //这意味着像 StartNew 或 ContinueWith 创建任务的执行操作将被视为 
        //System.Threading.Tasks.TaskScheduler.Default当前计划程序。
        Task task4 = new Task(() =>
        {
            DoSomethingLongTime2(4);
        }, TaskCreationOptions.HideScheduler);
        task4.Start();
        //以一种尽可能公平的方式安排任务,这意味着较早安排的任务将更可能较早运行,
        //而较晚安排运行的任务将更可能较晚运行。
        Task task5 = new Task(() =>
        {
            DoSomethingLongTime2(5);
        }, TaskCreationOptions.PreferFairness);
        task4.Start();

        DoSomethingLongTime2(0);

        //【DenyChildAttach】指定任何尝试作为附加的子任务都无法附加到父任务,会改成作为分离的子任务执行
    }, TaskCreationOptions.DenyChildAttach);
    task.Start();
    //task.Wait();
}

(2)TaskContinuationOptions
1 通过设置TaskContinuationOptions影响ContinueWith传入的任务的运行方式
2 【None】默认是在前面的任务完成后以异步方式运行, 如果延续为子任务,则会将其创建为分离的嵌套任务
3 其余枚举值感兴趣的可以自行尝试了解

        private void TaskMethod5()
        {

            Task task1 = new Task(() =>
            {
                DoSomethingLongTime2(1);
            });
            //在前面的任务完成后以异步方式运行, 如果延续为子任务,则会将其创建为分离的嵌套任务。
            task1.ContinueWith((t) =>
            {
                Console.WriteLine("TaskContinuationOptions.None");
            }, TaskContinuationOptions.None).ContinueWith((s) =>
            {
                Console.WriteLine("TaskContinuationOptions.None");
            }, TaskContinuationOptions.ExecuteSynchronously);//指定应同步执行延续任务。
            task1.Start();
        }

6.延迟执行

ThreadSleep与Task.Delay()的区别
由上可知,使用Sleep是会阻塞主线程的,而是用Delay则不会阻塞主线程,因为他是另开一个线程在达到指定时间后异步执行。

7.多线程异常捕获

        //【1--直接在Task外包一层捕获异常,这样是无法捕获到异常的】
        private void TaskExceptionTest1()
        {
            try
            {
                throw new Exception("自定义异常");
            }
            catch (Exception)
            {
                Debug.WriteLine("自定义异常已处理");//自定义异常的可以轻松捕获
            }

            try
            {
                //对于多线程,在线程外面使用try,catch 无法捕获到异常
                Task task = Task.Run(() =>
                {
                    throw new Exception("Task内部异常");
                });
            }
            catch (Exception ex)
            {

            }
        }

        //【2 --在线程内部获取异常,这个就是正常使用】
        public static void TaskExceptionTest2()
        {
            Task task = new Task(() =>
            {
                //直接在线程内部捕获异常,同样很容易,
                //【不仅捕获到异常还处理了异常】
                try
                {
                    throw new Exception("Task内部异常");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"线程发生异常-异常信息:{ex.Message}");
                }
            });
            task.Start();
        }

        //【3--同样Task外层包一层try catch,同时使用线程等待Task.WaitAll】
        //这样会捕获到多线程内部所有的异常
        //多线程专用的异常AggregateException,另外可通过InnerExceptions获取异常集合
        //在多个子线程的情况下:与在线程内部捕获异常相比,这样更加方便,但是编译的时候会报错
        //内部使用try catch代码会显得臃肿一点,但是简单,看具体情况选择使用
        public static void TaskExceptionTest3()
        {
            try
            {
                Task task1 = Task.Run(() =>
                {
                    throw new Exception("task1内部异常");
                });
                Task task2 = Task.Run(() =>
                {
                    throw new Exception("task2内部异常");
                });
                //线程等待
                Task.WaitAll(task1,task2);
            }
            catch (AggregateException aex)
            {
                foreach (var exception in aex.InnerExceptions)
                {
                    Debug.WriteLine($"线程发生异常-异常信息:{exception.Message}");
                }
            }
            catch (Exception ex)
            {

            }
        }

8.取消线程

1)使用变量的方式,取消线程

一般取消流程是:设置一个变量来控制任务是否停止,例如设置一个变量IsCancel ,然后线程轮询查看IsCancel ,如果IsCancel 为true就取消线程,代码如下:

        private bool IsCancel = false;

		//点击按钮开启一个线程
        private void button8_Click(object sender, EventArgs e)
        {
            int index = 0;
            Task.Run(()=> 
            {
                while (!IsCancel)
                {
                    Debug.WriteLine($"线程运行中,第{index + 1}次执行");
                    index++;
                    Thread.Sleep(1000);
                }
            });
        }
        
		//点击按钮,更改变量值,从而实现取消线程
        private void button9_Click(object sender, EventArgs e)
        {
            IsCancel = true;
        }

2)使用CancellationTokenSource取消线程

通过专门的类 CancellationTokenSource 来取消线程,上面案例,如果同样使用CancellationTokenSource 取消线程,则代码如下:

        //【1】实例化CancellationTokenSource 
        CancellationTokenSource source = new CancellationTokenSource();


        private void button8_Click(object sender, EventArgs e)
        {
            int index = 0;
            Task.Run(()=> 
            {
                //【2】使用IsCancellationRequested属性值,默认为false
                while (!source.IsCancellationRequested)
                {
                    Debug.WriteLine($"线程运行中,第{index + 1}次执行");
                    index++;
                    Thread.Sleep(1000);
                }
            });
        }

        private void button9_Click(object sender, EventArgs e)
        {
            //【3】使用CancellationTokenSource中的Cancel方法
            //该方法会将IsCancellationRequested变为false
            source.Cancel();

            //【4】使用CancelAfter可以在指定时间后,取消线程,等待过程中不会阻塞主线程
            source.CancelAfter(1000);//在1s后取消线程,这1s不会阻塞主线程
        }

3)source.Token.Register

由上看来好像区别不太大,但是CancellationTokenSource 的功能不止于此,使用Token属性可以注册一个委托,
这样可以实现,取消后想做点什么事情。代码如下:

        //【1】实例化CancellationTokenSource 
        CancellationTokenSource source = new CancellationTokenSource();

        private void button8_Click(object sender, EventArgs e)
        {
            int index = 0;
            //【2】注册一个取消时需要调用的委托
            source.Token.Register(()=> 
            {
                Debug.WriteLine("通知:线程已经取消了,我要去干点别的什么了!");
            });

            Task.Run(()=> 
            {
                //【3】使用IsCancellationRequested属性值,默认为false
                while (!source.IsCancellationRequested)
                {
                    Debug.WriteLine($"线程运行中,第{index + 1}次执行");
                    index++;
                    Thread.Sleep(1000);
                }
            },source.Token);//【4】将token 传入Task
        }

        private void button9_Click(object sender, EventArgs e)
        {
            source.Cancel();
        }

结果如下:
在这里插入图片描述

4)CreateLinkedTokenSource组合取消

利用CreateLinkedTokenSource构建CancellationTokenSource的组合体,其中任何一个体取消,则组合体就取消,代码如下:

        private void button8_Click(object sender, EventArgs e)
        {
            TestCancellationTokenSource("1-Cancel");
            TestCancellationTokenSource("2-Cancel");
            TestCancellationTokenSource("3-Cancel");
        }

        private void TestCancellationTokenSource(string name)
        {
            CancellationTokenSource source1 = new CancellationTokenSource();
            CancellationTokenSource source2 = new CancellationTokenSource();
            CancellationTokenSource source3 = new CancellationTokenSource();
            CancellationTokenSource combineSource = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token, source3.Token);
            //【2】取消后观察,各CancellationTokenSource的IsCancellationRequested的变化
            if (name.Equals("1-Cancel")) source1.Cancel();
            else if (name.Equals("2-Cancel")) source2.Cancel();
            else source3.Cancel();

            //【3】输出各个IsCancellationRequested的值
            Debug.WriteLine($"【{name}】source1.IsCancellationRequested={source1.IsCancellationRequested}");
            Debug.WriteLine($"【{name}】source2.IsCancellationRequested={source2.IsCancellationRequested}");
            Debug.WriteLine($"【{name}】source3.IsCancellationRequested={source3.IsCancellationRequested}");
            Debug.WriteLine($"【{name}】combineSource.IsCancellationRequested={combineSource.IsCancellationRequested}");
            Debug.WriteLine($"------------------------------------------------------------------------------------------");
        }

输出结果如下:
在这里插入图片描述
由此可知,无论组合中哪一个状态发生改变,组合的那个CancellationTokenSource状态一定会随之发生改变

5)监控取消

使用Cancel()之后,调用ThrowIfCancellationRequested就会抛异常,不取消,不会抛异常
在这里插入图片描述

9.解决中间变量问题

            //{
            //    for (int i = 0; i < 5; i++)
            //    {
            //        Debug.WriteLine($"ThreadID={Thread.CurrentThread.ManagedThreadId.ToString("00")}_i={i}");
            //    }
            //}
            //【方式1】:Task开启线程的时候,延迟开启,在循环的时候,不会阻塞主线程,
            //           循环很快,线程执行业务逻辑的时候,循环已经结束了,i已经变成5
            //{
            //    for (int i = 0; i < 5; i++)
            //    {
            //        Task.Run(() =>
            //        {
            //            Debug.WriteLine($"ThreadID={Thread.CurrentThread.ManagedThreadId.ToString("00")}_i={i}");
            //        });
            //    }
            //}
            //【方式2】:在每次的循环的时候赋予变量一个值,这里i每次输出的结果还会是5,但是k每个的结果都会不同,
            //因为每次运行到Task.Run的时候就会新申请一个线程,线程内k的值就是申请的那一刻i复制给K的值,而申请和启动不是同时进行的
            //运行流程: i循环 的过程中,不断复赋值给k,当程序走到Task.Run,,线程获得了k的值,但是还没有完成线程启动,
            //等待i已经循环完的时候,程序可能才完成了5个线程的申请并开始启动,因此每个线程获得k值都是不一样的
            {
                for (int i = 0; i < 5; i++)
                {
                    int k = i;
                    Task.Run(() =>
                    {
                        Debug.WriteLine($"ThreadID:{Thread.CurrentThread.ManagedThreadId}\ti={i}_k={ k}");
                    });
                }
            }

五、Parallel

1.Parallel介绍

Parallel类是对线程的抽象,提供数据与任务的并行性。
Parallel.For和Parallel.ForEach方法在每次迭代的时候调用相同的代码,而Parallel.Invoke()方法允许同时调用不同的方法。
Parallel.ForEach()方法用于数据的并行性,Parallel.Invoke()方法用于任务的并行性。

2.Parallel.Invoke()

尽可能并行执行提供的每个操作,需要传入一个委托数组。

public static void Invoke(params Action[] actions)

案例代码:

        static void Main(string[] args)
        {
            Console.WriteLine("----主线程--Start-----");
            ParallelTest();
            Console.WriteLine("----主线程--End-----");
            Console.ReadLine();
        }


        private static void ParallelTest()
        {
            var actions = new Action[]
            {
                new Action (()=>{ActionTest("action1"); }),
                ()=>{ActionTest("action2"); },//简化
                ()=>{ ActionTest("action3"); },
            };

            Console.WriteLine("ParallelTest ---Start");
            Parallel.Invoke(actions);//并行执行

            Console.WriteLine("ParallelTest ---End");
        }

        static void ActionTest(string name)
        {
            Console.WriteLine($"thread:{Thread.CurrentThread.ManagedThreadId}, value:{name}");
        }

运行结果:
在这里插入图片描述
(1)由上面案例的运行结果可知:

  • 使用Parallel.Invoke是会阻塞主线程的,与Task.WaitAll()效果类似,只是默认就拥有这个特性了,那么随之而来就是灵活性的缺失。
  • 使用Parallel.Invoke会并行的执行多个委托,在此过程中会创建多个子线程

(2)把Parallel包在一个Task里面实现不卡主线程,如:

            Task.Run(()=> 
            {
                Parallel.Invoke(actions);
            });

(3)使用Invoke的重载方法,传入ParallelOptions参数,限制开启的线程数量,可以做到不影响线程池的线程数量又能控制当前执行所用的线程数量

public static void Invoke(ParallelOptions parallelOptions, params Action[] actions);
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 2;//设置最大并发数
Parallel.Invoke(options,actions);

3.Parallel.For()

Parallel.For()方法类似于C#的for循环语句,循环多次执行一个任务,可以并行运行迭代,但迭代的顺序并没指定。主要用于处理针对数组元素的并行操作(数据的并行)

static void Main(string[] args)
{
    int[] nums = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
    Parallel.For(0, nums.Length, (i) =>
    {
        Console.WriteLine($"索引:{i},数组元素:{nums[i]},线程ID:{Thread.CurrentThread.ManagedThreadId});
    });
    Console.ReadKey();
}

4.Parallel.ForEach()

主要用于处理泛型集合元素的并行操作(数据的并行)

public void Test()
{
    List<int> nums = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
    Parallel.ForEach(nums, (item) =>
    {
        Console.WriteLine($"输出元素:{item}、线程ID:{Thread.CurrentThread.ManagedThreadId}" );
    });
    Console.ReadKey();
}

总结

以上就是本文要讲的内容,希望通过以上的介绍,可以让你对线程有更深的理解,在后续使用线程的使用能够更加得心应手。如有不对之处,还请不吝批评指正。


参考:
百度百科
多线程
C# Task和async/await详解
c# Parallel类的使用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/73341.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【微电网】具有柔性结构的孤岛直流微电网的分级控制(Malab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f4dd;目前更新&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;电力系统相关知识&#xff0c;期刊论文&…

carsim/trucksim获取轮胎侧偏刚度、纵向刚度

本文参考&#xff1a;https://blog.csdn.net/weixin_44902384/article/details/107926814 这个方法适应计算侧偏刚度、纵向刚度&#xff0c;因为魔术公式里y 可以代表侧向力、纵向力 针对上面的内容&#xff0c;有两个问题需要解释。1是魔术公式轮胎中 有的是tan-1 有的是ar…

[Linux]------线程池的模拟实现和读者写者锁问题

文章目录前言一、线程池二、线程安全的单例模式什么是单例模式什么是设计模式单例模式的特点三、STL&#xff0c;智能指针和线程安全STL中的容器是否是线程安全的&#xff1f;智能指针是否是线程安全的&#xff1f;四、其他常见的各种锁五、读者写者问题读写锁读写锁接口初始化…

云开发智能家居客户案例详解(内附拓扑图)

万物互联&#xff0c;大至全世界&#xff0c;小至一间房&#xff0c;物联网和云计算技术的高速发展使得住宅变得愈发智能化。 在“互联网”时代&#xff0c;智能家居开始走入千家万户&#xff0c;不断提升着家居生活的安全性、舒适型、便利性和环保性&#xff0c;逐渐变成人们…

Linux 用户权限

用户权限1、访问权限2、chmod 命令3、chown 命令4、chgrp命令5、权限掩码6、lsattr 命令7、chattr命令8、文件的特别权限suid权限set位权限粘滞位权限&#xff08;Sticky&#xff09;9、ACL访问控制列表setfacl命令getfacl命令示例10、sudo11、SELinux1、访问权限 shell在创建…

SpringBoot2学习笔记--入门及HelloWorld

SpringBoot2学习笔记--入门及HelloWorld1 系统要求1.1、maven设置2、HelloWorld2.1、创建maven工程2.2、引入依赖2.3、创建主程序2.4、编写业务2.5、测试2.6、简化配置2.7、简化部署1 系统要求 ● Java 8 & 兼容java14 . ● Maven 3.3 ● idea 2019.1.2 1.1、maven设置 …

Java版 剑指offer笔记(一)

1.数组中重复的数字 思路1&#xff1a; 使用哈希表&#xff0c;哈希表是一种根据关键码&#xff08;key&#xff09;直接访问值&#xff08;value&#xff09;的一种数据结构。而这种直接访问意味着只要知道key就能在O(1)时间内得到value&#xff0c;因此哈希表常用来统计频率…

软件测试有哪些常用的测试方法?

软件测试是软件开发过程中重要组成部分&#xff0c;是用来确认一个程序的质量或者性能是否符合开发之前提出的一些要求。软件测试的目的有两方面&#xff0c;一方面是确认软件的质量&#xff0c;另一方面是提供信息&#xff0c;例如&#xff0c;给开发人员或者程序经理反馈意见…

4.MyBatis映射

需求分析 1.订单商品数据模型 (1).表 用户表user:记录了购买商品的用户信息 订单表orders:记录了用户所创建的订单信息 订单明细表orderdetail:记录了订单的详细信息 商品表item:记录了商品详细信息 (2).表与表之间的业务关系 在分析表与表之间的业务关系时&#xff0c;需要建…

Nginx的反向代理和负载均衡

Nginx&#xff1a; Nginx作为面试中的大…小头目&#xff0c;自然是不能忽视的&#xff0c;而以下两点就是它能成为面试中头目的招牌。 反向代理和负载均衡 在此之前&#xff0c;我们先对Nginx做一个简单的了解 Nginx概述&#xff1a; Nginx (engine x) 是一个高性能的HTTP…

Ansible——inventory 主机清单

Ansible——inventory 主机清单Ansible——inventory 主机清单inventory简介ansible配置文件的优先级ansible命令常用参数主机清单文件hosts&#xff08;/etc/ansible/hosts&#xff09;通过列表的方式标识主机范围指定主机端口使用主机名表示主机范围inventory 中的变量主机变…

JS 数组方法 every 和 some 的区别

1. 前言 2. every 和 some 相同点 3. every 和 some 的区别 4. every 和 some 总结 1. 前言 JS 数组方法 every 和 some 的区别 &#xff1f; 这是某位前端玩家遇到的面试题 特定场景合理的使用 JS 方法&#xff0c;不仅可以减少我们的代码量&#xff0c;还能更轻松的阅读…

宇航服,真正的“科技”与“狠活”!

千百年的探索仰望和摘星的遐想&#xff0c;已照进现实&#xff0c;浩瀚的天宫&#xff0c;我们亦可置身其中。 北京时间2022年12月4日20时09分&#xff0c;神舟十四号载人飞船返回舱在东风着陆场成功着陆&#xff0c;标志着太空出差183天的宇航员正式回家&#xff01;据悉&…

基于PCA 和迭代 Canny Edge皮肤病变分割算法研究附Matlab代码

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

NLP_learning 中文基本任务与处理(分词、停用词、词性标注、语句依存分析、关键词抽取、命名实体识别)介绍、jieba工具库

文章目录1、分词2、停用词和N-gram停用词N-gram3、 更多任务&#xff08;词性标注、依赖分析、NER、关键词抽取&#xff09;词性标注句法依存分析命名实体识别关键词抽取4、 jieba工具库使用&#xff08;1&#xff09;基本分词函数与用法&#xff08;2&#xff09;词性标注&…

【LeetCode】C++:数组类算法-双索引技巧-对撞指针

目录 167. 两数之和 II - 输入有序数组 125.验证回文串 345.反转字符串中的元音字母 11.盛最多水的容器 209.长度最小的数组 167. 两数之和 II - 输入有序数组 给你一个下标从1开始的整数数组 numbers &#xff0c;该数组已按非递减顺序排列 &#xff0c;请你从数组中找出…

视频剪辑软件哪个好用?快把这些软件收好

现如今自媒体行业正在如火如荼的发展&#xff0c;越来越多的人加入进视频剪辑的队伍中。小伙伴们也有萌生想要剪辑视频的念头吗&#xff1f;大家是否苦于不知道该如何视频剪辑呢&#xff1f;为了帮助大家解决这个问题&#xff0c;今天我就来为大家教几种不错的剪辑方法&#xf…

YOLOv5图像分割中的NMS处理

在上一篇文章YOLOv5图像分割--SegmentationModel类代码详解有讲到图像经过YOLOv5网络后得到的输出形式&#xff0c;主要是调用了BaseModel类下的forward得到的输出&#xff0c;输出的shape为【batch,25200,117】&#xff0c;这里的25200相当于总的anchors数量【以640*640的输入…

vuex原理和下载

vuex&#xff1a;状态管理模式 vue全家桶&#xff1a;vue-cli&#xff08;脚手架&#xff09;、vue-router&#xff08;路由管理器&#xff09;、vuex&#xff08;状态管理模式&#xff09; 原理图示&#xff1a; 原理描述&#xff1a; vuex在vue组件外面进行组件状态的管理…

引用的小细节内联函数

1.引用的细节 引用&#xff0c;简单来说就是“取别名”。既然是别名&#xff0c;那么引用就一定具有以下的特点 引用在定义时必须初始化。 就好比起别名起码得告诉别人是给谁起的别名吧 一个变量可以有多个引用 就好比一个人可以有多个别名。比如张某某&#xff0c;有两个外号…