专栏首页喵叔's 专栏搞懂线程池(一)

搞懂线程池(一)

创建线程是一个很代价很高的操作,每个异步操作创建线程都会对 CPU 产生显著的性能影响。为了解决这个问题我们引入了线程池的概念,所谓的线程池就是我们提前分配一定的资源,把这些资源放在资源池中,每次需要用到的使用从里面取出一个,用完后再放回去。线程池一般用在需要创建大量的短暂的且开销大的资源里。.NET 中的线程池位于 System.Threading.ThreadPool 类,它接受 CLR 的管理。 ThreadPool 类中拥有一个 QueueUserWorkItem 方法,该方法为静态方法。它接受一个委托,表示用户定义的异步操作。在方法被调用后,委托会进入到内部队列中。如果池中没有任何线程,将创建一个新的 Worker Thread (工作者线程)并将队列中第一个委托放入到该 Work Thread 中。 这里有一点要注意,当有新的操作加入到线程池里时,如果之前的操作完成了,那么这个新的操作将会重用线程来执行。但是如果新的操作加入线程池的太快太多,那么线程池将会创建更多的线程来执行操作。然后创建的线程数量是有限制的,达到限制的数量后,以后加进来的操作将会在队列中等待线程被放回线程池并有能力执行它们。当没有任何操作进入线程池中时,线程池会释放掉超过过期时间的线程,以减少操作系统和 CPU 的压力。

Tip:

  1. 一定不要在线程池中放入长时间运行的操作或者放入会阻塞线程的操作,这样会导致严重的性能问题和莫名其妙的bug。
  2. 线程池中的所有线程都是后台线程,当应用程序中的所有前台线程完成后后台线程也就停止工作,即使它还没有完成所作的工作。

零、 线程池中的 APM 和委托

所谓 APM 是异步编程模型,他是一种模式,该模式允许用更少的线程去做更多的操作,.NET Framework 很多类也实现了该模式。我们也可以在自定义的类中实现返回类型为 IAsyncResult 接口的 BeginXXX 方法 和 EndXXX 方法 。委托类型也定义了 BeginInvoke 和 EndInvoke 方法。下面我们即通过一个例子来看一下在线程池中怎么使用 APM 和委托。

using System;
using System.Threading;
using static System.Threading.Thread;
using static System.Console;

namespace APMAndInvoke
{
    class Program
    {
        static void Main(string[] args)
        {
            int threadId = 0;
            ThreadHotel threadHotel = Cooking;
            IAsyncResult asyncResult = threadHotel.BeginInvoke(out threadId, CallAttendant, "呼叫服务员!!");
            string result = threadHotel.EndInvoke(out threadId, asyncResult);
            WriteLine(result);
            asyncResult = threadHotel.BeginInvoke(out threadId, CallAttendant, "呼叫服务员!!");
            result = threadHotel.EndInvoke(out threadId, asyncResult);
            WriteLine(result);
            Read();
        }

        private delegate string ThreadHotel(out int threadId);
        private static void CallAttendant(IAsyncResult asyncResult)
        {
            WriteLine(asyncResult.AsyncState);
            WriteLine($"我是厨师 {CurrentThread.ManagedThreadId} 号,饭做好了。");
        }
        private static string Cooking(out int threadId)
        {
            WriteLine($"当前厨师是否是 ThreadHotel 的员工  {CurrentThread.IsThreadPoolThread}");
            WriteLine($"厨师编号:{CurrentThread.ManagedThreadId}");
            WriteLine("开始做饭!!!!");
            WriteLine("完成做饭");
            threadId = CurrentThread.ManagedThreadId;
            return $"厨师 {threadId} 做的饭!";
        }
    }
}

上面我们模拟了一个厨师做饭的过程。首先我们定义了一个委托 ThreadHotel ,接着调用 BeginInvoke 方法来运行委托。 BeginInvoke 方法接受一个回调函数,回调函数会在异步执行完成后被调用,并且我们传递了一个字符串到回调函数内(这个字符串是一个自定义状态,我们在这里不仅可以传递字符串还可以传递任何 object 类型的数据)。 BeginInvoke 将返回实现了 IAsyncResult 接口的对象,可用于检测异步调用的过程。当操作完成时 BeginInvoke 的回调函数会进入到线程池中等待空闲的线程调用。之后我们通过 EndInvoke 方法获取异步调用的结果。如果异步调用尚未完成,EndInvoke 将阻塞调用线程直到它完成。 EndInvoke 方法可以将异步操作中未处理的异常抛出到调用线程中,因此我们在使用异步时必须要调用 Begin 和 End 方法。

