ShowProgramCode

2022年12月30日 星期五

C# .Net Core6 多執行緒處理ThreadPool PartII

從上一篇可知ThreadPool的基本設定了,不過接下來又遇上新的問題。

  1.  如何確認所有執行緒完成?
  2.  將主控台專案改為WinForm專案使用ThreadPool...

首先,先說明如何確認所有執行緒完成工作?
在此我使用者WaitHandle.WaitAll()來確認。
依照官網的說明,必須將doneEvent = new ManualResetEvent(false)帶入ThreadPool,最終使用doneEvent陣列帶入WaitHandle.WaitAll()即可。
二話不說,先上程式碼。

  1. internal class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. const int FibonacciCalculations = 10;
  6. firtstTest(FibonacciCalculations);
  7. }
  8. private static void ThreadPoolCallbackV1(object obj)
  9. {
  10. if(obj == null) return;
  11. ManualResetEvent doneEvent = (ManualResetEvent)obj;
  12. Console.WriteLine(nameof(ThreadPoolCallbackV1));
  13. doneEvent.Set();
  14. }
  15. private static void firtstTest(int theadCount)
  16. {
  17. ThreadPool.SetMinThreads(2, 2);
  18. ThreadPool.SetMaxThreads(3, 3);
  19. ManualResetEvent[] doneEvents = new ManualResetEvent[theadCount];
  20. for(var i = 0; i < theadCount; i++)
  21. {
  22. doneEvents[i] = new ManualResetEvent(false);
  23. Console.WriteLine($"{nameof(firtstTest)} 第 {i} 次執行...");
  24. ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, doneEvents[i]);
  25. }
  26. WaitHandle.WaitAll(doneEvents);
  27. Console.WriteLine("All calculations are complete.");
  28. }
  29. }

是的,這個是主控台的程式碼,當所有執行緒完成後,才會印出最後一個句子。

接下來的問題,就是要把這段程式碼改道WomForm的專案了...
最剛開始,我想...在主控台都測通過了,沒有問題。於是,隨便開了個空的Form表單,就把程式碼貼過去了。
結果? 當然是失敗了...

我花了至少三天才搞定它,雖然解法非常簡單...
二話不說,先上程式碼。

Program.cs

  1. internal static class Program
  2. {
  3. ///
  4. /// The main entry point for the application.
  5. ///
  6. //[STAThread] 將STAThread改為MTAThread,WaitHandle.WaitAll不支援STAThread,改為MTAThread就能夠執行...
  7. [MTAThread]
  8. static void Main()
  9. {
  10. // To customize application configuration such as set high DPI settings or default font,
  11. // see https://aka.ms/applicationconfiguration.
  12. ApplicationConfiguration.Initialize();
  13. Application.Run(new Form1());
  14. }
  15. }

Form1.cs

  1. private string _content = string.Empth;
  2. public partial class Form1 : Form
  3. {
  4. public TpcGrpcStressTest()
  5. {
  6. InitializeComponent();
  7. }
  8. private static void ThreadPoolCallbackV1(object obj)
  9. {
  10. if(obj == null) return;
  11. ManualResetEvent doneEvent = (ManualResetEvent)obj;
  12. _content += nameof(ThreadPoolCallbackV1) + Environment.NewLine;
  13. doneEvent.Set();
  14. }
  15.  
  16. private static void firtstTest(int theadCount)
  17. {
  18. ThreadPool.SetMinThreads(2, 2);
  19. ThreadPool.SetMaxThreads(3, 3);
  20. ManualResetEvent[] doneEvents = new ManualResetEvent[theadCount];
  21. for(var i = 0; i < theadCount; i++)
  22. {
  23. doneEvents[i] = new ManualResetEvent(false);
  24. _content += $"{nameof(firtstTest)} 第 {i} 次執行..." + Environment.NewLine;
  25. ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, doneEvents[i]);
  26. }
  27. WaitHandle.WaitAll(doneEvents);
  28. }
  29.  
  30. private void SubmitBtn_Click(object sender, EventArgs e)
  31. {
  32. firtstTest(10);
  33. TextBox1.Text = _content + "All calculations are complete.";
  34. }
  35. }

答案超簡單,但困擾我許久,特別記錄下來,免得下次又發生。

參考網址: AutoResetEvent.WaitAll 等到人生三大事,然后大笑开心。


本以為到此為止,但是當我測試超過100筆執行緒時,又一個問題出現了...
System.NotSupportedException: 'The number of WaitHandles must be less than or equal to 64.'

再去查了資訊,才發現原來超過64個執行緒使用WaitHandle.WaitAll會錯誤...
實際找到解法後,說明不需要使用WaitHandle.WaitAll來檢查程序是否執行完成,那麼二話不說,修改程式碼...

Program.cs

  1. internal static class Program
  2. {
  3. ///
  4. /// The main entry point for the application.
  5. ///
  6. [STAThread]
  7. static void Main()
  8. {
  9. // To customize application configuration such as set high DPI settings or default font,
  10. // see https://aka.ms/applicationconfiguration.
  11. ApplicationConfiguration.Initialize();
  12. Application.Run(new Form1());
  13. }
  14. }

Form1.cs

  1. public partial class Form1 : Form
  2. {
  3. private string _content = string.Empth;
  4. private static int _numerOfThreadsNotYetCompleted = 0;
  5. private static ManualResetEvent _doneEvent = new ManualResetEvent(false);
  6. public TpcGrpcStressTest()
  7. {
  8. InitializeComponent();
  9. }
  10. private static void ThreadPoolCallbackV1(object obj)
  11. {
  12. if(obj == null) return;
  13. try
  14. {
  15. ManualResetEvent doneEvent = (ManualResetEvent)obj;
  16. _content += $"{nameof(ThreadPoolCallbackV1)} 第 {(int)i} 次執行...") + Environment.NewLine;
  17. doneEvent.Set();
  18. }
  19. finally
  20. {
  21. if (Interlocked.Decrement(ref _numerOfThreadsNotYetCompleted) == 0)
  22. _doneEvent.Set();
  23. }
  24. }
  25.  
  26. private static void firtstTest(int theadCount)
  27. {
  28. ThreadPool.SetMinThreads(2, 2);
  29. ThreadPool.SetMaxThreads(3, 3);
  30. for(var i = 0; i < theadCount; i++)
  31. {
  32. _content += nameof(firtstTest) + Environment.NewLine;
  33. ThreadPool.QueueUserWorkItem(ThreadPoolCallbackV1, (object)i);
  34. }
  35. _doneEvent.WaitOne();
  36. }
  37.  
  38. private void SubmitBtn_Click(object sender, EventArgs e)
  39. {
  40. _numerOfThreadsNotYetCompleted = 100;
  41. firtstTest(100);
  42. TextBox1.Text = _content + "All calculations are complete.";
  43. }
  44. }

參考網址: Solved: “The number of WaitHandles must be less than or equal to 64″

2022年12月15日 星期四

C# .Net Core6 多執行緒處理ThreadPool

現行遇到一個狀況,需要使用多執行緒連線TCP Server。
原先我是想使用Thread,不過在查詢如何使用多個執行緒設定時,發現了另外一個可用的方法ThreadPool

首先是Microsoft目前並不建議直接使用Thread控制,加上ThreadPool可以直接在for迴圈內使用,自動增加多個執行緒。對於我目前要做一個壓測用的程式而言,只要在設定檔寫好測試次數或者測試時間,用for迴圈執行ThreadPool就能得到想要的結果。實在是太方便了!

不過,在這當中我遇到了一些問題,為防止自己忘記,先記錄在此。

  1.  依照說明傳入是非固定的object,該如何設為固定的物件?
  2.  設定的執行緒的最大值,但實際程式執行時,同一時間內的執行緒超過上限...

首先是關於第一項的問題,該如何將object轉為固定的物件?
關於這一點,必須先說明ThreadPool如何使用...
下面的範例是我用來測試使用的,不過後來改過,所以不確定這個是否可以執行...

  1. static void Main(string[] args)
  2. {
  3. for(var i=0;i<100;i++)
  4. {
  5. ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), i);
  6. }
  7. }
  8.  
  9. private static void countIndex(object? obj)
  10. {
  11. string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  12. string msg = $"{timeStr}\t[{obj}] \t[{Thread.CurrentThread.ManagedThreadId}] \t This is Test!!";
  13. Console.WriteLine(msg);
  14. }

測試沒有問題後,我發現我要帶入的是一個完整的資料格式。
我用過不少方法,都沒有成功,最後改為下面的方式就可以執行了。
不過必須要用try catch包好,物件轉換錯誤可能會讓程式整個當掉,但因為是內部使用,就沒寫那麼多防呆了。

  1. static void Main(string[] args)
  2. {
  3. for(var i=0;i<100;i++)
  4. {
  5. DTO dto = new dto;
  6. dto.Index = i;
  7. ...
  8. ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
  9. }
  10. }
  11.  
  12. private static void countIndex(object? obj)
  13. {
  14. try
  15. {
  16. //這裡的轉換物件須注意
  17. DTO dto = (DTO)obj;
  18. ...
  19. string msg = getMessage(obj.Index, dto.Message);
  20. Console.WriteLine(msg);
  21. }
  22. catch (Exception ex)
  23. {
  24. string msg = getMessage(0, $"error = {ex.ToString()}");
  25. Console.WriteLine(msg);
  26. }
  27. }
  28.  
  29. private static string getMessage(int Index, string msg)
  30. {
  31. string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  32. return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
  33. }

