服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - C# 异步多线程入门到精通之ThreadPool篇

C# 异步多线程入门到精通之ThreadPool篇

2022-12-09 12:43菜鸟厚非 C#

ThreadPool 是 .net 2.0 时代的产物,有了 Thread 为什么还会有 ThreadPool 呢?ThreadPool 可以做到限制线程数量、重用线程

上一篇:C# 异步多线程入门到精通之Thread篇
下一篇:异步多线程之入Task,待更新

启动线程池线程

ThreadPool 提供的 API 相对于 Thread 是比较少的,在 ThreadPool 中需使用 QueueUserWorkItem 方法,来启动一个线程

例如:Dosome 是个普通的方法,传入 QueueUserWorkItem 方法开启新线程执行此方法

public static void Dosome()
{
  Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
  Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  ThreadPool.QueueUserWorkItem(x => Dosome());

  Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Console.ReadLine();
}

启动线程,可以看到新开启了一个子线程 3 执行任务,而主线程 1 并没有等待子线程 3

C# 异步多线程入门到精通之ThreadPool篇

 

线程池线程数量

在 1.0 时代的 Thread 是没有线程数量概念的,在 ThreadPool 2.0 时代,线程池线程数量可以通过 SetMaxThreads、SetMaxThreads 方法设置最小最大线程。也可以查看线程池线程数量,以通过 GetMinThreads、GetMaxThreads 方法获取线程池最小及最大线程数量。

注意:一般不建议设置 ThreadPool 线程数量,这个操作是全局的。二般情况,当线程池线程耗尽,会造成死锁。

例如:以通过 SetMaxThreads、SetMaxThreads、GetMinThreads、GetMaxThreads 方法来操作查看线程

{
  ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);//工作线程,io线程
  Console.WriteLine($"【default】最小 workerThreadsMin:{workerThreadsMin}  completionPortThreadsMin:{completionPortThreadsMin}");

  ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);//工作线程,io线程
  Console.WriteLine($"【default】最大 workerThreadsMax:{workerThreadsMax}  completionPortThreadsMax:{completionPortThreadsMax}");
}

ThreadPool.SetMinThreads(3, 3); // 设置4其实也不是4,应为本机为逻辑八核,最小也就是这个
ThreadPool.SetMaxThreads(7, 7);

{
  ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);//工作线程,io线程
  Console.WriteLine($"【自定义】最小 workerThreadsMin:{workerThreadsMin}  completionPortThreadsMin:{completionPortThreadsMin}");

  ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);//工作线程,io线程
  Console.WriteLine($"【自定义】最大 workerThreadsMax:{workerThreadsMax}   completionPortThreadsMax:{completionPortThreadsMax}");
}

ThreadPool.SetMinThreads(5, 5); // 设置4其实也不是4,应为本机为逻辑八核,最小也就是这个
ThreadPool.SetMaxThreads(16, 16);

{
  ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);//工作线程,io线程
  Console.WriteLine($"【自定义】最小 workerThreadsMin:{workerThreadsMin}  completionPortThreadsMin:{completionPortThreadsMin}");

  ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);//工作线程,io线程
  Console.WriteLine($"【自定义】最大 workerThreadsMax:{workerThreadsMax}   completionPortThreadsMax:{completionPortThreadsMax}");
}

C# 异步多线程入门到精通之ThreadPool篇

 

线程池线程等待

看了前面 ThreadPool 相关的讲解,有小伙伴可能会发现,我们一直没有说等待线程,那 ThreadPool 有相关的 API 吗?答案:没有

但可以通过 ManualResetEvent 方式实现线程等待。一般来说不建议线程等待,二般情况也不建议。应为线程池里面,线程数量有限,写代码无意间造成的线程等待没有释放,一旦线程池线程耗尽就会形成死锁。除非非得等待情况,但记得一定要释放等待,多多检查代码。

例如:线程等待,ManualResetEvent 初始化为 false,Set() 方法会设置为 true,WaitOne() 方法会检查 ManualResetEvent 对象是否为 true,如果不为会一直等待,如果为 true 会直接过去