一、异步操作

当我们需要在线程池中加入异步操作时,通过 ThreadPool.QueueUserWorkItem 方法即可实现线程池异步操作。 QueueUserWorkItem 有两个重载,分别是 QueueUserWorkItem(WaitCallback)QueueUserWorkItem(WaitCallback, Object) 。这两种重载都传入了一个要执行的方法,这个方法将加入到线程池的队列中,当有空闲的线程时,空闲线程将调用这个方法。第二个重载将需要执行的方法的必要参数传入了进来。下面我们依然通过一个简单的例子来看一下。

using System;
using System;
using System.Threading;
using static System.Console;
using static System.Threading.Thread;


namespace AsynchronousOperation
{
    class Program
    {
        static void Main(string[] args)
        {
            WriteLine("服务员:您吃点什么");
            //顾客点的餐
            string[] dishes = new string[] { "醋溜白菜", "水煮肉片", "疙瘩汤" };
            WriteLine($"顾客:我要吃 {string.Join("、",dishes)}");
            //服务员下单
            ThreadPool.QueueUserWorkItem(Cooking, dishes);
            Sleep(2000);
            //顾客加菜
            string[] addVegetables = new string[] { "拍黄瓜", "白菜豆腐汤" };
            WriteLine($"顾客:服务员再来份 {string.Join("、",addVegetables)}");
            //服务员再次下单
            ThreadPool.QueueUserWorkItem(Cooking, addVegetables);
            //服务员错误下单
            ThreadPool.QueueUserWorkItem(Cooking);
            Read();

        }

        static void Cooking(object state)
        {
            int threadId = CurrentThread.ManagedThreadId;
            if (state==null)
            {
                WriteLine($"{threadId} 号厨师:空菜单?WTF");
                return;
            }
            string[] dishes = (string[])state;

            WriteLine($"本店厨师:{threadId} 号开始炒菜");
            for (int i = 0; i < dishes.Length; i++)
            {
                WriteLine($"厨师 {threadId} 号开始制作 {dishes[i]}");
                Sleep(2000);
                WriteLine($"厨师 {threadId} 号制作 {dishes[i]} 完成");
            }
        }
    }
}

上述代码我们模拟了顾客点餐、厨师做饭和顾客加菜的过程。首先我们定义了 Cooking 方法来模拟厨师做菜,在方法中通过 Sleep 来模拟厨师做每一道菜的时间。之后我们在 Main 方法里通过 ThreadPool.QueueUserWorkItem 方法将顾客第一次点餐的内容传入 Cooking 中。如果存在空闲的厨师(线程),那么空闲的厨师开始就开始接单做饭。接着我们通过 Sleep 方法来暂停 2 秒,然后我们再次通过 ThreadPool.QueueUserWorkItem 方法将顾客所加的菜传入 Cooking 方法中。这时如果上一个做菜的厨师空闲下来了,那么它将接单继续做饭,反之由其他厨师接单做饭。从上面的代码中我们可以看出,虽然我们两次点餐之间暂停了 2 秒但是第一次点单的那个厨师还没有做完所有的饭,因此第二次点单后接单做饭的厨师是另一个厨师。当我们把两次点单的时间间隔变为 20 秒后,第一次点单和第二次点单的接单做饭的厨师都是同一个厨师了。前面的代码我们使用的是闭包机制,我们也可以使用传递 lambda 表达式的形式。如下代码:

ThreadPool.QueueUserWorkItem(state =>
{
    int threadId = CurrentThread.ManagedThreadId;
    string[] lambd_dishes = (string[])state;

    WriteLine($"本店厨师:{threadId} 号开始炒菜");
    for (int i = 0; i < lambd_dishes.Length; i++)
    {
        WriteLine($"厨师 {threadId} 号开始制作 {lambd_dishes[i]}");
        Sleep(2000);
        WriteLine($"厨师 {threadId} 号制作 {lambd_dishes[i]} 完成");
    }
},dishes);

我们不推荐在线程池中使用 lambda 表达式的方式,首先这样会出现大量重复代码,其次代码一旦很多票很容易造成不易理解的问题。相对来说闭包更加灵活,允许我们传递多个具有静态类型的对象。

二、时间换空间

