多线程详细介绍

什么是进程线程:我们来看一下自己的任务管理器

  

这里的每一项都是一个进程,我们的发布的每一个应用程序都需要一个进程去运行,在一个进程内可以有多个线程去计算执行程序。我们看下面的图片:

  

我们可以看一下进程和线程的数量,很明显可以看出,线程和进程的关系。我们的每一个操作都需要一个线程来执行,鼠标的点击就需要线程去响应我们的操作。

现在我们不难理解,我们一个应用程序就代表一个进程,想让我们的程序高效的运行我们就可以启用多个线程去执行了,当然采用多线程的话有好处也是有代价的,好处合理的利用计资源了,但是线程过多了,你的CPU利用率就加大了,也有可能导致电脑的卡死。

在我们的程序中线程的代表就是:Thread本篇文章我们就说一下线程。我们先了解一下同步异步。

同步:指的是在同一线程下执行,并且会等待结果执行完毕。

异步:不再同一个线程下执行,并且执行得顺序不可控,不会等待执行结果完毕。

我们先用委托来演示一下多线程,如果不怎么了解委托得可以看一下上一篇文章:

    DelegateMethod Method = () =>
            {
           Console.WriteLine($"我的线程ID是:{Thread.CurrentThread.ManagedThreadId}");
            };
            Method.BeginInvoke(null, null);
            Method.Invoke();

本来想用上面的代码去先简单的演示一下,谁知道NetCore 的程序集提供了,但是平台目前还不支持。 我们可以私下使用NET 试一下。

使用Thread 演示

 

            Console.WriteLine($"*********************************我是同步方法 *******************************");

            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine($"我的线程ID是:{Thread.CurrentThread.ManagedThreadId}");
            }
            Console.WriteLine($"*********************************同步方法结束 *******************************");

            Console.WriteLine($"*********************************我是异步方法 *******************************");
            for (int i = 0; i < 10; i++)
            {
                 Thread thread = new Thread(() => { Console.WriteLine($"我的线程ID是:{Thread.CurrentThread.ManagedThreadId}"); });
                 thread.Start();
            }
            Console.WriteLine($"*********************************异步方法结束 *******************************");
  
            Console.ReadKey();

执行结果:

   

上面的代码执行结果中我们可以看到:同步方法 是同一个线程来执行的,之上而下有序的执行,但是异步方法启动了多个线程去执行的,并且线程是无序的。看到这样的情况我们就会知道如果我启动了很多线程线程用完之后也是有回收的,回收之后同样会分配的,也就是说同一个操作中线程的ID 可能会多次出现的。

            Console.WriteLine($"*********************************异步启动线程数量计算  *******************************");
            List<int> ListInt = new List<int>();
            int Conut = 0;
            for (int i = 0; i < 2000; i++)
            {
                Thread thread = new Thread(() =>
                {
                    if (ListInt.Contains(Thread.CurrentThread.ManagedThreadId))
                    {
                        Conut++;
                        Console.WriteLine($"我的重复的线程我的 ID是:{Thread.CurrentThread.ManagedThreadId}  重复线程总数量:{Conut}");
                    }
                
                    ListInt.Add(Thread.CurrentThread.ManagedThreadId);
                });
                thread.Start();
            }
            Console.WriteLine($"*********************************异步启动线程数量计算结果:{ListInt.Count}  *******************************");

结果: 

  

上面的结果我们可以看到:线程回收再利用。其实thread线程的回收就是我们的GC来做的,这就是C# 的强大之处,自动帮助我们回收了。需要注意的是这样使用线程我们的回收过程是比较慢的,这个回收速度是我们计算机性能决定的。

在上面的结果中我们可以看到我们总共申请了1996个线程,其中有881个线程是重复的,线程的申请和销毁是耗费很多性能的,接下来我们看一下线程池。

线程thread有哪些可操作的属性

CurrentContext

获取线程正在其中执行的当前上下文。

CurrentCulture

获取或设置当前线程的区域性。

CurrentPrinciple

获取或设置线程的当前负责人(对基于角色的安全性而言)。

CurrentThread

获取当前正在运行的线程。

CurrentUICulture

获取或设置资源管理器使用的当前区域性以便在运行时查找区域性特定的资源。

ExecutionContext

获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。

IsAlive

获取一个值,该值指示当前线程的执行状态。

IsBackground

获取或设置一个值,该值指示某个线程是否为后台线程。

IsThreadPoolThread

获取一个值,该值指示线程是否属于托管线程池。

ManagedThreadId

获取当前托管线程的唯一标识符。

Name

获取或设置线程的名称。

Priority

获取或设置一个值,该值指示线程的调度优先级。

ThreadState

