欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解C#異步多線程使用中的常見問題

 更新時(shí)間:2022年01月05日 11:03:46   作者:菜鳥厚非  
本文主要介紹了C#異步多線程使用中的常見問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下

異常處理

小伙伴有沒有想過,多線程的異常怎么處理,同步方法內(nèi)的異常處理,想必都非常非常熟悉了。那多線程是什么樣的呢,接著我講解多線程的異常處理

首先,我們定義個(gè)任務(wù)列表,當(dāng) 11、12 次的時(shí)候,拋出一個(gè)異常,最外圍使用 try catch 包一下

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    try
    {
        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();
        for (int i = 0; i < 20; i++)
        {
            string name = $"第 {i} 次";

            Action<object> action = t =>
            {
                Thread.Sleep(2 * 1000);
                if (name.ToString().Equals("第 11 次"))
                {
                    throw new Exception($"{t},執(zhí)行失敗");
                }
                if (name.ToString().Equals("第 12 次"))
                {
                    throw new Exception($"{t},執(zhí)行失敗");
                }
                Console.WriteLine($"{t},執(zhí)行成功");
            };

            tasks.Add(taskFactory.StartNew(action, name));
        }
    }
    catch (AggregateException aex)
    {
        foreach (var item in aex.InnerExceptions)
        {
            Console.WriteLine("Main AggregateException:" + item.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main Exception:" + ex.Message);
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

啟動程序,可以看到 vs 捕獲到了異常的代碼行,但 catch 并未捕獲到異常,這是為什么呢?是因?yàn)榫€程里面的異常被吞掉了,從運(yùn)行的結(jié)果也可以看到,main end 在子線程沒有執(zhí)行任時(shí)就已經(jīng)結(jié)束了,那說明 catch 已經(jīng)執(zhí)行過去了。

在這里插入圖片描述

那有沒有辦法捕獲多線程的異常呢?答案:有的,等待線程完成計(jì)算即可

看下面代碼,有個(gè)特殊的地方 AggregateException.InnerExceptions 專門為多線程準(zhǔn)備的,可以查看多線程異常信息

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    try
    {
        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();
        for (int i = 0; i < 20; i++)
        {
            string name = $"第 {i} 次";

            Action<object> action = t =>
            {
                Thread.Sleep(2 * 1000);
                if (name.ToString().Equals("第 11 次"))
                {
                    throw new Exception($"{t},執(zhí)行失敗");
                }
                if (name.ToString().Equals("第 12 次"))
                {
                    throw new Exception($"{t},執(zhí)行失敗");
                }
                Console.WriteLine($"{t},執(zhí)行成功");
            };

            tasks.Add(taskFactory.StartNew(action, name));
        }

        Task.WaitAll(tasks.ToArray());
    }
    catch (AggregateException aex)
    {
        foreach (var item in aex.InnerExceptions)
        {
            Console.WriteLine("Main AggregateException:" + item.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main Exception:" + ex.Message);
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

啟動線程,可以看到任務(wù)全部執(zhí)行完畢,且 AggregateException.InnerExceptions 存儲了,子線程執(zhí)行時(shí)的異常信息

在這里插入圖片描述

但 WaitAll 不好,總不能一直 WaitAll 吧,它會卡界面。并不適用于異步場景對吧,接著來看另外一直解決方案。就是子線程里不允許出現(xiàn)異常,如果有自己處理好,即 try catch 包一下,平時(shí)工作中建議這么做。

使用 try catch 將子線程執(zhí)行的代碼包一下,且在 catch 打印錯(cuò)誤信息

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    try
    {
        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();
        for (int i = 0; i < 20; i++)
        {
            string name = $"第 {i} 次";

            Action<object> action = t =>
            {
                try
                {
                    Thread.Sleep(2 * 1000);
                    if (name.ToString().Equals("第 11 次"))
                    {
                        throw new Exception($"{t},執(zhí)行失敗");
                    }
                    if (name.ToString().Equals("第 12 次"))
                    {
                        throw new Exception($"{t},執(zhí)行失敗");
                    }
                    Console.WriteLine($"{t},執(zhí)行成功");
                }
                catch (Exception ex)
                {
					Console.WriteLine(ex.Message);
                }
            };

            tasks.Add(taskFactory.StartNew(action, name));
        }
    }
    catch (AggregateException aex)
    {
        foreach (var item in aex.InnerExceptions)
        {
            Console.WriteLine("Main AggregateException:" + item.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main Exception:" + ex.Message);
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

啟動程序,可以看到任務(wù)全部執(zhí)行,且子線程異常也捕獲到

在這里插入圖片描述

線程取消

有時(shí)候會有這樣的場景,多個(gè)任務(wù)并發(fā)執(zhí)行,如果某個(gè)任務(wù)失敗了,通知其他的任務(wù)都停下來。首先打個(gè)預(yù)防針 Task 在外部無法中止的,Thread.Abort 不靠譜。其實(shí)線程取消的這個(gè)想法是錯(cuò)誤的,線程是 OS 的資源,程序是無法掌控什么時(shí)候取消,發(fā)出一個(gè)動作可能立馬取消,也可能等 1 s 取消。

解決方案:線程自己停止自己,定義公共的變量,修改變量狀態(tài),其他線程不斷檢測公共變量

例如:CancellationTokenSource 就是公共變量,初始化為 false 狀態(tài),程序執(zhí)行 CancellationTokenSource .Cancel() 方法會取消,其他線程檢測到 CancellationTokenSource .IsCancellationRequested 會是取消狀態(tài)。CancellationTokenSource.Token 在啟動 Task 時(shí)傳入,如果已經(jīng) CancellationTokenSource.Cancel() ,這個(gè)任務(wù)會放棄啟動,拋出一個(gè)異常的形式放棄。

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    try
    {
        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();
        CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); // bool 

        for (int i = 0; i < 20; i++)
        {
            string name = $"第 {i} 次";

            Action<object> action = t =>
            {
                try
                {
                    Thread.Sleep(2 * 1000);
                    if (name.ToString().Equals("第 11 次"))
                    {
                        throw new Exception($"{t},執(zhí)行失敗");
                    }
                    if (name.ToString().Equals("第 12 次"))
                    {
                        throw new Exception($"{t},執(zhí)行失敗");
                    }
                    if (cancellationTokenSource.IsCancellationRequested) // 檢測信號量
                    {
                        Console.WriteLine($"{t},放棄執(zhí)行");
                        return;
                    }
                    Console.WriteLine($"{t},執(zhí)行成功");
                }
                catch (Exception ex)
                {
                    cancellationTokenSource.Cancel();
                    Console.WriteLine(ex.Message);
                }
            };

            tasks.Add(taskFactory.StartNew(action, name,cancellationTokenSource.Token));
        }

        Task.WaitAll(tasks.ToArray());
    }
    catch (AggregateException aex)
    {
        foreach (var item in aex.InnerExceptions)
        {
            Console.WriteLine("Main AggregateException:" + item.Message);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Main Exception:" + ex.Message);
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

啟動程序,可以看到 11、12 此任務(wù)失敗,18、19 放棄了任務(wù)執(zhí)。有的小伙伴疑問了,12 之后的部分為什么執(zhí)行成功了,因?yàn)?CPU 是分時(shí)分片的嗎,會有延遲,延遲少不了。

在這里插入圖片描述

臨時(shí)變量

首先看個(gè)代碼,循環(huán) 5 次,多線程的方式,依次輸出序號

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    for (int i = 0; i < 5; i++)
    {
        Task.Run(() => {
            Console.WriteLine(i);
        });
    }

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

啟動程序,不是我們預(yù)期的結(jié)果 0、1、2、3、4,為什么是 5 個(gè) 5 呢?因?yàn)槿讨挥幸粋€(gè) i ,當(dāng)主線程執(zhí)行完畢時(shí) i = 5 ,但子線程可能還沒有開始執(zhí)行任務(wù),輪到子線程取 i 時(shí),已經(jīng)是主線程 1 循環(huán)完畢后的 5 了。

在這里插入圖片描述

改造代碼:在 for 循環(huán)內(nèi)加一行代碼 int k = i,且在子線程用的變量也改為 k

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

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

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

啟動程序,可以看到是我們預(yù)期的結(jié)果 0、1、2、3、4,為什么會這樣子呢?因?yàn)槿逃?5 個(gè) k,每次循環(huán)都會創(chuàng)建一個(gè) k 存儲當(dāng)前的 i,不同的子線程使用的也是,每次循環(huán)的 i 值。

在這里插入圖片描述

線程安全

首先為什么會有線程安全的概念呢?首先我們來看一個(gè)正常程序,如下

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    int TotalCount = 0;
    List<int> vs = new List<int>();

    for (int i = 0; i < 10000; i++)
    {
        TotalCount += 1;
        vs.Add(i);
    }

    Console.WriteLine(TotalCount);
    Console.WriteLine(vs.Count);

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

啟動程序,可以看到循環(huán) 10000 次,最終的求和與列表里的數(shù)據(jù)量都是 10000,這是正常的

在這里插入圖片描述

接著,將求和與添加列表,換成多線程,等待全部線程完成工作后,打印信息

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    int TotalCount = 0;
    List<int> vs = new List<int>();

    TaskFactory taskFactory = new TaskFactory();
    List<Task> tasks = new List<Task>();

    for (int i = 0; i < 10000; i++)
    {
        int k = i;
        tasks.Add(taskFactory.StartNew(() =>
        {
            TotalCount += 1;
            vs.Add(i);
        }));
    }

    Task.WaitAll(tasks.ToArray());

    Console.WriteLine(TotalCount);
    Console.WriteLine(vs.Count);

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

啟動程序,可以看到,兩個(gè)結(jié)果都不是 10000 呢?這就是線程安全

因?yàn)?TotalCount 是個(gè)共享的變量,當(dāng)多個(gè)線程去取 TotalCount 進(jìn)行 +1 后,線程都去放值的時(shí)候,后一個(gè)線程會替換掉前一個(gè)線程放置的值,所以就會形成做最終不是 10000 的結(jié)果。列表,可以看做是一個(gè)連續(xù)的塊,當(dāng)多線程添加的時(shí)候,也會進(jìn)行覆蓋。

在這里插入圖片描述

如何解決呢?答案:lock、安全隊(duì)列、拆分合并計(jì)算。下面對 lock 進(jìn)行講解,安全隊(duì)列與拆分合并計(jì)算,有興趣的小伙伴可以私下交流

1 .lock
第一種,通過加鎖的方式,這種也是日常工作總常用的一種。首先定義個(gè)私有的靜態(tài)引用類型的變量,然后將需要鎖的運(yùn)算放到 lock () 方法內(nèi)

在 { } 內(nèi)同一時(shí)刻,只有一個(gè)線程執(zhí)行,所以盡可能 {} 放置必要的邏輯運(yùn)行提高效率。lock 只能鎖引用類型,原理是占用這個(gè)引用鏈接。不要用 string 會享元,即如 lock() 是相同的字符串,無論定義多少個(gè)變量,其實(shí)都是一個(gè)。

internal class Program
{
    private static readonly object _lock = new object();

    static void Main(string[] args)
    {
        Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

        int TotalCount = 0;
        List<int> vs = new List<int>();

        TaskFactory taskFactory = new TaskFactory();
        List<Task> tasks = new List<Task>();

        for (int i = 0; i < 10000; i++)
        {
            int k = i;
            tasks.Add(taskFactory.StartNew(() =>
            {
                lock (_lock)
                {
                    TotalCount += 1;
                    vs.Add(i);
                }
            }));
        }

        Task.WaitAll(tasks.ToArray());

        Console.WriteLine(TotalCount);
        Console.WriteLine(vs.Count);

        Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

        Console.ReadLine();
    }
}

啟動程序,可以看到,此時(shí)在多線程的情況下,最終的結(jié)果是正常的

在這里插入圖片描述

這段代碼,是官方推薦寫法 private 防止外面也被引用,static 保證全場唯一

private static readonly object _lock = new object();

擴(kuò)展:與 lock 等價(jià)的有個(gè) Monitor,用法如下

private static object _lock = new object();

static void Main(string[] args)
{
    Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    int TotalCount = 0;
    List<int> vs = new List<int>();

    TaskFactory taskFactory = new TaskFactory();
    List<Task> tasks = new List<Task>();

    for (int i = 0; i < 10000; i++)
    {
        int k = i;
        tasks.Add(taskFactory.StartNew(() =>
        {
            Monitor.Enter(_lock);
            TotalCount += 1;
            vs.Add(i);
            Monitor.Exit(_lock);
        }));
    }

    Task.WaitAll(tasks.ToArray());

    Console.WriteLine(TotalCount);
    Console.WriteLine(vs.Count);

    Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}");

    Console.ReadLine();
}

在這里插入圖片描述

 到此這篇關(guān)于詳解C#異步多線程使用中的常見問題的文章就介紹到這了,更多相關(guān)C#異步多線程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C#繪制鼠標(biāo)指針的示例代碼

    C#繪制鼠標(biāo)指針的示例代碼

    這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)將鼠標(biāo)的指針樣式給繪制成圖片,顯示或者保存下來,文中的示例代碼講解詳細(xì),需要的可以參考一下
    2024-01-01
  • 淺談C#中Process類的使用詳解

    淺談C#中Process類的使用詳解

    本篇文章是對C#中Process類的使用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下
    2013-05-05
  • C# 通過反射獲取類型的字段值及給字段賦值的操作

    C# 通過反射獲取類型的字段值及給字段賦值的操作

    這篇文章主要介紹了C# 通過反射獲取類型的字段值及給字段賦值的操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-01-01
  • winform多線程組件BackgroundWorker使用

    winform多線程組件BackgroundWorker使用

    這篇文章介紹了winform多線程組件BackgroundWorker的使用方法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-05-05
  • C# 獲取數(shù)據(jù)庫中所有表名、列名的示例代碼

    C# 獲取數(shù)據(jù)庫中所有表名、列名的示例代碼

    這篇文章主要介紹了C# 獲取數(shù)據(jù)庫中所有表名、列名,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-06-06
  • C# 語音功能的實(shí)現(xiàn)方法

    C# 語音功能的實(shí)現(xiàn)方法

    最近看了一些資料,是c#實(shí)現(xiàn)語音功能的?,F(xiàn)在就簡單的講一下怎么實(shí)現(xiàn)。
    2013-03-03
  • C#特性(Attribute)

    C#特性(Attribute)

    這篇文章介紹了C#的特性(Attribute),文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-03-03
  • C#控制臺程序的開發(fā)與打包為一個(gè)exe文件實(shí)例詳解

    C#控制臺程序的開發(fā)與打包為一個(gè)exe文件實(shí)例詳解

    所謂控制臺程序,就是沒有界面,運(yùn)行程序后只有一個(gè)黑色的類似cmd窗口,通過這個(gè)窗口進(jìn)行交互,下面這篇文章主要給大家介紹了關(guān)于C#控制臺程序的開發(fā)與打包為一個(gè)exe文件的相關(guān)資料,需要的朋友可以參考下
    2023-05-05
  • C#使用Equals()方法比較兩個(gè)對象是否相等的方法

    C#使用Equals()方法比較兩個(gè)對象是否相等的方法

    這篇文章主要介紹了C#使用Equals()方法比較兩個(gè)對象是否相等的方法,涉及C#操作對象的相關(guān)技巧,需要的朋友可以參考下
    2015-04-04
  • windows中使用C# 調(diào)用 C語言生成的dll

    windows中使用C# 調(diào)用 C語言生成的dll

    本文給大家介紹的是在Windows系統(tǒng)中使用C#調(diào)用C語言生成的DLL文件的一種思路,非常的簡單實(shí)用,有需要的小伙伴可以參考下
    2016-11-11

最新評論