当我要创建并运行大量的线程时,如果通过 new Thread() 的方式创建,虽然运行耗时很短但是这些线程消耗了大量的操作系统资源。在一些计算密集型的项目中这样会造成整个系统运行缓慢,甚至是操作系统运行缓慢。这时我们可以牺牲时间来换取减少对操作系统资源的占用,这就是所谓的时间换空间。同样我们通过例子来看一下。

using System;
using System.Diagnostics;
using System.Threading;
using static System.Console;
using static System.Threading.Thread;

namespace TimeForSpace
{
    class Program
    {
        static void Main(string[] args)
        {
            var sw = new Stopwatch();
            sw.Start();
            using (var cd = new CountdownEvent(200))
            {
                for (int i = 0; i < 200; i++)
                {
                    Thread thread = new Thread(() =>
                    {
                        Sleep(200);
                        cd.Signal();
                    });
                    thread.Start();
                }
                cd.Wait();

            }
            sw.Stop();
            WriteLine($"普通方式耗时 {sw.ElapsedMilliseconds} 毫秒");
            sw.Reset();
            sw.Start();
            using (var cd = new CountdownEvent(200))
            {
                for (int i = 0; i < 200; i++)
                {
                    ThreadPool.QueueUserWorkItem(_ =>
                    {
                        Sleep(200);
                        cd.Signal();
                    });
                }
                cd.Wait();
            }
            sw.Stop();
            WriteLine($"线程池方式耗时 {sw.ElapsedMilliseconds} 毫秒");
            Read();
        }
    }
}

首先我们以普通方式创建了 200 个线程,通过运行结果我们可以看出普通方式只需 4 秒多就完成了,但是我们通过资源管理器看到资源占用出现了明显的升高。接着我们通过线程池的方式创建并运行 200 个线程,这时运行所有线程需要 6 秒多,但是资源占用明显减少。虽然这种方式可以降低资源占用,但是并不是所有的项目都适合这种的方式,我们要根据项目情况来考虑使用。

三、 代码下载

代码下载

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 线程同步(一)

    当多个线程同时对同一个内存地址进行写入时,由于CPU时间调度上的问题写入数据会被多次的覆盖,所以就要使线程同步。所谓的同步就是协同步调,按预定的先后次序进行运行...

    喵叔
  • 线程基础必知必会(二)

    这篇文章将在上篇文章的基础上,进一步讲解线程的相关知识。这篇文章涉及到的知识点有 线程优先级、前台与后台线程、线程参数、lock、Monitor 和 线程异常处...

    喵叔
  • 线程基础必知必会(一)

    从这篇文章开始,我将利用两篇文章讲解线程的基础知识,本篇文章涉及到了 创建线程、线程等待、线程暂停、线程终止 和 线程状态检测 相关的内容。这篇文章及其下一篇文...

    喵叔
  • 大白话java多线程,高手勿入

    我们看到的这些单独运行的程序就是一个独立的进程,进程之间是相互独立存在的。我们上面图中的360浏览器、百度云盘等等都是独立的进程。

    java金融
  • 大白话Java多线程,小白都能看的懂的哦

    我们看到的这些单独运行的程序就是一个独立的进程,进程之间是相互独立存在的。我们上面图中的360浏览器、百度云盘、云数据库mysql等等都是独立的进程。

    java金融
  • SpringBoot开发案例之多任务并行+线程池处理

    前几篇文章着重介绍了后端服务数据库和多线程并行处理优化,并示例了改造前后的伪代码逻辑。当然了,优化是无止境的,前人栽树后人乘凉。作为我们开发者来说,既然站在了巨...

    小柒2012
  • .NET面试题系列[18] - 多线程同步(1)

    多个线程同时访问共享资源时,线程同步用于防止数据损坏或发生无法预知的结果。对于仅仅是读取或者多个线程不可能同时接触到数据的情况,则完全不需要进行同步。

    s055523
  • 【RT-Thread笔记】临界区问题及IPC机制

    在多线程实时系统中,多个线程操作/访问同一块区域(代码),这块代码就称为临界区。 例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中...

    正念君
  • 操作系统线程描述

    线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条...

    goodspeed
  • 「每天一道面试题」ReentrantLock是如何实现公平锁及可重入的?

    A、B两个线程同时执行lock()方法获取锁,假设A先执行获取到锁,此时state值加1,如果线程A在继续执行的过程中又执行了lock()方法(根据持有锁的线程...

    JavaQ

扫码关注云+社区

领取腾讯云代金券