首頁 > 軟體

C#多執行緒的相關操作講解

2022-03-20 19:00:12

一、執行緒異常

我們在單執行緒中,捕獲異常可以使用try-catch,程式碼如下所示:

using System;

namespace MultithreadingOption
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 單執行緒中捕獲異常
            try
            {
                int[] array = { 1, 23, 61, 678, 23, 45 };
                Console.WriteLine(array[6]);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"message:{ex.Message}");
            }
            #endregion


            Console.ReadKey();
        }
    }
}

程式執行結果:

那麼在多執行緒中如何捕獲異常呢?是不是也可以使用try-catch進行捕獲?我們先看下面的程式碼:

using System;
using System.Threading.Tasks;

namespace MultithreadingOption
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 單執行緒中捕獲異常
            //try
            //{
            //    int[] array = { 1, 23, 61, 678, 23, 45 };
            //    Console.WriteLine(array[6]);
            //}
            //catch (Exception ex)
            //{
            //    Console.WriteLine($"message:{ex.Message}");
            //}
            #endregion

            #region 多執行緒中的異常

            try
            {
                for (int i = 0; i < 30; i++)
                {
                    string str = $"main_{i}";
                    // 開啟執行緒
                    Task.Run(() => 
                    {
                        Console.WriteLine($"{str} 開始了");
                        if(str.Equals("main_5"))
                        {
                            throw new Exception("main_5 發生了異常");
                        }
                        else if (str.Equals("main_11"))
                        {
                            throw new Exception("main_11 發生了異常");
                        }
                        else if (str.Equals("main_18"))
                        {
                            throw new Exception("main_18 發生了異常");
                        }
                        Console.WriteLine($"{str} 結束了");

                    });
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"message:{ex.Message}");
            }


            #endregion

            Console.ReadKey();
        }
    }
}

程式執行結果:

我們看到結果中並沒有輸出異常資訊,是不是沒有丟擲異常呢?我們起程式碼進行偵錯,看偵錯資訊:

我們看到程式中確實也丟擲了異常,但是程式卻沒有捕獲到,那麼異常去哪裡了呢?異常被多執行緒給吞掉了,那麼如何在多執行緒中捕獲異常呢?如果把try-catch寫線上程裡面呢?每一個執行緒都是單執行緒的,把try-catch寫在每一個執行緒裡面就沒有意義了。在多執行緒中捕獲異常,需要使用到WaitAll(),看下面的程式碼:

try
{
     // 定義一個Task型別的List集合
     List<Task> taskList = new List<Task>();
     for (int i = 0; i < 30; i++)
     {
            string str = $"main_{i}";
            // 開啟執行緒,並把執行緒新增到集合中
            taskList.Add(Task.Run(() =>
            {
                 Console.WriteLine($"{str} 開始了");
                 if (str.Equals("main_5"))
                 {
                     throw new Exception("main_5 發生了異常");
                 }
                 else if (str.Equals("main_11"))
                 {
                      throw new Exception("main_11 發生了異常");
                 }
                 else if (str.Equals("main_18"))
                 {
                       throw new Exception("main_18 發生了異常");
                 }
                 Console.WriteLine($"{str} 結束了");
          }));
      }

     // 等待所有執行緒都執行完
     Task.WaitAll(taskList.ToArray());
}
catch (Exception ex)
{
      Console.WriteLine($"message:{ex.Message}");
}

我們用程式碼進行偵錯,偵錯結果:

這時就可以進入到catch裡面了,我們監視ex,發現ex是AggregateException型別的異常,我們在進一步優化程式碼:

try
{
     // 定義一個Task型別的List集合
     List<Task> taskList = new List<Task>();
     for (int i = 0; i < 30; i++)
     {
            string str = $"main_{i}";
            // 開啟執行緒,並把執行緒新增到集合中
            taskList.Add(Task.Run(() =>
            {
                 Console.WriteLine($"{str} 開始了");
                 if (str.Equals("main_5"))
                 {
                     throw new Exception("main_5 發生了異常");
                 }
                 else if (str.Equals("main_11"))
                 {
                      throw new Exception("main_11 發生了異常");
                 }
                 else if (str.Equals("main_18"))
                 {
                       throw new Exception("main_18 發生了異常");
                 }
                 Console.WriteLine($"{str} 結束了");
          }));
      }

     // 等待所有執行緒都執行完
     Task.WaitAll(taskList.ToArray());
}
catch(AggregateException are)
{
     foreach (var exception in are.InnerExceptions)
     {
          Console.WriteLine(exception.Message);
     }
}
catch (Exception ex)
{
      Console.WriteLine($"message:{ex.Message}");
}

最後執行程式:

我們發現這時就可以捕獲到具體的異常資訊了。

二、執行緒取消

在上面的範例中,我們捕獲到了多執行緒中發生的異常,並且也輸出了異常資訊,但是這樣是不友好的。在實際開發中,我們使用多執行緒並行執行任務,假如其中某一個任務失敗了或者發生了異常,我們希望可以通知其他的執行緒,都停止下來,那麼該如何做呢?這時就需要使用到執行緒取消。

Task不能外部終止任務,只能自己終止自己。

.Net框架提供了CancellationTokenSource類,該類裡面有一個bool型別的屬性:IsCancellationRequested,預設是false,表示是否取消執行緒。還提供了一個Cancel()方法,該方法可以把IsCancellationRequested的屬性值設定為true,並且不能在設定回去。程式碼如下:

// 範例化物件
CancellationTokenSource cts = new CancellationTokenSource();

for (int i = 0; i < 20; i++)
{
      string str = $"main_{i}";
      // 開啟執行緒
      Task.Run(() =>
      {
             try
             {
                  Console.WriteLine($"{str} 開始了");
                  // 暫停
                  Thread.Sleep(new Random().Next(50, 100) * 100);
                  if (str.Equals("main_5"))
                  {
                       throw new Exception("main_5 發生了異常");
                  }
                  else if (str.Equals("main_11"))
                  {
                        throw new Exception("main_11 發生了異常");
                  }
                  if (cts.IsCancellationRequested == false)
                  {
                        Console.WriteLine($"{str} 結束了");
                  }
                  else
                  {
                         Console.WriteLine($"{str} 執行緒取消");
                  }

            }
            catch (Exception ex)
            {
                   // 發生了異常,將IsCancellationRequested的值設定為true
                   cts.Cancel();
                   Console.WriteLine($"message:{ex.Message}");
            }
     });
}

程式執行結果:

可以看到,當有異常發生之後,有的執行緒就被取消了。這樣就初步實現了執行緒取消。

在上面的範例中,我們是先開啟了執行緒,如果發生了異常,則取消執行緒。那麼會有這樣一種情況:執行緒中發生了異常,可能這時候有的執行緒還沒有開啟,那麼能不能就不讓這些執行緒在開啟呢?Task的Run方法有一個過載:

第二個引數就表示取消執行緒。而且CancellationTokenSource類裡面正好有這個引數:

所以,我們可以利用Run方法的過載來實現不開啟執行緒,程式碼如下:

try
{
    // 範例化物件
    CancellationTokenSource cts = new CancellationTokenSource();
    // 建立Task型別的集合
    List<Task> taskList = new List<Task>();
    for (int i = 0; i < 20; i++)
    {
        string str = $"main_{i}";
        // 開啟執行緒 Task.run 以後 新增Token 就可以在某一個執行緒發生異常之後,讓沒有開啟的執行緒不開啟了
        taskList.Add(Task.Run(() =>
        {
            try
            {
                Console.WriteLine($"{str} 開始了");
                // 暫停
                Thread.Sleep(new Random().Next(50, 100) * 10);
                if (str.Equals("main_5"))
                {
                    throw new Exception("main_5 發生了異常");
                }
                else if (str.Equals("main_11"))
                {
                    throw new Exception("main_11 發生了異常");
                }
                if (cts.IsCancellationRequested == false)
                {
                    Console.WriteLine($"{str} 結束了");
                }
                else
                {
                    Console.WriteLine($"{str} 執行緒取消");
                }

            }
            catch (Exception ex)
            {
                // 發生了異常,將IsCancellationRequested的值設定為true
                cts.Cancel();
            }

        }, cts.Token));
    }

    // 等待所有執行緒執行完
    Task.WaitAll(taskList.ToArray());
}
catch (AggregateException are)
{
    foreach (var exception in are.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}

程式執行結果:

輸出結果中有一句話:已取消一個任務,但是我們的程式碼裡面沒有列印這句話,這是從哪裡來的呢?這是因為第二個引數Token的原因,加了這個引數以後,如果就執行緒發生了異常,就不在繼續開啟執行緒。

三、臨時變數

我們先來看看下面一段程式碼:

for (int i = 0; i < 20; i++)
{
    // 開啟執行緒
    Task.Run(() =>
    {
        Task.Run(() => Console.WriteLine($"this is {i}  ThreadId: {Thread.CurrentThread.ManagedThreadId.ToString("00")}"));
    });
}

這段程式碼的輸出結果是什麼呢?我們執行程式檢視結果:

可能有人會感到疑惑:為什麼輸出的都是20呢,而不是每次迴圈變數的值?這是什麼原因呢。這是因為我們申請執行緒的時候不會發生阻塞,而且還是延遲執行的。我們知道,程式碼的執行速度是非常快的,迴圈20次幾乎一瞬間就完成了,這是i就變成了20,但是執行緒是延遲執行的,當執行緒真正去執行的時候,對應的是同一個i,這時i是20,所以輸出的都是20。那麼該如何輸出每次迴圈的值呢?看下面的程式碼:

for (int i = 0; i < 20; i++)
{
    // 定義一個新的變數
    int k = i;
    // 開啟執行緒
    Task.Run(() =>
    {
        Task.Run(() => Console.WriteLine($"this is {i}_{k}  ThreadId: {Thread.CurrentThread.ManagedThreadId.ToString("00")}"));
    });
}

程式執行結果:

這樣每次迴圈的時候,都重新定義變數k,保證每次都是全新的,所以k的值就是每次迴圈的值。

四、執行緒安全

什麼是執行緒安全呢?執行緒安全:如果你的程式碼在程序中有多個執行緒同時執行這一段,如果每次執行的結果都跟單執行緒執行時的結果一致,那麼就是執行緒安全的。

在什麼情況下會出現執行緒安全的問題呢?

一般都是有全域性變數/共用變數/靜態變數/硬碟檔案/資料庫的值,只要多執行緒存取和修改,就會出現執行緒安全的問題。看下面的程式碼:

int syncNum = 0;

int AsyncNum = 0;
for (int i = 0; i < 10000; i++)
{
    syncNum++;
}
Console.WriteLine($"syncNum={syncNum}"); //單執行緒10000   10000

for (int i = 0; i < 10000; i++)
{
    Task.Run(() =>
    {
        AsyncNum++;
    });
}
Console.WriteLine($"AsyncNum ={AsyncNum}");

程式執行結果:

這就是執行緒安全造成的問題。那麼該如何解決這個問題呢?這時可以使用lock關鍵字解決。lock關鍵字定義如下:

private static readonly object Form_Lock = new object();//鎖物件的標準寫法

修改程式碼如下:

int syncNum = 0;

int AsyncNum = 0;
for (int i = 0; i < 10000; i++)
{
    syncNum++;
}
Console.WriteLine($"syncNum={syncNum}");

for (int i = 0; i < 10000; i++)
{
    Task.Run(() =>
    {
        lock (Form_Lock)
        {
            AsyncNum++;
        }
    });
}
// 休眠5秒,等待所有執行緒都執行完畢
Thread.Sleep(5000);
Console.WriteLine($"AsyncNum ={AsyncNum}");

程式執行結果:

除了使用lock,我們還可以使用資料分拆,避免多執行緒操作同一個資料,這樣又安全又高效。

到此這篇關於C#多執行緒相關操作的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com