专业编程基础技术教程

网站首页 > 基础教程 正文

C#中多线程的那点事-线程池

ccvgpt 2024-08-18 14:28:03 基础教程 9 ℃



C#中多线程的那点事-线程池

上一篇《C#中多线程的那点事儿-多线程的代价》,我们通过实例程序,演示了线程的昂贵代价。在代码中,不能滥用多线程,需要合理控制线程的数量,避免CPU频繁的进行线程上下文切换。

聪明的小明同学,昨天听了外老师的讲解之后,意犹未尽的回家,正琢磨着,怎么样使用多线程技术,加速大量小任务的处理速度!


就在今天早晨,小明在早餐店看到了这样一幕:

这是一家非常火爆的早餐店,老板配备了两台收银机,在早高峰的时候,两台收银机全速运行,以提高顾客点单的速度。

小明点了一份热干面,只见收银员将小明的订单递给了第一位小哥,小哥接到订单之后,非常熟练地将适量面条,放入一个锥形的容器中,然后放入身旁的热锅中去煮。

这时候,前面的另一个阿姨,正好取走了放在锅里的另一份面条,快速的取一个碗,倒入面条,加入芝麻酱,还有其他调料,半分钟左右,这一份热面就交到了小明前面的一位小姐姐手中。

然后阿姨就开始做小明的那一碗面条了。。。


小明似乎明白了什么,他兴致勃勃的来到教室,向我讲述他买早餐时的所见,说道:

老师,流水线啊!我们可以让一个线程,处理多个小任务,不就可以避免开启过多线程的问题了吗!这样在提高任务处理速度的同时,也避免了开启过多线程的昂贵成本。如果我们有多种任务,还可以开启多个线程还处理不同的任务。

这时外老师的脸上,洋溢着满意的笑容,对小明说到:小明,你前途无量啊!加油,好好学。


流水线

早餐店的老板,不可能为每一个顾客,雇一个员工为其提供服务。而是雇几个员工,分别负责不同的工序。在比较耗时的工序,通常会增加员工的数量,以保证流水线中的每一个员工都不闲着。

流水线同时还有一大好处,就是每一个员工,只需掌握自己这一个工序的技能,每天重复同样的工作,动作非常的娴熟。员工培养的成本也非常低。还不用担心员工自己出去单干。。。

ThreadPool

这个思路,在软件行业,也同样流行。

我们可以自己将任务分类,分组,将每一组任务,交给一个线程来处理。我们只需要简单的代码,就可以实现这个逻辑。

当然,软件行业还有一个特点,就是通常不需要我们自己造轮子,而是直接使用别人造好的轮子。没错,微软已经为我们提供了现成的线程池实现ThreadPool。我们直接使用即可。

下面进入实例讲解,先回顾一下上一篇文章的示例:

class Data
{
    public Data(int id)
    {
        ID = id;
    }
    public int ID { get; set; }

    public void DoSomeThing()
    {
        ID += 1;
    }
}

static void MultiThread(IList<Data> datas)
{
    foreach (var item in datas)
    {
        var thd = new System.Threading.Thread(
            () => item.DoSomeThing());
        thd.Start();

        // 这里先不管 thd.Join();
        // 也不要求 DoSomeThing 始终执行
    }
}

然后我们使用ThreadPool再写一个测试函数:

static void MultiThreadPool(IList<Data> datas)
{
    foreach (var item in datas)
    {
        ThreadPool.QueueUserWorkItem(
            obj => item.DoSomeThing());
        // 我们无法控制 ThreadPool 中的线程的 开始、挂起、和中止等
        // 这个需要注意的地方
    }
}

我们在两个测试函数中,都只将任务添加到多线程中去执行,先不考虑其执行结果,单纯比较线程的添加效率:

public static void Main()
{
    Console.WriteLine("Hello Thread World!");

    var dts = MakeData();
    var sw = new Stopwatch();
    sw.Start();
    MultiThread(dts);
    sw.Stop();
    var tc1 = sw.ElapsedTicks;
    Console.WriteLine(#34;Multi Thread Time Costs: {tc1}");

    sw.Restart();
    MultiThreadPool(dts);
    sw.Stop();
    var tc2 = sw.ElapsedTicks;
    Console.WriteLine(#34;Thread Pool Time Costs: {tc2}");
    Console.WriteLine(#34;Rate: {tc1/tc2}"); // 加入对比

    Console.ReadKey();
}

编译执行结果如下:



可以看到,ThreadPool的效率,提升了5倍左右。这可能不算特别突出的效率提升。但是当我们把任务的数量提升到1万个,情况会有什么不同吗:

static IList<Data> MakeData()
{
    var dataNum = 10000; // 任务数量
    var datas = new List<Data>(dataNum);
    for (int i = 0; i < dataNum; i++)
    {
        datas.Add(new Data(i));
    }

    return datas;
}

执行结果如下:


可以看到,速度提升已经到了200多倍了。可见任务数量越多,ThreadPool的优势越明显。

最后我们将任务数量调整为20个,并在代码中,加入检测线程ID的代码,然后观察一下线程池中的任务的执行情况:

static IList<Data> MakeData()
{
    var dataNum = 20; // 任务数量
    var datas = new List<Data>(dataNum);
    for (int i = 0; i < dataNum; i++)
    {
        datas.Add(new Data(i));
    }

    return datas;
}

class Data
{
    public Data(int id)
    {
        ID = id;
    }
    public int ID { get; set; }

    public void DoSomeThing()
    {
        ID += 1;
        Console.WriteLine(
            #34;Thread ID:{Thread.CurrentThread.ManagedThreadId}" +
            #34;\t Data ID:{ID}");
    }
}

public static void Main()
{
    Console.WriteLine("Hello Thread World!");

    var dts = MakeData();

    MultiThreadPool(dts);

    Console.ReadKey();
}

执行结果如下:

可以看出,20个任务,分布在8个线程中执行,其中10号线程,执行的任务数量最多。

由此可以看出来,ThreadPool确实可以重复使用线程。其中的线程,和流水线中的工人一样,可以重复做一个工序中的工作。

当然,ThreadPool中的线程,比工人厉害的地方,就是线程可以完成各种各样的工序,哪个工序需要,就去哪个环节干活。

所以使用ThreadPool,比现实中的流水线,可以更加灵活,因为每一个工人都是全能选手。

使用ThreadPool注意事项

我们无法控制ThreadPool中的线程的开始、挂起、和中止。

长时间运行在线程,不宜放在ThreadPool中,而是应该单独开启线程,否则会导致线程池被长期占用,而无法执行其他线程。

本文中的示例,可能并不完全合适。大家在实际的工作中,要学会举一反三,触类旁通,灵活运用。

终于讲完了!我看了一眼小明,他又在看着我点头!

系列文章

下面是给同学们准备的干货:

C#中多线程的那点事儿-Thread入门

C#中多线程的那点事-多线程的代价

C#中多线程的那点事-锁

C#中多线程的那点事-死锁

C#中多线程的那点事-Task再次起航

C#中多线程的那点事-Parallel

C#中多线程的那点事-Linq & PLinq

C#中多线程的那点事-async & await

Tags:

最近发表
标签列表