public static void Dosome()
{
  Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
  Thread.Sleep(5 * 1000); // 模拟任务耗时
  Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
}

static void Main(string[] args)
{
  Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  ManualResetEvent manualResetEvent = new ManualResetEvent(false);
  ThreadPool.QueueUserWorkItem(x =>
  {
      Dosome();
      manualResetEvent.Set(); // 会变成 true
  });
  manualResetEvent.WaitOne();

  Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Console.ReadLine();
}

启动程序,可以看到主线程 1 等待 子线程 3 执行完成后,在执行了 Main 方法结束代码

C# 异步多线程入门到精通之ThreadPool篇

例如:线程耗尽形成死锁,首先对线程池线程数量进行了限制,最大为 10 个线程。接着我们循环启动 18 个线程工作,且让前 18 个线程形成等待。

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadPool.SetMinThreads(4, 4);
ThreadPool.SetMaxThreads(10, 10);
ThreadPool.GetMinThreads(out int workerThreadsMin, out int completionPortThreadsMin);//工作线程,io线程
Console.WriteLine($"【自定义】最小 workerThreadsMin:{workerThreadsMin}  completionPortThreadsMin:{completionPortThreadsMin}");
ThreadPool.GetMaxThreads(out int workerThreadsMax, out int completionPortThreadsMax);//工作线程,io线程
Console.WriteLine($"【自定义】最大 workerThreadsMax:{workerThreadsMax}   completionPortThreadsMax:{completionPortThreadsMax}");

ManualResetEvent manualResetEvent = new ManualResetEvent(false);
for (int i = 0; i < 20; i++)
{
   int k = i;
   ThreadPool.QueueUserWorkItem((x) =>
   {
       Console.WriteLine(k);
       if (k < 18)
       {
           manualResetEvent.WaitOne();
       }
       else
       {
           manualResetEvent.Set();
       }
   });
}
manualResetEvent.WaitOne();

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

启动程序,可以看到,当开启 10 个线程后,程序就已经不再运行了。这是当程循环开启第 11 个子线程时,发现线程池里面没有线程了,就会一直等待,这样一个状态就是死锁。

C# 异步多线程入门到精通之ThreadPool篇

 

线程回调

讲到现在,细心的小伙伴会发现一直没有说线程回调,即当子线程执行一个任务完成后,再执行一个任务。其实 Thread 与 ThreadPool 都没有回调,但是可以创造出 Callback,那就是包一层,如果不行那就再包一层。

 

Thread

例如:创建一个普通方法 ThreadWithCallback 传入两个委托参数,一个实际任务,一个 Callback。接着在内部使用 Thread 开启一个新的线程,执行 action、callback 方法。

private static void ThreadWithCallback(Action action, Action callback)
{
  Thread thread = new Thread(() =>
  {
      action.Invoke();
      callback.Invoke();
  });
  thread.Start();
}

