C#并行编程(2):.NET线程池
線程?Thread
在總結線程池之前,先來看一下.NET線程。
.NET線程與操作系統(Windows)線程有什么區別?
.NET利用Windows的線程處理功能。在C#程序編寫中,我們首先會新建一個線程對象System.Threading.Thread,并為其指定一個回調方法;當我們調用線程對象的Start方法啟動線程時,會創建一個操作系統線程來執行回調方法。.NET中的線程實際上等價于Windows系統線程,都是CPU調度和分配的對象。
前臺線程和后臺線程
.NET把線程分為前臺線程和后臺線程,兩者幾乎相同,唯一的區別是,前臺線程會阻止進程的正常退出,后臺線程則不會。下面用一個例子描述前、后臺線程的區別:
class Program
{
static void Main(string[] args)
{
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.RunBackgroundThread();
{
Thread.Sleep(5000);
Thread.CurrentThread.Abort();
}
Console.ReadKey();
}
}
public class ThreadDemo
{
private readonly Thread _foregroundThread;
private readonly Thread _backgroundThread;
public ThreadDemo()
{
this._foregroundThread = new Thread(WriteNumberWorker) { Name = "ForegroundThread"};
this._backgroundThread = new Thread(WriteNumberWorker) { Name = "BackgroundThread", IsBackground = true };
}
private static void WriteNumberWorker()
{
for (int i = 0; i < 20; i++)
{
Console.WriteLine($"{DateTime.Now}=> {Thread.CurrentThread.Name} writes {i + 1}.");
Thread.Sleep(500);
}
}
public void RunForegroundThread()
{
this._foregroundThread?.Start();
}
public void RunBackgroundThread()
{
this._backgroundThread?.Start();
}
}
線程池?ThreadPool
線程的創建和銷毀要耗費很多時間,而且過多的線程不僅會浪費內存空間,還會導致線程上下文切換頻繁,影響程序性能。為改善這些問題,.NET運行時(CLR)會為每個進程開辟一個全局唯一的線程池來管理其線程。
線程池內部維護一個操作請求隊列,程序執行異步操作時,添加目標操作到線程池的請求隊列;線程池代碼提取記錄項并派發給線程池中的一個線程;如果線程池中沒有可用線程,就創建一個新線程,創建的新線程不會隨任務的完成而銷毀,這樣就可以避免線程的頻繁創建和銷毀。如果線程池中大量線程長時間無所事事,空閑線程會進行自我終結以釋放資源。
線程池通過保持進程中線程的少量和高效來優化程序的性能。
C#中線程池是一個靜態類,維護兩種線程,工作線程和異步IO線程,這些線程都是后臺線程。線程池不會影響進程的正常退出。
線程池的使用
線程池提供兩個靜態方法SetMaxThreads和SetMinThreads讓我們設置線程池的最大線程數和最小線程數。最大線程數指的是,該線程池能夠創建的最大線程數,當線程數達到設定值且忙碌,異步任務將進入請求隊列,直到有線程空閑才會執行;最小線程數指的是,線程池優先嘗試以設置數量的線程處理請求,當請求數達到一定量(未做深入研究)時,才會創建新的線程。
下面的例子展示了線程池的特性及常見使用方式。
class Program{
static void Main(string[] args)
{
RunCancellableWork();
Console.ReadKey();
}
static void RunThreadPoolDemo()
{
ThreadPoolDemo.ThreadPoolDemo.ShowThreadPoolInfo();
ThreadPool.SetMaxThreads(100, 100);
ThreadPool.SetMinThreads(8, 8);
ThreadPoolDemo.ThreadPoolDemo.ShowThreadPoolInfo();
ThreadPoolDemo.ThreadPoolDemo.MakeThreadPoolDoSomeWork(100);
ThreadPoolDemo.ThreadPoolDemo.MakeThreadPoolDoSomeIOWork();
}
static void RunCancellableWork()
{
Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] started a work");
Console.WriteLine("Press 'Esc' to cancel the work.");
Console.WriteLine();
ThreadPoolDemo.ThreadPoolDemo.DoSomeWorkWithCancellation();
if (Console.ReadKey(true).Key == ConsoleKey.Escape)
{
ThreadPoolDemo.ThreadPoolDemo.CTSource.Cancel();
}
}
}
public class ThreadPoolDemo
{
public static void ShowThreadPoolInfo()
{
int workThreads, completionPortThreads;
ThreadPool.GetAvailableThreads(out workThreads, out completionPortThreads);
Console.WriteLine($"GetAvailableThreads?=>?workThreads:{workThreads};completionPortThreads:{completionPortThreads}");
ThreadPool.GetMaxThreads(out workThreads, out completionPortThreads);
Console.WriteLine($"GetMaxThreads?=>?workThreads:{workThreads};completionPortThreads:{completionPortThreads}");
ThreadPool.GetMinThreads(out workThreads, out completionPortThreads);
Console.WriteLine($"GetMinThreads?=>?workThreads:{workThreads};completionPortThreads:{completionPortThreads}");
Console.WriteLine();
}
public static void MakeThreadPoolDoSomeWork(int workCount = 10)
{
for (int i = 0; i < workCount; i++)
{
int index = i;
ThreadPool.QueueUserWorkItem(s =>
{
Thread.Sleep(100);
Debug.Print($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] is running. [{index}]");
ShowAvailableThreads("WorkerThread");
});
}
}
public static void MakeThreadPoolDoSomeIOWork()
{
IList<string> uriList = new List<string>()
{
"http://news.baidu.com/",
"https://www.hao123.com/",
"https://map.baidu.com/",
"https://tieba.baidu.com/",
"https://wenku.baidu.com/",
"http://fanyi-pro.baidu.com",
"http://bit.baidu.com/",
"http://xueshu.baidu.com/",
"http://www.cnki.net/",
"http://www.wanfangdata.com.cn",
};
foreach (string uri in uriList)
{
WebRequest request = WebRequest.Create(uri);
request.BeginGetResponse(ac =>
{
try
{
WebResponse response = request.EndGetResponse(ac);
ShowAvailableThreads("IOThread");
Debug.Print($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] is running. [{response.ContentLength}]");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}, request);
}
}
private static void ShowAvailableThreads(string sourceTag = null)
{
int workThreads, completionPortThreads;
ThreadPool.GetAvailableThreads(out workThreads, out completionPortThreads);
Console.WriteLine($"{sourceTag}?GetAvailableThreads?=>?workThreads:{workThreads};completionPortThreads:{completionPortThreads}");
Console.WriteLine();
}
public static CancellationTokenSource CTSource { get; set; } = new CancellationTokenSource();
public static void DoSomeWorkWithCancellation()
{
ThreadPool.QueueUserWorkItem(t =>
{
Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] begun running. [0 - 9999]");
for (int i = 0; i < 10000; i++)
{
if (CTSource.Token.IsCancellationRequested)
{
Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] recived the cancel token. [{i}]");
break;
}
Thread.Sleep(100);
}
Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] was cancelled.");
});
}
}
線程池的調度
前面提到,線程池內部維護者一個工作項隊列,這個隊列指的是線程池全局隊列。實際上,除了全局隊列,線程池會給每個工作者線程維護一個本地隊列。
當我們調用ThreadPool.QueueUserWorkItem方法時,工作項會被放入全局隊列;使用定時器Timer的時候,也會將工作項放入全局隊列;但是,當我們使用任務Task的時候,假如使用默認的任務調度器,任務會被調度到工作者線程的本地隊列中。
工作者線程優先執行本地隊列中最新進入的任務,如果本地隊列中已經沒有任務,線程會嘗試從其他工作者線程任務隊列的隊尾取任務執行,這里需要進行同步。如果所有工作者線程的本地隊列都沒有任務可以執行,工作者線程才會從全局隊列取最新的工作項來執行。所有任務執行完畢后,線程睡眠,睡眠一定時間后,線程醒來并銷毀自己以釋放資源。
線程池處理異步IO的內部原理
上面的例子中,從網站獲取信息需要用到線程池的異步IO線程,線程池內部利用IOCP(IO完成端口)與硬件設備建立連接。異步IO實現過程如下:
托管的IO請求線程調用Win32本地代碼ReadFile方法
ReadFile方法分配IO請求包IRP并發送至Windows內核
Windows內核把收到的IRP放入對應設備驅動程序的IRP隊列中,此時IO請求線程已經可以返回托管代碼
驅動程序處理IRP并將處理結果放入.NET線程池的IRP結果隊列中
線程池分配IO線程處理IRP結果
小結
.NET線程池是并發編程的主要實現方式。C#中Timer、Parallel、Task在內部都是利用線程池實現的異步功能,深入理解線程池在并行編程中十分重要。
原文地址:https://www.cnblogs.com/chenbaoshun/p/10566124.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結
以上是生活随笔為你收集整理的C#并行编程(2):.NET线程池的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .net core 并发下的线程安全问题
- 下一篇: ASP.NET Core launchs