当前位置: 首页 > article >正文

C#异步多线程——ThreadPool线程池

C#实现异步多线程的方式有多种,以下总结的是ThreadPool的用法。

线程池的特点

线程池受CLR管理,线程的生命周期,任务调度等细节都不需要我们操心了,我们只需要专注于任务实现,使用ThreadPool提供的静态方法把我们的任务添加到任务队列中,剩下的请放心交给CLR。

  • 线程重用:和其它池化资源有着共同特点,当创建某个对象,创建和销毁代价太高得情况,而且这个对象又可以反复利用,往往我们可以准备一个容器,这个容器可以保存一批这样的对象,如果需要使用这个对象,不需要创建,可以去池中去拿,拿到就用,用完再放回池中,这样就减少了创建和销毁的开销,性能可以得到提升。
  • ThreadPool使用简单,没有直接操作线程的API,例如暂停,恢复,销毁线程,设置前台后台线程(默认都是后台线程),设置优先级等。
  • 有线程数限制,无法无限使用。

环境准备

Visual Studio 创建测试项目,我使用的是Windows Forms App(.NET Framework)模板创建。简单的添加几个测试按钮,在Click事件中进行简单测试。并且准备一段模拟非常耗时操作的代码,例如:

private void DoSomethingLong(string name)
{
    Console.WriteLine($"********** DoSomethingLong Start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");

    long lResult = 0;
    //2000000000? 能看出明显延时就好
    for (int i = 0; i < 2000000000; i++)
    {
        lResult += i;
    }

    Console.WriteLine($"********** DoSomethingLong   End {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

认识下ThreadPool

两种常用启动方式

  • WaitCallback:本质是一个委托,没有返回值,有一个object类型的参数。如果任务不需要传入参数可以使用:public static bool QueueUserWorkItem(WaitCallback callBack)
  • 如果需要传入参数可以使用另外一个重载方法,传入的object类型参数会自动传入到WaitCallback委托中的object参数中:public static bool QueueUserWorkItem(WaitCallback callBack, object state)
private void BtnThreadPool_Click(object sender, EventArgs e)
{
    Console.WriteLine("");
    Console.WriteLine($"********** BtnThreadPool_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");

    WaitCallback waitCallbackWithoutParam = state => Console.WriteLine($"waitCallbackWithoutParam {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
    ThreadPool.QueueUserWorkItem(waitCallbackWithoutParam);

    WaitCallback waitCallbackWithParam = state => Console.WriteLine($"waitCallbackWithParam state={state} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
    ThreadPool.QueueUserWorkItem(waitCallbackWithParam, "ParameterizedThreadPool");
    Console.WriteLine($"********** BtnThreadPool_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

输出结果:
在这里插入图片描述

设置&获取线程数

  • SetMaxThreads:设置最大线程数:
  • SetMinThreads:设置最小线程数:
  • GetMaxThreads:获取最大线程数:
  • GetMinThreads:获取最小线程数:
private void BtnThreadPoolInfo_Click(object sender, EventArgs e)
{
    {
        //默认线程数
        ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);
        ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);
        Console.WriteLine($"默认线程数:workerThreadsMax={workerThreadsMax} workerThreadsMin={workerThreadsMin}");
    }
    {
        //设置线程数
        ThreadPool.SetMaxThreads(16, 16);    //设置最大线程数
        ThreadPool.SetMinThreads(12, 12);    //设置最小线程数
        ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);
        ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);
        Console.WriteLine($"设置线程数:workerThreadsMax={workerThreadsMax} workerThreadsMin={workerThreadsMin}");
    }
}

输出结果:
在这里插入图片描述
我用的电脑是6核12线程的,这个分配是不是很合理?所以CLR是很“聪明”的。如果没有什么极端的需求通常我们不需要自作聪明的去设置他们;

线程等待

ThreadPool没有提供等待线程完成的API,可以使用ManualResetEvent:

private void BtnThreadPoolWait_Click(object sender, EventArgs e)
{
    Console.WriteLine("");
    Console.WriteLine($"********** BtnThreadPoolWait_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");

    ManualResetEvent manualResetEvent = new ManualResetEvent(false);

    ThreadPool.QueueUserWorkItem(state =>
    {
        this.DoSomethingLong("BtnThreadPoolWait_Click");
        manualResetEvent.Set();
    });


    for (int i = 0; i < 1000000000; i++)
    {

    }
    Console.WriteLine($"********** 主线程可以先干点别的 {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
    manualResetEvent.WaitOne();

    Console.WriteLine($"********** BtnThreadPoolWait_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

输出结果:
在这里插入图片描述

为什么死锁了?

看下下面使用线程池发生死锁的例子:

private void BtnThreadPoolDeadLock_Click(object sender, EventArgs e)
{
    Console.WriteLine("");
    Console.WriteLine($"********** BtnThreadPoolDeadLock_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");

    ThreadPool.SetMaxThreads(16, 16);
    ManualResetEvent manualResetEvent = new ManualResetEvent(false);

    for (int i = 0; i < 20; i++)
    {
        int k = i;
        ThreadPool.QueueUserWorkItem(state =>
        {
            Console.WriteLine($"k={k} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
            if (k < 18)
            {
                manualResetEvent.WaitOne();
            }
            else
            {
                manualResetEvent.Set();
            }
        });
    }

    if (manualResetEvent.WaitOne())
    {
        Console.WriteLine("没有死锁");
    }

    Console.WriteLine($"********** BtnThreadPoolDeadLock_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

输出结果:
在这里插入图片描述

  • 程序开始最大线程数被设置为16,线程池中所有线程都在阻塞等待信号,导致发生死锁,程序已经卡死,因为没有线程可用了,发送信号的任务无法执行。
  • 一般不要阻塞线程池的线程。
  • 不要把最大线程数设置的很小,使用默认即可。

如何回调

上一篇文章我们讲委托的异步调用时,提到BeginInvoke可以传入回调,在线程执行完后自动执行这个回调,可是ThreadPool并没有提供传入回调的API,我们可以自己动手去封装一个,只要启动线程时先完成线程任务委托的调用,再执行回调就可以了,例如:

private void ThreadPoolWithCallback(Action act, Action callback)
{
    ThreadPool.QueueUserWorkItem(state =>
    {
        act.Invoke();
        callback.Invoke();
    });
}

private void BtnThreadPoolWithCallback_Click(object sender, EventArgs e)
{
    this.ThreadPoolWithCallback(() =>
    {
        Console.WriteLine($"这里是Action Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
        for (int i = 0; i < 2000000000; i++)
        {

        }
        Console.WriteLine($"这里是Action   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
    }, () =>
    {
        Console.WriteLine($"这里是Callback Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
        for (int i = 0; i < 2000000000; i++)
        {

        }
        Console.WriteLine($"这里是Callback   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
    });
}

输出结果:
在这里插入图片描述

如何获取返回值

上一篇文章我们讲委托的异步调用时,提到EndInvoke可以等待线程结束并获取返回值,可是ThreadPool没有提供获取线程返回值的API,传入的委托都是无返回值的,我们可以自己封装,先看下有问题的封装:

private T ThreadPoolWithReturn<T>(Func<T> func)
{
    T t = default(T);
    ManualResetEvent manualResetEvent = new ManualResetEvent(false);

    ThreadPool.QueueUserWorkItem(state =>
    {
        t = func.Invoke();
        manualResetEvent.Set();
    });

    manualResetEvent.WaitOne();
    return t;
}

private void BtnThreadPoolWithReturn_Click(object sender, EventArgs e)
{
    int result = this.ThreadPoolWithReturn<int>(() =>
    {
        Console.WriteLine($"这里是func Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
        for (int i = 0; i < 2000000000; i++)
        {

        }
        Console.WriteLine($"这里是func   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
        return DateTime.Now.Millisecond;
    });

    Console.WriteLine($"result={result} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
}

输出结果:
在这里插入图片描述
应该看出来虽然拿到了结果,但仔细看这个异步是“假异步”,线程刚启动就开始阻塞,等待线程拿到结果,这不和同步一样了吗?这里好像有点矛盾,又要结果,又不想阻塞,是不可能的,要结果就要等计算完成,但这个等待的时机我们可以好好斟酌一下,没必要子线程刚启动,主线程就阻塞等待,我们可以在子线程启动后,主线程先干点别的,等主线程真正要拿结果的时候再等待。那要如何实现?可以在子线程启动后返回一个委托,等需要拿结果的时候我们再调用这个委托。

private Func<T> ThreadPoolWithReturn<T>(Func<T> func)
{
    T t = default(T);
    ManualResetEvent manualResetEvent = new ManualResetEvent(false);

    ThreadPool.QueueUserWorkItem(state =>
    {
        t = func.Invoke();
        manualResetEvent.Set();
    });

    //返回委托在这里可以立即结束不需要阻塞,只有要结果的时候才会等待完成
    //因为这个委托中持有上下文环境比如 manualResetEvent 比如t,所以外部调用委托的时候可以拿到这些对象
    return () =>
    {
        manualResetEvent.WaitOne();
        return t;
    };
}

private void BtnThreadPoolWithReturn_Click(object sender, EventArgs e)
{
    Console.WriteLine("");
    Console.WriteLine($"********** BtnThreadPoolWithReturn_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");

    Func<int> func = this.ThreadPoolWithReturn<int>(() =>
    {
        Console.WriteLine($"这里是func Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
        for (int i = 0; i < 2000000000; i++)
        {

        }
        Console.WriteLine($"这里是func   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
        return DateTime.Now.Millisecond;
    });

    //假如拿结果前做了一个耗时的计算
    for (int i = 0; i < 1000000000; i++)
    {

    }
    Console.WriteLine($"主线程拿结果前可以在这里干点别的... {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");

    int result = func.Invoke();
    Console.WriteLine($"result={result} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy - MM - dd HH: mm:ss.fff")}");
    Console.WriteLine($"********** BtnThreadPoolWithReturn_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
}

输出结果:
在这里插入图片描述

  • 返回委托并不会阻塞,只有在调用这个委托才会真正等待结果。
  • 拿结果前主线程可以和子线程可以并发运行。

http://www.kler.cn/a/471883.html

相关文章:

  • Mysql - 多表连接和连接类型
  • Redis 数据库源码分析
  • Solidity合约编写(五)
  • 二、模型训练与优化(1):构建并训练模型
  • 代码随想录day38 动态规划6
  • Ollama + FastGPT搭建本地私有企业级AI知识库 (Linux)
  • 分享:osgb倾斜数据转cesium-3dtiles 小工具.
  • 计算机网络之---有线网络的传输介质
  • STM32-WWDG/IWDG看门狗
  • 海陵HLK-TX510人脸识别模块 stm32使用
  • 常见的开源网络操作系统
  • 如何很快将文件转换成另外一种编码格式?编码?按指定编码格式编译?如何检测文件编码格式?Java .class文件编码和JVM运行期内存编码?
  • 关于Mac中的shell
  • RP2K:一个面向细粒度图像的大规模零售商品数据集
  • 使用ML.NET进行对象检测
  • opencv摄像头标定程序实现
  • Go语言的语法
  • 会员制营销与门店业绩提升:以开源AI智能名片S2B2C商城小程序为例的深度剖析
  • 基于微信小程序的考研资料分享系统的设计与实现springboot+论文源码调试讲解
  • 【阅读】认知觉醒
  • Mermaid 使用教程之流程图 - 从入门到精通
  • 2025新春烟花代码(一)HTML5夜景放烟花绽放动画效果
  • 基于Thinkphp6+uniapp的陪玩陪聊软件开发方案分析
  • flutter web 路由问题
  • 【Qt】C++11 Lambda表达式
  • C语言文件学习