static void Main(string[] args)
{
  Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  ThreadWithCallback(() =>
  {
      Console.WriteLine($"action,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
  }, () =>
  {
      Console.WriteLine($"callback,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
  });

  Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Console.ReadLine();
}

启动程序,可以看到 action 执行后,再执行了 callback

C# 异步多线程入门到精通之ThreadPool篇

 

ThreadPool

例如:创建一个普通方法 ThreadWithCallback 传入两个委托参数,一个实际任务,一个 Callback。接着在内部使用 ThreadPool 开启一个新的线程,执行 action、callback 方法。

private static void ThreadWithCallback(Action action, Action callback)
{
  ThreadPool.QueueUserWorkItem(x =>
  {
      action.Invoke();
      callback.Invoke();
  });
}

static void Main(string[] args)
{
  Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  ThreadWithCallback(() =>
  {
      Console.WriteLine($"action,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
  }, () =>
  {
      Console.WriteLine($"callback,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
  });

  Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Console.ReadLine();
}

启动程序,可以看到 action 执行后,再执行了 callback

C# 异步多线程入门到精通之ThreadPool篇

 

线程返回值

讲到现在,细心的小伙伴会发现一直没有说线程返回值,在 1.0、2.0 时代的 Thread、ThreadPool 是没有提供相关 API 的。但是可以创造出来,还是包一层,如果不行那就再包一层。

 

Thread

例如:创建一个普通方法 ThreadWithReturnCallback 返回与入参都是 Func< T >,内部启用一个 Thread 执行委托,return 一个带返回值的委托且 Thread 的线程等待放置里面。使用时给 ThreadWithReturnCallback 方法传入带返回值的委托即可,因为 ThreadWithReturnCallback 方法返回值也是委托,所以要想获得结果需要在外部 Invoke 一下,这个 Invoke 操作会卡主线程。

private static Func<T> ThreadWithReturnCallback<T>(Func<T> func)
{
  T t = default(T);
  Thread thread = new Thread(() =>
  {
      t = func.Invoke();
  });
  thread.Start();

  return () =>
  {
      thread.Join();
      return t;
  };
}

static void Main(string[] args)
{
  Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Func<int> func = ThreadWithReturnCallback<int>(() =>
  {
      return DateTime.Now.Millisecond;
  });

  int iResult = func.Invoke();
  Console.WriteLine(iResult);

  Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Console.ReadLine();
}

C# 异步多线程入门到精通之ThreadPool篇

 

ThreadPool

例如:创建一个普通方法 ThreadPoolWithReturnCallback 返回与入参都是 Func< T >,使用 QueueUserWorkItem 方法启动线程执行委托,因为 ThreadPool 本身并未提供线程等待方法,所以这里使用 ManualResetEvent 进行线程等待,return 一个带返回值的委托且 ManualResetEvent WaitOne 线程等待放置里面。使用时给 ThreadPoolWithReturnCallback 方法传入带返回值的委托即可,因为 ThreadPoolWithReturnCallback 方法返回值也是委托,所以要想获得结果需要在外部 Invoke 一下,这个 Invoke 操作会卡主线程。

private static Func<T> ThreadPoolWithReturnCallback <T>(Func<T> func)
{
  T t = default(T);

  ManualResetEvent manualResetEvent = new ManualResetEvent(false);

  ThreadPool.QueueUserWorkItem(x =>
  {
      t = func.Invoke();
      manualResetEvent.Set(); // 会变成 true
  });

  return () =>
  {
      manualResetEvent.WaitOne();    
      return t;
  };
}

static void Main(string[] args)
{
  Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Func<int> func = ThreadPoolWithReturnCallback <int>(() =>
  {
      return DateTime.Now.Millisecond;
  });

  int iResult = func.Invoke();
  Console.WriteLine(iResult);

  Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Console.ReadLine();
}

C# 异步多线程入门到精通之ThreadPool篇

 

线程池线程重用

在 1.0 时代的 Thread 每次创建实例都会向操作系统申请线程,2.0 时代的 ThreadPool 每次使用 QueueUserWorkItem 都会向线程池拿取线程,并不会向操作系统申请线程。所以,使用 ThreadPool 创建线程的效率是高于 Thread 的。

例如:我们开启三波线程执行任务,执行相同的任务

static void Main(string[] args)
{
  Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}\n");

  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"张三,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"李四,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"王五,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"麻溜,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });

  Thread.Sleep(1000);Console.WriteLine();

  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"张三,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"李四,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"王五,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"麻溜,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });

  Thread.Sleep(1000); Console.WriteLine();

  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"张三,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"李四,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"王五,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}"); });
  ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"麻溜,任务处理完成。ThreadId:{Thread.CurrentThread.ManagedThreadId}\n"); });

  Thread.Sleep(1000);

  Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

  Console.ReadLine();
}

启动程序,第一波的时候启用了 3、4、6、7,第二波重用了第一波的 6、7、第三波重用了第一、第二波的 3、4、5、8。其中未重用的呢,是线程并未回收(回收需要时间),所以未重用。

