.NET異步和多線程系列(四)- 多線程異常處理、線程取消、多線程的臨時變量問題、線程安全和鎖lock


本文是.NET異步和多線程系列第四章,主要介紹的是多線程異常處理、線程取消、多線程的臨時變量問題、線程安全和鎖lock等。

一、多線程異常處理

多線程里面拋出的異常,會終結當前線程,但是不會影響別的線程。那線程異常哪里去了? 被吞了

假如想獲取異常信息,這時候要怎么辦呢?下面來看下其中的一種寫法(不推薦):

/// <summary>
/// 1 多線程異常處理和線程取消
/// 2 多線程的臨時變量
/// 3 線程安全和鎖lock
/// </summary>
private void btnThreadCore_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnThreadCore_Click Start   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

    #region 多線程異常處理

    {
        try
        {
            List<Task> taskList = new List<Task>();
            for (int i = 0; i < 100; i++)
            {
                string name = $"btnThreadCore_Click_{i}";
                taskList.Add(Task.Run(() =>
                {
                    if (name.Equals("btnThreadCore_Click_11"))
                    {
                        throw new Exception("btnThreadCore_Click_11異常");
                    }
                    else if (name.Equals("btnThreadCore_Click_12"))
                    {
                        throw new Exception("btnThreadCore_Click_12異常");
                    }
                    else if (name.Equals("btnThreadCore_Click_38"))
                    {
                        throw new Exception("btnThreadCore_Click_38異常");
                    }
                    Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                }));
            }
            //多線程里面拋出的異常,會終結當前線程,但是不會影響別的線程。
            //那線程異常哪里去了? 被吞了。
            //假如我想獲取異常信息,還需要通知別的線程
            Task.WaitAll(taskList.ToArray()); //1 可以捕獲到線程的異常
        }
        catch (AggregateException aex) //2 需要try-catch-AggregateException
        {
            foreach (var exception in aex.InnerExceptions)
            {
                Console.WriteLine(exception.Message);
            }
        }
        catch (Exception ex) //可以多catch  先具體再全部
        {
            Console.WriteLine(ex);
        }

        //線程異常后經常是需要通知別的線程,而不是等到WaitAll,問題就是要線程取消?
        //工作中常規建議:多線程的委托里面不允許異常,包一層try-catch,然后記錄下來異常信息,完成需要的操作。
    }

    #endregion 多線程異常處理

    Console.WriteLine($"****************btnThreadCore_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}

上面的這種寫法往往太極端了,一下子捕獲了所有的異常。在真實工作中,線程異常后通常是需要通知別的線程(進行線程取消),而不是等到WaitAll。

工作中常規建議:多線程的委托里面不允許異常,包一層try-catch,然后記錄下來異常信息,完成需要的操作。具體的我們往下繼續看。

二、線程取消

多線程並發任務,某個失敗后,希望通知別的線程都停下來,要如何實現呢?

Thread.Abort--終止線程;向當前線程拋一個異常然后終結任務;線程屬於OS資源,可能不會立即停下來。非常不建議這樣子去做,該方法現在也被微軟給廢棄了。

既然Task不能外部終止任務,那只能自己終止自己(上帝才能打敗自己),下面我們來看下具體的代碼:(推薦

#region 線程取消

{
    //多線程並發任務,某個失敗后,希望通知別的線程都停下來,要如何實現呢?
    //Thread.Abort--終止線程;向當前線程拋一個異常然后終結任務;線程屬於OS資源,可能不會立即停下來。非常不建議這樣子去做,該方法現在也被微軟給廢棄了。
    //Task不能外部終止任務,只能自己終止自己(上帝才能打敗自己)

    //cts有個bool屬性IsCancellationRequested 初始化是false
    //調用Cancel方法后變成true(不能再變回去),可以重復Cancel
    try
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        List<Task> taskList = new List<Task>();
        for (int i = 0; i < 50; i++)
        {
            string name = $"btnThreadCore_Click_{i}";
            taskList.Add(Task.Run(() =>
            {
                try
                {
                    if (!cts.IsCancellationRequested)
                        Console.WriteLine($"This is {name} 開始 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");

                    Thread.Sleep(new Random().Next(50, 100));

                    if (name.Equals("btnThreadCore_Click_11"))
                    {
                        throw new Exception("btnThreadCore_Click_11異常");
                    }
                    else if (name.Equals("btnThreadCore_Click_12"))
                    {
                        throw new Exception("btnThreadCore_Click_12異常");
                    }
                    else if (name.Equals("btnThreadCore_Click_13"))
                    {
                        cts.Cancel();
                    }
                    if (!cts.IsCancellationRequested)
                    {
                        Console.WriteLine($"This is {name}成功結束 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                    }
                    else
                    {
                        Console.WriteLine($"This is {name}中途停止 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
                        return;
                    }
                } catch (Exception ex) { Console.WriteLine(ex.Message); cts.Cancel(); }
            }, cts.Token));
            //加參數cts.Token目的是:在Cancel時還沒有啟動的任務,就不啟動了。
            //但是所有沒有啟動的任務都會拋出一個異常cts.Token.ThrowIfCancellationRequested
        }
        //1 准備cts  2 try-catch-cancel  3 Action要隨時判斷IsCancellationRequested
        //盡快停止,肯定有延遲,在判斷環節才會結束

        Task.WaitAll(taskList.ToArray());

        //如果線程還沒啟動,能不能就別啟動了?加參數cts.Token
        //1 啟動線程傳遞Token  2 異常抓取  
        //在Cancel時還沒有啟動的任務,就不啟動了;也是拋異常,cts.Token.ThrowIfCancellationRequested
    }
    catch (AggregateException aex)
    {
        foreach (var exception in aex.InnerExceptions)
        {
            Console.WriteLine(exception.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

#endregion 線程取消

CancellationTokenSource有個bool屬性IsCancellationRequested,初始化是false,調用Cancel方法后變成true(不能再變回去),可以重復Cancel。cts是線程安全的

值得一提的是,使用Task.Run啟動線程的時候還傳了一個cts.Token的參數,目的是:調用Cancel方法后還沒有啟動的任務,就不啟動了,實現原理是所有沒有啟動的任務都會拋出一個System.Threading.Tasks.TaskCanceledException類型的異常,異常描述為“已取消一個任務”,拋出異常后任務自然也就終止了。一般情況下我們不會主動的去捕獲這種異常

那如果想看到這種異常信息的話可以通過Task.WaitAll(taskList.ToArray())加上try{...}catch (AggregateException aex){...}這種方式去捕獲該類型的異常。

PS:可以發現上面的這段代碼在線程內部的地方加了一個異常捕獲,工作中常規建議:多線程的委托里面不允許異常,包一層try-catch,然后記錄下來異常信息,完成需要的操作。

注意:此處的線程停止也只能說是盡快停止,肯定有延遲,在判斷環節才會結束。

三、多線程的臨時變量問題

#region 多線程的臨時變量問題

{
    //多線程的臨時變量問題,線程是非阻塞的,延遲啟動的;線程執行的時候,i已經是5了。
    for (int i = 0; i < 5; i++)
    {
        Task.Run(() =>
        {
            //此處i都是5
            Console.WriteLine($"This is btnThreadCore_Click_{i} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        });
    }


    //k是閉包里面的變量,每次循環都有一個獨立的k
    //5個k變量  1個i變量
    for (int i = 0; i < 5; i++)
    {
        int k = i;
        Task.Run(() =>
        {
            Console.WriteLine($"This is btnThreadCore_Click_{i}_{k} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        });
    }
}

#endregion 多線程的臨時變量問題

運行結果如下:

四、線程安全和鎖lock

線程安全:如果你的代碼在進程中有多個線程同時運行這一段,如果每次運行的結果都跟單線程運行時的結果一致,那么就是線程安全的。

線程安全問題一般都是有全局變量/共享變量/靜態變量/硬盤文件/數據庫的值,只要是多線程都能訪問和修改的就有可能是非線程安全。

非線程安全是因為多個線程相同操作,出現了覆蓋,那要怎么解決?

方案1:使用lock解決多線程沖突現在一般不推薦使用這個,會限制並發

lock是語法糖,Monitor.Enter,占據一個引用,別的線程就只能等着。

推薦鎖是private static readonly object lockObj = new object();

首先我們來看下lock的標准寫法:

//字段
private static readonly object lockObj = new object();
private int iNumSync = 0;
private int iNumAsync = 0; //非線程安全
private int iNumLockAsync = 0;
private List<int> iListAsync = new List<int>();
{
    for (int i = 0; i < 10000; i++)
    {
        this.iNumSync++; //單線程
    }

    for (int i = 0; i < 10000; i++)
    {
        Task.Run(() =>
        {
            this.iNumAsync++; //非線程安全
        });
    }

    for (int i = 0; i < 10000; i++)
    {
        Task.Run(() =>
        {
            //lock的標准寫法
            //推薦鎖是private static readonly object lockObj = new object();
            lock (lockObj) //任意時刻只有一個線程能進入方法塊,這不就變成了單線程,限制了並發
            {
                this.iNumLockAsync++;
            }
        });
    }

    for (int i = 0; i < 10000; i++)
    {
        int k = i;
        Task.Run(() => this.iListAsync.Add(k)); //非線程安全
    }

    Thread.Sleep(5 * 1000);
    Console.WriteLine($"iNumSync={this.iNumSync} iNumAsync={this.iNumAsync} iNumLockAsync={iNumLockAsync} listNum={this.iListAsync.Count}");
    //結果:iNumSync=1000 、 iNumAsync=1到1000之間 、 iNumLockAsync=1000 、 this.iListAsync.Count=1到1000之間
}

運行結果如下:

使用lock雖然可以解決線程安全問題,但是同時也限制了並發。

使用lock的注意點:

  A 不能是lock(null),可以編譯但不能運行;

  B 不推薦lock(this),外面如果也要用實例,就沖突了;

  C 不應該是lock(string字符串),string在內存分配上是重用的,會沖突;

  D lock里面的代碼不要太多,這里是單線程的;

下面我們來看些例子:

為什么不推薦lock(this)

public class Test
{
    private int iDoTestNum = 0;
    private string name = "浪子天涯";

    /// <summary>
    /// 鎖this會和外部鎖對象實例沖突
    /// </summary>
    public void DoTest()
    {
        //遞歸調用,lock (this)  會不會死鎖? 正確答案是不會死鎖!
        //這里是同一個線程,這個引用就是被這個線程所占據。
        lock (this)
        {
            Thread.Sleep(500);
            this.iDoTestNum++;
            if (this.iDoTestNum < 10)
            {
                Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}");
                this.DoTest();
            }
            else
            {
                Console.WriteLine("28號,課程結束!!");
            }
        }
    }

    /// <summary>
    /// 此次鎖字符串會和外部鎖值相同的字符串沖突
    /// 這是因為相同的字符串會被指向同一塊引用,這就相當於鎖同一個引用,即同一個鎖
    /// </summary>
    public void DoTestString()
    {
        //此次不會死鎖
        //這里是同一個線程,這個引用就是被這個線程所占據。
        lock (this.name)
        {
            Thread.Sleep(500);
            this.iDoTestNum++;
            if (this.iDoTestNum < 10)
            {
                Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}");
                this.DoTestString();
            }
            else
            {
                Console.WriteLine("28號,課程結束!!");
            }
        }
    }
}
#region 線程安全和鎖lock

{
    //線程安全:如果你的代碼在進程中有多個線程同時運行這一段,如果每次運行的結果都跟單線程運行時的結果一致,那么就是線程安全的。
    //線程安全問題一般都是有全局變量/共享變量/靜態變量/硬盤文件/數據庫的值,只要是多線程都能訪問和修改的就有可能是非線程安全。
    //非線程安全是因為多個線程相同操作,出現了覆蓋,那要怎么解決?

    //1、使用lock解決多線程沖突
    //lock是語法糖,Monitor.Enter,占據一個引用,別的線程就只能等着。
    //推薦鎖是private static readonly object lockObj = new object();
    //A 不能是lock(null),可以編譯但不能運行;
    //B 不推薦lock(this),外面如果也要用實例,就沖突了;
    //C 不應該是lock(string字符串),string在內存分配上是重用的,會沖突;
    //D lock里面的代碼不要太多,這里是單線程的;

    Test test = new Test();
    Task.Delay(1000).ContinueWith(t =>
    {
        lock (test) //和Test內部的lock(this)是同一個鎖,故此次盡管是子線程也要排隊等待
        {
            Console.WriteLine("*********lock(this) Start*********");
            Thread.Sleep(2000);
            Console.WriteLine("*********lock(this) End*********");
        }
    });
    test.DoTest();
}

#endregion 線程安全和鎖lock

運行結果如下:

仔細觀察會發現Task子線程的任務會等到test.DoTest()的任務執行完后才會執行,這是為什么呢?

有些人可能就會有疑問了,此處鎖this和鎖test實例看上去應該是2把鎖,互不影響才對啊,那為什么又會沖突呢?

實際上此處的this和test是同一個實例,那么鎖的當然也是同一個引用,故相當於是同一把鎖。

那又為什么不應該鎖string字符串呢?

我們在上面的例子上做一些調整如下所示:

#region 線程安全和鎖lock

{
    //線程安全:如果你的代碼在進程中有多個線程同時運行這一段,如果每次運行的結果都跟單線程運行時的結果一致,那么就是線程安全的。
    //線程安全問題一般都是有全局變量/共享變量/靜態變量/硬盤文件/數據庫的值,只要是多線程都能訪問和修改的就有可能是非線程安全。
    //非線程安全是因為多個線程相同操作,出現了覆蓋,那要怎么解決?

    //1、使用lock解決多線程沖突
    //lock是語法糖,Monitor.Enter,占據一個引用,別的線程就只能等着。
    //推薦鎖是private static readonly object lockObj = new object();
    //A 不能是lock(null),可以編譯但不能運行;
    //B 不推薦lock(this),外面如果也要用實例,就沖突了;
    //C 不應該是lock(string字符串),string在內存分配上是重用的,會沖突;
    //D lock里面的代碼不要太多,這里是單線程的;

    {
        //    Test test = new Test();
        //    Task.Delay(1000).ContinueWith(t =>
        //    {
        //        lock (test) //和Test內部的lock(this)是同一個鎖,故此次盡管是子線程也要排隊等待
        //        {
        //            Console.WriteLine("*********lock(this) Start*********");
        //            Thread.Sleep(2000);
        //            Console.WriteLine("*********lock(this) End*********");
        //        }
        //    });
        //    test.DoTest();
    }

    {
        Test test = new Test();
        string student = "浪子天涯";
        Task.Delay(1000).ContinueWith(t =>
        {
            lock (student)
            {
                Console.WriteLine("*********lock(string) Start*********");
                Thread.Sleep(2000);
                Console.WriteLine("*********lock(string) End*********");
            }
        });
        test.DoTestString();
    }
}

#endregion 線程安全和鎖lock

運行結果如下:

仔細觀察會發現這和lock(this)的效果是一樣的,那這又是為什么呢?

這是由於C#內存分配導致的,相同的字符串會被指向同一塊引用空間,那么此處的鎖this.name變量和鎖student變量就相當於鎖同一個引用,故相當於是同一把鎖

方案2:線程安全集合

使用System.Collections.Concurrent.ConcurrentQueue<int>等相關操作,System.Collections.Concurrent命名空間下的相關操作是線程安全的。

方案3:數據分拆,避免多線程操作同一個數據,又安全又高效推薦

在真實工作中遇到線程不安全的情況,如果有辦法使用數據分拆來解決則推薦使用數據分拆,數據分拆無法解決的時候再考慮使用鎖。

 

Demo源碼:

鏈接:https://pan.baidu.com/s/1Eaet92HhGoK9sHjXhz_VsA 
提取碼:7st0

此文由博主精心撰寫轉載請保留此原文鏈接:https://www.cnblogs.com/xyh9039/p/13592042.html

版權聲明:如有雷同純屬巧合,如有侵權請及時聯系本人修改,謝謝!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM