C#基本線程同步

来源:http://www.cnblogs.com/shouce/archive/2016/06/03/5555053.html
-Advertisement-
Play Games

0 概述 所謂同步,就是給多個線程規定一個執行的順序(或稱為時序),要求某個線程先執行完一段代碼後,另一個線程才能開始執行。 第一種情況:多個線程訪問同一個變數: 1. 一個線程寫,其它線程讀:這種情況不存在同步問題,因為只有一個線程在改變記憶體中的變數,記憶體中的變數在任意時刻都有一個確定的值; 2. ...


0 概述

  所謂同步,就是給多個線程規定一個執行的順序(或稱為時序),要求某個線程先執行完一段代碼後,另一個線程才能開始執行。

  

第一種情況:多個線程訪問同一個變數

  1. 一個線程寫,其它線程讀:這種情況不存在同步問題,因為只有一個線程在改變記憶體中的變數,記憶體中的變數在任意時刻都有一個確定的值;

  2. 一個線程讀,其它線程寫:這種情況會存在同步問題,主要是多個線程在同時寫入一個變數的時候,可能會發生一些難以察覺的錯誤,導致某些線程實際上並沒有真正的寫入變數;

  3. 幾個線程寫,其它線程讀:情況同2。

  多個線程同時向一個變數賦值,就會出現問題,這是為什麼呢?

  我們編程採用的是高級語言,這種語言是不能被電腦直接執行的,一條高級語言代碼往往要編譯為若幹條機器代碼,而一條機器代碼,CPU也不一定是在一個CPU周期內就能完成的。電腦代碼必須要按照一個“時序”,逐條執行。

  舉個例子,在記憶體中有一個整型變數number(4位元組),那麼計算++number(運算後賦值)就至少要分為如下幾個步驟:

  1. 定址:由CPU的控制器找尋到number變數所在的地址;

  2. 讀取:將number變數所在的值從記憶體中讀取到CPU寄存器中;

  3. 運算:由CPU的算術邏輯運算器(ALU)對number值進行計算,將結果存儲在寄存器中;

  4. 保存:由CPU的控制器將寄存器中保存的結果重新存入number在記憶體中的地址。

  這是最簡單的時序,如果牽扯到CPU的高速緩存(CACHE),則情況就更為複雜了。

CPU結構簡圖圖1 CPU結構簡圖

  在多線程環境下,當幾個線程同時對number進行賦值操作時(假設number初始值為0),就有可能發生衝突: 

  當某個線程對number進行++操作並執行到步驟2(讀取)時(0保存在CPU寄存器中),發生線程切換,該線程的所有寄存器狀態被保存到記憶體後後,由另一個線程對number進行賦值操作。當另一個線程對number賦值完畢(假設將number賦值為10),切換回第一個線程,進行現場恢復,則在寄存器中保存的number值依然為0,該線程從步驟3繼續執行指令,最終將1寫入到number所在記憶體地址,number值最終為1,另一個線程對number賦值為10的操作表現為無效操作。

  看一個例子:

 

[csharp] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. using System;  
  2. using System.Threading;  
  3.    
  4. namespace Edu.Study.Multithreading.WriteValue {  
  5.    
  6.     class Program {  
  7.    
  8.         /// <summary>  
  9.         /// 多個線程要訪問的變數  
  10.         /// </summary>  
  11.         private static int number = 0;  
  12.    
  13.         /// <summary>  
  14.         /// 令線程隨機休眠的隨機數對象  
  15.         /// </summary>  
  16.         private static Random random = new Random();  
  17.    
  18.         /// <summary>  
  19.         /// 線程入口方法, 這裡為了簡化編程, 使用了靜態方法  
  20.         /// </summary>  
  21.         private static void ThreadWork(object arg) {  
  22.    
  23.             // 迴圈1000次, 每次將number欄位的值加1  
  24.             for (int i = 0; i < 1000; ++i) {  
  25.                 // += 1操作比++操作需要更多的CPU指令, 以增加出現錯誤的幾率  
  26.                 number += 1;  
  27.                 // 線程在10毫秒內隨機休眠, 以增加出現錯誤的幾率  
  28.                 Thread.Sleep(random.Next(10));  
  29.             }  
  30.         }  
  31.    
  32.    
  33.         /// <summary>  
  34.         /// 主方法  
  35.         /// </summary>  
  36.         static void Main(string[] args) {  
  37.             do {  
  38.                 // 令number為0, 重新給其賦值  
  39.                 number = 0;  
  40.                 Thread t1 = new Thread(new ParameterizedThreadStart(ThreadWork));  
  41.                 Thread t2 = new Thread(new ParameterizedThreadStart(ThreadWork));  
  42.    
  43.                 // 啟動兩個線程訪問number變數  
  44.                 t1.Start();  
  45.                 t2.Start();  
  46.    
  47.                 // 等待線程退出, Timeout.Infinite表示無限等待  
  48.                 while (t1.Join(Timeout.Infinite) && t2.Join(Timeout.Infinite)) {  
  49.                     Console.WriteLine(number);  
  50.                     break;  
  51.                 }  
  52.                 Console.WriteLine("請按按回車鍵重新測試,任意鍵退出程式......");  
  53.             } while (Console.ReadKey(false).Key == ConsoleKey.Enter);  
  54.         }  
  55.     }  
  56. }  

  例子中,兩個線程(t1和t2)同時訪問number變數(初始值為0),對其進行1000次+1操作,在兩個線程都結束後,在主線程顯式number變數的最終值。可以看到,很經常的,最終顯示的結果不是2000,而是1999或者更少。究其原因,就是發生了我們上面講的問題:兩個線程在進行賦值操作時,時序重疊了

  可以做實驗,在CPU核心數越多的電腦上,上述代碼出現問題的幾率越小。這是因為多核心CPU可能會在每一個獨立核心上各自運行一個線程,而CPU設計者針對這種多核心訪問一個記憶體地址的情況,本身就設計了防範措施。

 

第二種情況:多個線程組成了生產者和消費者:

  我們前面已經講過,多線程並不能加快演算法速度(多核心處理器除外),所以多線程的主要作用還是為了提高用戶的響應,一般有兩種方式:

  • 將響應窗體事件操作和複雜的計算操作分別放在不同的線程中,這樣當程式在進行複雜計算時不會阻塞到窗體事件的處理,從而提高用戶操作響應;
  • 對於為多用戶服務的應用程式,可以一個獨立線程為一個用戶提供服務,這樣用戶之間不會相互影響,從而提高了用戶操作的響應。

  所以,線程之間很容易就形成了生產者/消費者模式,即一個線程的某部分代碼必須要等待另一個線程計算出結果後才能繼續運行。目前存在兩種情況需要線程間同步執行:

  • 多個線程向一個變數賦值或多線程改變同一對象屬性;
  • 某些線程等待另一些線程執行某些操作後才能繼續執行。

1 變數的原子操作

  CPU有一套指令,可以在訪問記憶體中的變數前,並將一段記憶體地址標記為“只讀”,此時除過標誌記憶體的那個線程外,其餘線程來訪問這塊記憶體,都將發生阻塞,即必須等待前一個線程訪問完畢後其它線程才能繼續訪問這塊記憶體。

  這種鎖定的結果是:所有線程只能依次訪問某個變數,而無法同時訪問某個變數,從而解決了多線程訪問變數的問題。

  原子操作封裝在Interlocked類中,以一系列靜態方法提供:

  • Add方法,對整型變數(4位、8位)進行原子的加法/減法操作,相當於n+=x或n-=x表達式的原子操作版本;
  • Increment方法,對整形變數(4位、8位)進行原子的自加操作,相當於++n的原子操作版本;
  • Decrement方法,對整型變數(4位、8位)進行原子的自減操作,相當於--n的原子操作版本;
  • Exchange方法,對變數或對象引用進行原子的賦值操作;
  • CompareExchange方法,對兩個變數或對象引用進行比較,如果相同,則為其賦值。

  例如:

Interlocked.Add方法演示

[csharp] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. int n = 0;  
  2.    
  3. // 將n加1  
  4. // 執行完畢後n的值變為1, 和返回值相同  
  5. int x = Interlocked.Add(ref n, 1);  
  6. // 將n減1  
  7. x = Interlocked.Add(ref n, -1);  
  8. Interlocked.Increment/Interlocked.Decrement方法演示  
  9. int n = 0;  
  10.    
  11. // 對n進行自加操作  
  12. // 執行完畢後n的值變為1, 和返回值相同  
  13. int x = Interlocked.Increment(ref n);  
  14. // 對n進行自減操作  
  15. x = Interlocked.Decrement(ref n);  
  16. Interlocked.Exchange方法演示  
  17. string s = "Hello";  
  18.    
  19. // 用另一個字元串對象"OK"為s賦值  
  20. // 操作完畢後s變數改變為引用到"OK"對象, 返回"Hello"對象的引用  
  21. string old = Interlocked.Exchange(ref s, "OK");  
  22. Interloceked.CompareExchange方法演示  
  23. string s = "Hello";  
  24. string ss = s;  
  25.    
  26. // 首先用變數ss和s比較, 如果相同, 則用另一個字元串對象"OK"為s賦值  
  27. // 操作完畢後s變數改變為引用到"OK"對象, 返回"Hello"對象的引用  
  28. string old = Interlocked.CompareExchange(ref s, ss, "OK");  

  註意,原子操作中,要賦值的變數都是以引用方式傳遞參數的,這樣才能在原子操作方法內部直接改變變數的值,才能完全避免非安全的賦值操作。

下麵我們將前一節中出問題的代碼做一些修改,修改其ThreadWork方法,在多線程下能夠安全的操作同一個變數:

 

[csharp] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. private static void ThreadWork(object arg) {  
  2.     for (int i = 0; i < 1000; ++i) {  
  3.         // 使用原子方式操作變數, 避免多個線程為同一變數賦值出現錯誤  
  4.         Interlocked.Add(ref number, 1);  
  5.         Thread.Sleep(random.Next(10));  
  6.     }  
  7. }  

 

  上述代碼解決了一個重要的問題:同一個變數同時只能被一個線程賦值

 

2 迴圈鎖、關鍵代碼段和令牌對象

  使用變數的原子操作可以解決整數變數的加減計算和各類變數的賦值操作(或比較後賦值操作)的問題,但對於更複雜的同步操作,原子操作並不能解決問題。

  有時候我們需要讓同一段代碼同時只能被一個線程執行,而不僅僅是同一個變數同時只能被一個線程訪問,例如如下操作:

 

[csharp] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. double a = 10;  
  2. double b = 20;  
  3.    
  4. c = Math.Pow(a, 2);  
  5. c += Math.Pow(b, 2);  
  6. c = Math.Sqrt(c);  
  7. c /= Math.PI;  

 

  假設變數c是一個類欄位,同時被若幹線程賦值,顯然僅通過原子操作,無法解決c變數被不同線程同時訪問的問題,因為計算c需要若幹步才能完成計算,需要比較多的指令,原子操作只能在對變數一次賦值時產生同步,面對多次賦值,顯然無能為力。無論c=Math.Pow(a, 2)這步如何原子操作後,這步結束後下步開始前,c的值都有可能其它線程改變,從而最終計算出錯誤的結果。

  所以鎖定必須要施加到一段代碼上才能解決上述問題,這就是關鍵代碼段

  關鍵代碼段需要兩個前提條件:

  • 一個作為令牌的對象;
  • 一個鎖操作。

  令牌對象有個狀態屬性:具備兩個屬性值:掛起和釋放。可以通過原子操作改變這個屬性的屬性值。規定:所有線程都可以訪問同一個令牌對象,但只有訪問時令牌對象狀態屬性為釋放狀態的那個線程,才能執行被鎖定的代碼,同時將令牌對象的狀態屬性更改為掛起。其餘線程自動進入迴圈檢測代碼(在一個迴圈中不斷檢測令牌對象的狀態),直到第一個對象訪問完鎖定代碼,將令牌對象狀態屬性重新設置為釋放狀態,其餘線程中的某一個才能檢測到令牌對象已經釋放並接著執行被鎖定的代碼,同時將令牌對象狀態屬性設置為掛起

  語法如下:

 

[csharp] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. lock (對象引用) {  
  2.     // 關鍵代碼段  
  3. }  

 

  其中lock稱為迴圈鎖,訪問的引用變數所引用的對象稱為令牌對象,一對大括弧中的代碼稱為關鍵代碼段。如果同時有多個線程訪問同一關鍵代碼段,則可以保證每次同時只有一個線程可以執行這段代碼,一個線程執行完畢後另一個線程才能解開鎖並執行這段代碼。

  所以前面的那段代碼可以改為:

 

[csharp] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. double a = 10;    
  2. double b = 20;    
  3.     
  4. lock (某對象引用) {   
  5.     c = Math.Pow(a, 2);    
  6.     c += Math.Pow(b, 2);    
  7.     c = Math.Sqrt(c);    
  8.     c /= Math.PI;    
  9. }   

 

  在.net Framework中,任意引用類型對象都可以作為令牌對象。

  鎖定使用起來很簡單,關鍵在使用前要考慮鎖定的顆粒度,也就是鎖定多少行代碼才能真正的安全。鎖定的代碼過少,可能無法保證完全同步,鎖定的代碼過多,有可能會降低系統執行效率(導致線程無法真正意義上的同時執行),我們舉個例子,解釋一下鎖定的顆粒度:

  程式界面設計如下:

迴圈鎖程式設計界面  圖2 迴圈鎖程式設計界面

  程式運行效果圖如下:

訊院所程式運行效果圖   圖3 程式運行效果圖

  源代碼摘錄如下:

FormMain.cs

 

[csharp] view plaincopy在CODE上查看代碼片派生到我的代碼片
  1. using System;  
  2. using System.Drawing;  
  3. using System.Threading;  
  4. using System.Windows.Forms;  
  5.    
  6. namespace Edu.Study.Multithreading.Lock {  
  7.    
  8.     /// <summary>  
  9.     /// 更新PictureBox背景色的委托  
  10.     /// </summary>  
  11.     /// <param name="index">要更改背景色的PictureBox對象在數組中的索引</param>  
  12.     /// <param name="color">背景色</param>  
  13.     public delegate void ChangeRadioButtonHandler(int index, Color color);  
  14.    
  15.    
  16.     /// <summary>  
  17.     /// 主窗體  
  18.     /// </summary>  
  19.     public partial class FormMain : Form {  
  20.    
  21.         /// <summary>  
  22.         /// RadioButton的數組  
  23.         /// </summary>  
  24.         private PictureBox[] picboxes = new PictureBox[10];  
  25.    
  26.         /// <summary>  
  27.         /// 線程1, 將picboxes數組中的PictureBox對象背景色逐個設置為紅色  
  28.         /// </summary>  
  29.         private Thread thread1 = null;  
  30.    
  31.         /// <summary>  
  32.         /// 線程1, 將picboxes數組中的PictureBox對象背景色逐個設置為綠色  
  33.         /// </summary>  
  34.         private Thread thread2 = null;  
  35.    
  36.         /// <summary>  
  37.         /// 主窗體構造器  
  38.         /// </summary>  
  39.         public FormMain() {  
  40.             InitializeComponent();  
  41.    
  42.             // 初始化picboxes數組, 向其中存放PictureBox對象引用  
  43.             for (int i = 0; i < this.picboxes.Length; ++i) {  
  44.                 PictureBox rb = new PictureBox();  
  45.    
  46.                 // 設置PictureBox對象大小  
  47.                 rb.Size = new Size(50, 50);  
  48.                 // 設置PictureBox邊框樣式  
  49.                 rb.BorderStyle = BorderStyle.Fixed3D;  
  50.                 // 設置PictureBox背景色初始為白色  
  51.                 rb.BackColor = Color.White;  
  52.    
  53.                 this.picboxes[i] = rb;  
  54.    
  55.                 // 將PictureBox控制項對象放置在流式佈局面板上  
  56.                 this.mainFlowLayoutPanel.Controls.Add(rb);  
  57.             }  
  58.    
  59.             // 根據控制項的數量重新計算窗體寬度  
  60.             this.Width =  
  61.                 this.mainFlowLayoutPanel.Padding.Left +  
  62.                 this.mainFlowLayoutPanel.Padding.Right +  
  63.                 this.picboxes.Length * (50 + this.picboxes[0].Margin.Left + this.picboxes[0].Margin.Right);  
  64.         }  
  65.    
  66.         /// <summary>  
  67.         /// 實現ChangeRadioButtonHandler委托, 轉換設置某個PictureBox控制項背景色  
  68.         /// </summary>  
  69.         /// <param name="index">要更改背景色的PictureBox在數組中的索引</param>  
  70.         /// <param name="color">背景色</param>  
  71.         private void ChangeRadioButton(int index, Color color) {  
  72.    
  73.             // 操作如下: 從this.picboxes數組中, 每次將index參數指定的PictureBox對象設置為參數color指定的顏色  
  74.             // 並將前一個PictureBox對象背景色設置為白色  
  75.    
  76.             if (index == 0) { // 如果index參數為零, 表示數組中第一個PictureBox對象  
  77.    
  78.                 // 將數組最後一個PictureBox對象背景色設置為白色  
  79.                 this.picboxes[this.picboxes.Length - 1].BackColor = Color.White;  
  80.    
  81.             } else { // 如果index參數不為零  
  82.    
  83.                 // 將 index-1指定的PictureBox背景色設置為白色  
  84.                 this.picboxes[index - 1].BackColor = Color.White;  
  85.             }  
  86.    
  87.             // 將index指定的PictureBox對象背景色設置為color參數指定的顏色  
  88.             this.picboxes[index].BackColor = color;  
  89.         }  
  90.    
  91.         /// <summary>  
  92.         /// 線程方法1, 展示顆粒度較小的鎖定  
  93.         /// </summary>  
  94.         /// <param name="arg">傳入的參數對象, 這裡為一個Color類的對象, 表示背景色</param>  
  95.         private void ThreadWorkTest1(object arg) {  
  96.             try { // 用於退出線程的異常捕獲結構  
  97.                 while (true) {  
  98.                     // 遍歷this.picboxes數組  
  99.                     for (int i = 0; i < this.picboxes.Length; ++i) {  
  100.                         // 以當前Form類對象為令牌對象, 這次鎖定發生在迴圈內  
  101.                         lock (this) {  
  102.                             // 執行ChangeRadioButton方法, 更改PictureBox的背景色  
  103.                             this.BeginInvoke(new ChangeRadioButtonHandler(this.ChangeRadioButton), i, arg);  
  104.                             Thread.Sleep(500);  
  105.                         }  
  106.                     }  
  107.                 }  
  108.             } catch (ThreadAbortException) {  
  109.             }  
  110.         }  
  111.    
  112.         /// <summary>  
  113.         /// 線程方法2, 展示顆粒度較大的鎖定  
  114.         /// </summary>  
  115.         /// <param name="arg">傳入的參數對象, 這裡為一個Color類的對象, 表示背景色</param>  
  116.         private void ThreadWorkTest2(object arg) {  
  117.             try { // 用於退出線程的異常捕獲結構  
  118.                 while (true) {  
  119.                     // 以當前Form類對象為令牌對象, 這次鎖定鎖定整個迴圈  
  120.                     lock (this) {  
  121.                         // 遍歷this.picboxes數組  
  122.                         for (int i = 0; i < this.picboxes.Length; ++i) {  
  123.                             // 執行ChangeRadioButton方法, 更改PictureBox的背景色  
  124.                             this.BeginInvoke(new ChangeRadioButtonHandler(this.ChangeRadioButton), i, arg);  
  125.                             Thread.Sleep(500);  
  126.                         }  
  127.                     }  
  128.                 }  
  129.    &nb
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 原因:這是由於管理工具的服務中的windows防火牆被禁用了。 解決方案:在window7中點擊控制面板,然後點擊管理工具,在點服務,然後找到windows firewall 然後將其改為自動就就可以了。 ...
  • 本文是Linux Shell系列教程的第(六)篇,更多shell教程請看:Linux Shell系列教程 Shell在編程方面非常強大,其數組功能也非常的完善,今天就為大家介紹下Shell數組的用法。 Shell支持一維數組(不支持多維數組),並且沒有限定數組的大小。 類似與C語言,數組元素的下標由 ...
  • 之前安裝了一個rehat6的linux系統,把交叉編譯搭建給忽視了,結果在編譯uboot的時候出現問題,顯示找不到arm-linux-gcc。於是自己來搭建交 叉編譯環境。出現好多錯。先是解壓時沒在後邊加 -C/,後是直接自己創建了個目錄,把解壓後的bin目錄複製到自己創建的目錄。還是出現問題。但 ...
  • 哈哈,我又回來了,簡單的重新裝了一邊虛擬機,又把vim配置了一遍,這回有信心把youcomplete的安裝方法貼出來了,先給個權威的鏈接,然後給出具體步驟,保證沒問題可以安裝成功 http://www.centoscn.com/image-text/install/2016/0424/7115.ht ...
  • 我的機子炸了,然後我就得重新裝我的虛擬機,再然後我就想去弄好我的共用文件夾安裝vmtools,安裝的時候出現了一個問題,我忘記以前是怎麼解決的,又困擾了我好久 Searching for a valid kernel header path... The path "" is not a valid ...
  • 如何一步步點亮LED 註:實驗是基於s5pv210的板子,這篇筆記參考自《朱老師物聯網大講堂》朱老師隨堂筆記 文中提到的手冊下載鏈接:(https://yunpan.cn/OcSz7Yh35ISJK7 訪問密碼 6665;https://yunpan.cn/OcSz7SzsfS7a6p 訪問密碼 1 ...
  • 我們經常在Linux下可以看到inode,都不知道是什麼東東,那麼我們現在來慢慢瞭解下。 一、inode是什麼? 理解inode,要從文件儲存說起。 文件儲存在硬碟上,硬碟的最小存儲單位叫做"扇區"(Sector)。每個扇區儲存512位元組(相當於0.5KB)。 操作系統讀取硬碟的時候,不會一個個扇區 ...
  • 創建基本的鏈接和URL<!--?xml:namespace prefix = "o" ns = "urn:schemas-microsoft-com:office:office" /--> 在我們介紹鏈接或URL之前先做一些準備,我們這部分要介紹的知識將要使用的項目就是之前建立的HelperMeth ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...