C# 异步多线程入门到精通之ThreadPool篇

 

扩展知识-委托线程

委托的 BeginInvoke 方法使用的是线程池的线程,在任务执行完成后,子线程时不会被立马回收的,除非调用 EndInvoke 可以立马结束子线程回到线程池,利于线程更好的重用。

例如:BeginInvoke 线程不能立马被回收重用

static void Main(string[] args)
{
  Action<int> action = x =>
  {
      Console.WriteLine($"我是 {x},Thread:{Thread.CurrentThread.ManagedThreadId}");
  };

  for (int i = 0; i < 5; i++)
  {
      action.BeginInvoke(i,null,null);
  }

  Console.ReadLine();
}

启动线程,并发五次,分别启用了4、5、7、8、9,五个线程

C# 异步多线程入门到精通之ThreadPool篇

例如:EndInvoke 线程更好重用,BeginInvoke 方法的第二个参数

static void Main(string[] args)
{
  Action<int> action = x =>
  {
      Console.WriteLine($"我是 {x},Thread:{Thread.CurrentThread.ManagedThreadId}");
  };

  for (int i = 0; i < 5; i++)
  {
      action.BeginInvoke(i, e => { action.EndInvoke(e); }, null);
  }

  Console.ReadLine();
}

启程序,可以看到并发 5 次只使用了,线程 3 与 8。

C# 异步多线程入门到精通之ThreadPool篇

到此这篇关于C# 异步多线程入门到精通之ThreadPool篇的文章就介绍到这了,更多相关C# ThreadPool内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/weixin_46785144/article/details/121104117

延伸 · 阅读

精彩推荐
  • C#C#中WinForm控件的拖动和缩放的实现代码

    C#中WinForm控件的拖动和缩放的实现代码

    本篇文章主要介绍了C#中WinForm控件的拖动和缩放的实现代码,C# WinForm控件的拖动和缩放是个很有用的功能,有兴趣的可以了解一下。...

    十日十乞00110372021-12-22
  • C#C#使用Windows Service的简单教程(创建、安装、卸载、调试)

    C#使用Windows Service的简单教程(创建、安装、卸载、调试)

    这篇文章主要为大家详细介绍了C#创建、安装、卸载、调试Windows Service(Windows 服务)的简单教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    邪心魔佛一页书6762021-12-16
  • C#QT、WPF、PyQt、Electron 桌面应用解决方案

    QT、WPF、PyQt、Electron 桌面应用解决方案

    Qt是一个1991年由Qt Company开发的跨平台C++图形用户界面应用程序开发框架。本文为大家分享下QT、WPF、PyQt、Electron 桌面应用解决方案,有需要的朋友可以参考...

    知乎6852022-09-01
  • C#Mongodb在CSharp里实现Aggregate实例

    Mongodb在CSharp里实现Aggregate实例

    本篇文章主要介绍了Mongodb在CSharp里实现Aggregate实例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    张占岭9172022-01-05
  • C#C#仿QQ聊天窗口

    C#仿QQ聊天窗口

    这篇文章主要为大家详细介绍了C#仿QQ聊天窗口,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    Dust_SongYunfei10552022-11-25
  • C#C# 导出Excel的6种简单方法实现

    C# 导出Excel的6种简单方法实现

    C# 导出 Excel 的6种简单方法:数据表导出到 Excel,对象集合导出到 Excel,数据库导出到 Excel,微软网格控件导出到 Excel,数组导出到 Excel,CSV 导出到 Excel,...

    Johnson Manohar5352022-08-05
  • C#C#使用Unity实现剪刀石头布游戏

    C#使用Unity实现剪刀石头布游戏

    这篇文章主要为大家详细介绍了C#语言使用Unity实现剪刀石头布游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    莹莹carriex4722022-03-08
  • C#C# Bitmap图像处理加速的实现

    C# Bitmap图像处理加速的实现

    本文主要介绍了C# Bitmap图像处理加速的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    Dear_Xuan3282022-12-08