获取一个值,该值包含当前线程的状态。

 

 线程池:ThreadPool

       ThreadPool:线程池中的线程都是后台线程IsBackground属性都是True.不会影响所有的前台线程,也就是说不会影响用户体验。每个线程都有默认的堆栈大小和优先级,位于多线程池中。 一旦线程池中的线程完成任务,它将返回到等待线程队列中,这时我们就可以利用这些闲置的线程。通过这种重复使用,应用程序可以避免产生为每个任务创建新线程的开销。在每一个进程中都只会有一个线程池

 

            //public static bool SetMaxThreads(int workerThreads, int completionPortThreads);
            //public static bool SetMinThreads(int workerThreads, int completionPortThreads);
            //workerThreads 工作线程数量  completionPortThreads I/O线程数量
            ThreadPool.SetMaxThreads(12,12);  
            ThreadPool.SetMinThreads(12, 12);

我这里设置工作线程,I/O线程数量来源于我得计算机核心数量,保持每个核心最大最小线程都启动一个。查看计算机处理器核心数量。

  

上面的设置是说我在线程池中给准备了数量为12 的线程。你可以申请最多12个线程,在使用完之后我会立马进行自动的回收,回收之后的线程继续存放在线程池中等待使用。相比于 Thread线程池ThreadPool对于线程的回收更快,性能更好。

代码看一下性能:

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            for (int i = 0; i < 1000; i++)
            {
                Thread thread = new Thread(() =>
                {
                    Console.WriteLine($"*********************************我是多线程Thread方法 *******************************");
                });
                thread.Start();
            }
            Console.WriteLine($"*********************************Thread方法结束  耗费时间 :{stopwatch.ElapsedMilliseconds}  *******************************");

            Console.WriteLine($"*********************************多线程ThreadPool启动  *******************************");

            Stopwatch stopwatch1 = new Stopwatch();
            stopwatch1.Start();
            for (int i = 0; i < 1000; i++)
            {
                    WaitCallback act = (t) =>
                    {
                        Console.WriteLine($"*********************************我是多线程ThreadPool方法 *******************************");
                    };
                    ThreadPool.QueueUserWorkItem(act);
            }
            Console.WriteLine($"*********************************ThreadPool方法结束  耗费时间 :{stopwatch1.ElapsedMilliseconds}  *******************************");

结果:Thread

  

结果:ThreadPool

  

上面的两个方法我们都只是输出一行字符,但是以1000次的来说看一下性能相差有多少。所以建议大家都使用线程池。

推荐官方文档:https://docs.microsoft.com/zh-cn/dotnet/standard/threading/?view=netframework-4.7.2

线程池:Task

随着框架的发展我们有了Task 他也是基于ThreadPool来重新封装的。他的出现方便了我们对多线程的回调等待更好的操作。

推荐一篇博客:https://www.cnblogs.com/lonelyxmas/p/9509298.html

并行计算的多线程 Parallel

Parallel 多线程,这个类似同步的,他是在Task的基础之上又一次的封装。假如说我们启动多个线程,她像其他的一样启动了很多的子线程去执行的,而是和当前线程一样并行去执行的。并且当前线程也参与执行。他会卡住线程,等到全部执行完毕后才会继续

这个操作上不如Task 灵活,比如Task 可以等待其中一个线程执行完成后继续主线程,Parallel 是必须等待全部执行完毕。

Parallel里面大致分为三个方法: For,ForEach,Invoke

Invoke:

            Console.WriteLine($"*********************************我是主线程线程 ID:{Thread.CurrentThread.ManagedThreadId} *******************************");
            Action act = () => {

                Console.WriteLine($"我的线程ID是:{Thread.CurrentThread.ManagedThreadId}");
            };
            Parallel.Invoke(act, act, act, act, act);

结果:

  

上面结果中我们可以看到主线程参与了进来。

ForEach:

        List<int> vs = new List<int>() { 1, 2, 3, 4, 5 };
            Parallel.ForEach<int>(vs, t =>
            {
                Console.WriteLine($"*********************************我是  {t} *******************************");
            });

结果:

  

For

       List<int> vs = new List<int>() { 1, 2, 3, 4, 5 };
            //从零开始 循环多少次
            Parallel.For(0, vs.Count,t=> {
                Console.WriteLine($"*********************************我是  {t} *******************************");
            });

结果:

  

上面的代码中我们可以看到Parallel 适合在我们循环的时候去使用这样并行的去执行,我们可以减少程序的执行时间。

如果当我们需要执行的集合过大有可能会 并行很多线程时我们怕会影响我们计算机的I/O 我们还可以设置最大的并行数防止程序执行时i/o风暴

       //设置 Parallel 最大并行线程的数量
            ParallelOptions options = new ParallelOptions();
            //最大并行数为10;
            options.MaxDegreeOfParallelism = 10;

Parallel 官方介绍:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.parallel?redirectedfrom=MSDN&view=netframework-4.7.2

获取当前计算机 最大线程数

        int workerThreads;
            int completionPortThreads;
            ThreadPool.GetMaxThreads( out workerThreads, out completionPortThreads);
            Console.WriteLine($"最大工作线程:{workerThreads} 最大I/O线程:{completionPortThreads} ");

            ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);
            Console.WriteLine($"最小工作线程:{workerThreads} 最小 I/O线程:{completionPortThreads} ");

结果:

  

有不足之处 希望大家指出相互学习,

            转载请注明出处 谢谢!

 

posted @ 2019-04-15 18:35  乐途  阅读(798)  评论(2编辑  收藏  举报