其次是關於執行緒上限的設定...
依照我上方的程式碼,執行緒ID與Index是可以做比對的,所以我發現了在同一時間,執行緒超出我設定的上限...
當時的程式碼大致如下:

  1. static void Main(string[] args)
  2. {
  3. ThreadPool.SetMaxThreads(5, 5);
  4. for(var i=0;i<100;i++)
  5. {
  6. DTO dto = new dto;
  7. dto.Index = i;
  8. ...
  9. ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
  10. }
  11. }
  12.  
  13. private static void countIndex(object? obj)
  14. {
  15. try
  16. {
  17. //這裡的轉換物件須注意
  18. DTO dto = (DTO)obj;
  19. ...
  20. string msg = getMessage(obj.Index, dto.Message);
  21. Console.WriteLine(msg);
  22. }
  23. catch (Exception ex)
  24. {
  25. string msg = getMessage(0, $"error = {ex.ToString()}");
  26. Console.WriteLine(msg);
  27. }
  28. }
  29.  
  30. private static string getMessage(int Index, string msg)
  31. {
  32. string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  33. return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
  34. }

但是實際執行,跳出的執行緒至少超過10個...
這和我當初想像的結果完全不同,測試過許多方法都沒有用,最後才發現只設定SetMaxThreads是不行的!
將程式碼調整成下方後,測試的結果終於與我想的相同了。Thread.CurrentThread.ManagedThreadId出現的號碼只有設定的數量,同一個時間執行緒也不會超過上限...

  1. static void Main(string[] args)
  2. {
  3. //min & max 必須同時設定才有效...
  4. ThreadPool.SetMinThreads(2, 2);
  5. ThreadPool.SetMaxThreads(5, 5);
  6. for(var i=0;i<100;i++)
  7. {
  8. DTO dto = new dto;
  9. dto.Index = i;
  10. ...
  11. ThreadPool.QueueUserWorkItem(new WaitCallback(countIndex), dto);
  12. }
  13. }
  14.  
  15. private static void countIndex(object? obj)
  16. {
  17. try
  18. {
  19. //這裡的轉換物件須注意
  20. DTO dto = (DTO)obj;
  21. ...
  22. string msg = getMessage(obj.Index, dto.Message);
  23. Console.WriteLine(msg);
  24. }
  25. catch (Exception ex)
  26. {
  27. string msg = getMessage(0, $"error = {ex.ToString()}");
  28. Console.WriteLine(msg);
  29. }
  30. }
  31.  
  32. private static string getMessage(int Index, string msg)
  33. {
  34. string timeStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
  35. return $"{timeStr}\t[{Index}] \t[{Thread.CurrentThread.ManagedThreadId}] \t {msg}";
  36. }

參考網址:
Microsoft官網說明
kinanson的技術回憶
安德魯的部落格
余小章 @ 大內殿堂
玩轉C#之【執行序-實際實作】
C# .NET Blazor MAUI Xamarin Research

C# .net Core6 主控台使用強行別讀取 AppSetting.json

在主控台讀取設定檔有兩種格式:
  1.  .Xml
  2.  .Json
現在我要記錄的是第二種.json的格式該如何在主控台的專案使用強型別讀取。
首先,在專案的最上層新增一個appsetting.json檔案
接著,新增一個物件檔,處理設定檔的讀取。
  1. internal class ConfigHelp
  2. {
  3. private readonly IConfigurationRoot config;
  4. public ConfigHelp()
  5. {
  6. config = new ConfigurationBuilder()
  7. .SetBasePath(Directory.GetCurrentDirectory())
  8. .AddJsonFile("appsetting.json", true)
  9. .Build();
  10. }
  11. public void Get<T>(string configName, ref T resp)
  12. {
  13. config.GetSection(configName).Bind(resp);
  14. }
  15. public string? Get(string configName)
  16. {
  17. return config[configName];
  18. }
  19. }
appsetting.json檔案
  1. {
  2. "Setting": {
  3. "MinThreads": 2,
  4. "MaxThreads": 3,
  5. "TestTimes": 300
  6. }
  7. }
設定檔的DTO
  1. public class Setting
  2. {
  3. public int MinThreads { get; set; }
  4. public int MaxThreads { get; set; }
  5. public int TestTimes { get; set; }
  6. }
實際主控台程式讀取。
  1. internal class Program
  2. {
  3. private static Setting setting = new Setting();
  4. static void Main(string[] args)
  5. {
  6. ConfigHelp config = new ConfigHelp();
  7. config.Get("Setting", ref setting);
  8. }
  9. }