多线程到底是什么?

进程Processer:

进程不是一个物理存在的,是一个计算机的概念(虚拟的),把一个程序运行时占用的全部计算资源(CPU,内存,磁盘,网络等资源)的一个合集。

线程Thread:

也是计算机概念(虚拟的),是进程的最小执行流(任何一个操作的响应都需要执行流),线程也有自己的计算资源,线程依托于进程。

多线程MutiThread:

多个执行流并发执行,就是多线程。

进程向CLR申请线程,CLR向计算器申请。

Thread.CurrentThread.ManagedThreadId 当前线程的Id--任意操作的响应都需要线程,此id无意义,是CLR自定义的标识。

.net 历史上的多线程写法有很多,Thread,ThreadPool,BeginInvoke,Task,Parallel,AwaitAsync 但任何一个多线程,都不离开委托delegate,其中的最佳实践是Task。

线程不是越多越好,或者说不是越多越快,一般认为线程数量不要超过cpu核心数量*3,但是这个不是绝对的还需要根据每个线程具体执行的任务,我之前就经历过处理视频时,线程数量超过核心数量时,整体处理任务的耗时反而不如线程小于等于CPU数量时的情况。所以一般来说,如果单个线程处理的任务,没有那么消耗cpu资源的话,线程数量可以适当多一点;如果单个线程内执行的任务,比较耗费cpu资源的话,没有必要添加太多线程,这样只能增加cpu的调度损耗。

实践中,一般来说,根据任务类型,建立多个线程,每种线程都严格控制数量,而不是动态的随意建立线程,这样一方面方便对线程管理,另一方面减少由于过多的线程造成的cpu调度损耗等。

下边开始介绍线程的无序性,简单来说就是,执行相同的任务,先开始的线程不一定先结束,后开始的任务不一定后结束。为什么会造成这种情况呢,是因为线程是计算机资源,代码中申请线程后,不一定先申请的就会被真正的先拿到线程。这个不像是代码一定具有可控制。那么执行相同任务的时间不同的原因又是什么呢?是因为CPU的调度策略(时间片)。

1.多线程的临时变量问题

.net 多线程 例子 netcore多线程_.net core 多线程

先声明一个单独的变量的时候,再线程内使用该变量的时候,是按每次循环,单独创建一个变量。所以,造成虽然执行的顺序不可空,但是传进去的值是按照预期传进去的。

.net 多线程 例子 netcore多线程_asp.net 多线程开发_02

那么还有一个更容易理解的版本是这样的

.net 多线程 例子 netcore多线程_.net core 多线程_03

2.多线程的无序性,造成这样的结果是因为线程属于计算器资源,线程的分配回收控制权在计算机,代码中只能取申请,那么什么时候执行,由计算机自身根据运行的情况和自有逻辑来决定。

模拟一个停车场的简单逻辑,先开门,然后陆陆续续有人进入和退出,等所有人都退出了,才可以关门。

.net 多线程 例子 netcore多线程_多线程_04

Task.WaitAll();主线程会被阻塞,等待所有被等待线程执行完毕。此外还有Task.WaitAny();表示任意一个线程执行完毕。

如果不想阻塞主线程,还有什么其他的实现方式吗?简单粗暴的话,可以再这些任务外边用Task.Run(()=>{})重新包装一层,这样主线程就不会被阻塞了。但是这种,线程里边包裹线程的方法很难控制,尽量不要用。

那么用回调方法来实现是一条更加优雅的方式。

.net 多线程 例子 netcore多线程_asp.net 多线程开发_05

taskList[4].ContinueWith(t => Console.WriteLine("T5已退场请注意"));

这是给某一个特定线程添加回调函数,在该线程释放的时候,执行回调方法。下边还有监听线程list的方法,如下图:

.net 多线程 例子 netcore多线程_asp.net 多线程开发_06

这里边有两个方法分别是ContinueWhenAny和ContinueWhenAll,两个方法适用的场景不同,一个是任意一个线程执行完成时,一个是所有线程执行完成时,应用时根据实际场景选用即可。结合上边某个线程的ContinueWith,这三个方法应该可以满足基本的使用场景了。

Task.Factory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine("今日第一辆退场车辆已退场"));

Task.WaitAny(taskList.ToArray());

这俩个方法同时出现时,谁先执行?这个时随机的。同理,ContinueWhenAll和Task.WaitAll(taskList.ToArray());也是同一个道理。

这里还有另一个小的引申,加入我就是想用WaitAll来主线程中做一些流程控制,那么又想让WaitAll和ContinueWhenAll有顺序怎么办,这里有一个小技巧提供给大家,当作思路拓展吧。

.net 多线程 例子 netcore多线程_多线程_07

假如 ,我想等所有车辆退场之后,先打扫下卫生,再关门,我应该怎么操作?

.net 多线程 例子 netcore多线程_多线程_08

看看我们干了什么,我们把ContinueWhenAll方法(本身是一个线程)假如到线程数组中,这样WaitAll的时候,就会等ContinueWhenAll执行完了再触发执行WaitAll.因为有一些特定的操作是不方便放在某个特定线程内的,还是需要再主线程中去控制。这块的解决问题的思路很巧妙,也很常见,让我想起了前端中组件内跟零一组件相互控制的一些操作,感觉有异曲同工之妙。

假若任务不可预知,没有规律,给固定数量线程来处理这些任务,有哪些可用的实现方式?

1.任务队列形式,每个线程去任务队列取数据,然后执行,执行完毕之后再去取一个任务。这个方案要注意,任务队列的线程安全问题,防止任务被重复拿取和执行。

2.用Task.WaitAny()的小技巧来实现,实现方式如下:

.net 多线程 例子 netcore多线程_.net core 多线程_09

如图可视,任务队列长度为100,分配5个线程来实现,每个任务执行的时间长度不一致。从结果来看,每当一个任务执行结束,就会有一个新的线程来接管下一个任务。

代码如下:

public static void TestThreadPool()
    {
        List<Task> taskList = new List<Task>();
        for (int i = 0; i < 100; i++)
        {
            var flag = i;
            taskList.Add(Task.Run(() =>
            {
                Console.WriteLine($"TaskStart:{Thread.CurrentThread.ManagedThreadId},taskid:{flag}");
                var r = new Random();
                var val = 800+ r.Next(1, 100);
                Thread.Sleep(val);
                Console.WriteLine($"TaskEnd:{Thread.CurrentThread.ManagedThreadId},taskid:{flag},cost:{val}");
            }));

            if (taskList.Count== 5) {
                Task.WaitAny(taskList.ToArray());
                taskList = taskList.Where(t=>t.Status==TaskStatus.Running).ToList();
            }
        }
        Console.ReadLine();
    }

3.其实.net 给大家提供了一个更方便或者说更专业的选手来处理这一类型的业务,那就是Paralle,下边用Paralle来简单处理这一类的任务

.net 多线程 例子 netcore多线程_asp.net 多线程开发_10

public static void TestParalle()
    {
        List<int> works = new List<int>();
        for (int i = 0; i < 100; i++) {
            works.Add(i);
        }
        
        ParallelOptions parallelOptions = new ParallelOptions() {
            MaxDegreeOfParallelism = 5
        };
        Parallel.ForEach(works, parallelOptions, work =>
        {
            Console.WriteLine($"TaskStart:{Thread.CurrentThread.ManagedThreadId}");
            var r = new Random();
            var val = 800 + r.Next(1, 100);
            Thread.Sleep(val);
            Console.WriteLine($"TaskEnd:{Thread.CurrentThread.ManagedThreadId},cost:{val}");
        });
        Console.ReadLine();
    }