Threads(非同步和多線程)

来源:https://www.cnblogs.com/taotaozhuanyong/archive/2019/09/20/11559834.html
-Advertisement-
Play Games

Task是.NET Framework3.0出現的,線程是基於線程池的,然後提供豐富的api,Thread方法很多很強大,但是太過強大,沒有限制。 DoSomethingLong方法如下: /// <summary> /// 一個比較耗時耗資源的私有方法 /// </summary> /// <pa ...


Task是.NET Framework3.0出現的,線程是基於線程池的,然後提供豐富的api,Thread方法很多很強大,但是太過強大,沒有限制。

DoSomethingLong方法如下:

 /// <summary>
 /// 一個比較耗時耗資源的私有方法
 /// </summary>
 /// <param name="name"></param>
 private void DoSomethingLong(string name)
 {
     Console.WriteLine($"****************DoSomethingLong Start  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
     long lResult = 0;
     for (int i = 0; i < 1_000_000_000; i++)
     {
         lResult += i;
     }
     Thread.Sleep(2000);

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

Task的使用:

{
    Task task = new Task(() => this.DoSomethingLong("btnTask_Click_1"));
    task.Start();
}
{
    Task task = Task.Run(() => this.DoSomethingLong("btnTask_Click_2"));
}
{
    TaskFactory taskFactory = Task.Factory;
    Task task = taskFactory.StartNew(() => this.DoSomethingLong("btnTask_Click_3"));
}

 

 

 如果這樣去調用:

ThreadPool.SetMaxThreads(8, 8);
for (int i = 0; i < 100; i++)
{
    int k = i;
    Task.Run(() =>
    {
        Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(2000);
    });
}

 

 

如果去掉設置最大線程的代碼:

for (int i = 0; i < 100; i++)
{
    int k = i;
    Task.Run(() =>
    {
        Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(2000);
    });
}

運行結果如下:

 

 

 

 ThreadPool.SetMaxThreads(8, 8);

  線程池是單例的,全局唯一的,設置後,同時併發的Task只有8個,而且是復用的,Task的線程是源於線程池的,全局的,請不要這樣設置。

假如我想控制下Task的併發數量,改怎麼做?

{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("在Sleep之前");
    Thread.Sleep(2000);//同步等待--當前線程等待2s 然後繼續
    Console.WriteLine("在Sleep之後");
    stopwatch.Stop();
    Console.WriteLine($"Sleep耗時{stopwatch.ElapsedMilliseconds}");
}
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("在Delay之前");
    Task task = Task.Delay(2000)
        .ContinueWith(t =>
        {
            stopwatch.Stop();
            Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}");

            Console.WriteLine($"This is ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        });//非同步等待--等待2s後啟動新任務
    Console.WriteLine("在Delay之後");
    stopwatch.Stop();
    Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}");
}

運行結果如下:

  

 

 如果將最後一個stopwatch註釋掉:

 

 {
     Stopwatch stopwatch = new Stopwatch();
     stopwatch.Start();
     Console.WriteLine("在Sleep之前");
     Thread.Sleep(2000);//同步等待--當前線程等待2s 然後繼續
     Console.WriteLine("在Sleep之後");
     stopwatch.Stop();
     Console.WriteLine($"Sleep耗時{stopwatch.ElapsedMilliseconds}");
 }
 {
     Stopwatch stopwatch = new Stopwatch();
     stopwatch.Start();
     Console.WriteLine("在Delay之前");
     Task task = Task.Delay(2000)
         .ContinueWith(t =>
         {
             stopwatch.Stop();
             Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}");

             Console.WriteLine($"This is ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
         });//非同步等待--等待2s後啟動新任務
     Console.WriteLine("在Delay之後");
     //stopwatch.Stop();
     //Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}");
 }

 

 

 

什麼時候用多線程?

  任務併發是時候

多線程能幹嘛?

  提升速度,優化用戶體驗。

 

比如,現在有一個場景,在公司開會,領導在分配任務,不能併發,因為只能有一個領導在講話分配任務,當任務分配下去,開發們確實可以同時開始擼代碼,這個是可以併發的。

 TaskFactory taskFactory = new TaskFactory();
 List<Task> taskList = new List<Task>();
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle1", "Portal")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle2", "  DBA ")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle3", "Client")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle4", "BackService")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle5", "Wechat")));

 

 現在要求,誰第一個完成,獲得紅包獎勵(ContinueWhenAny);所有完成後,一起慶祝下(ContinueWhenAll),將其放入一個List<Task>裡面去

 TaskFactory taskFactory = new TaskFactory();
 List<Task> taskList = new List<Task>();
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle1", "Portal")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle2", "  DBA ")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle3", "Client")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle4", "BackService")));
 taskList.Add(taskFactory.StartNew(() => this.Coding("bingle5", "Wechat")));

 //誰第一個完成,獲取一個紅包獎勵
 taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"XXX開發完成,獲取個紅包獎勵{Thread.CurrentThread.ManagedThreadId.ToString("00")}"));
 //項目完成後,一起慶祝一下
 taskList.Add(taskFactory.ContinueWhenAll(taskList.ToArray(), rArray => Console.WriteLine($"開發都完成,一起慶祝一下{Thread.CurrentThread.ManagedThreadId.ToString("00")}")));

ContinueWhenAny  ContinueWhenAll 非阻塞式的回調;而且使用的線程可能是新線程,也可能是剛完成任務的線程,唯一不可能是主線程

//阻塞當前線程,等著任意一個任務完成
Task.WaitAny(taskList.ToArray());//也可以限時等待
Console.WriteLine("準備環境開始部署");
//需要能夠等待全部線程完成任務再繼續  阻塞當前線程,等著全部任務完成
Task.WaitAll(taskList.ToArray());
Console.WriteLine("5個模塊全部完成後,集中點評");

 

   Task.WaitAny  WaitAll都是阻塞當前線程,等任務完成後執行操作,阻塞卡界面,是為了併發以及順序控制,網站首頁:A資料庫 B介面 C分散式服務 D搜索引擎,適合多線程併發,都完成後才能返回給用戶,需要等待WaitAll,列表頁:核心數據可能來自資料庫/介面服務/分散式搜索引擎/緩存,多線程併發請求,哪個先完成就用哪個結果,其他的就不管了。

 假如說我想控制下Task的併發數量,該怎麼做?  20個

 List<Task> taskList = new List<Task>();
 for (int i = 0; i < 10000; i++)
 {
     int k = i;
     if (taskList.Count(t => t.Status != TaskStatus.RanToCompletion) >= 20)
     {
         Task.WaitAny(taskList.ToArray());
         taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
     }
     taskList.Add(Task.Run(() =>
     {
         Console.WriteLine($"This is {k} running ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
         Thread.Sleep(2000);
     }));
 }

 

Parallel併發執行多個Action線程,主線程會參與計算---阻塞界面。等於TaskWaitAll+主線程計算

 Parallel.Invoke(() => this.DoSomethingLong("btnParallel_Click_1"),
     () => this.DoSomethingLong("btnParallel_Click_2"),
     () => this.DoSomethingLong("btnParallel_Click_3"),
     () => this.DoSomethingLong("btnParallel_Click_4"),
     () => this.DoSomethingLong("btnParallel_Click_5"));
Parallel.For(0, 5, i => this.DoSomethingLong($"btnParallel_Click_{i}"));
Parallel.ForEach(new int[] { 0, 1, 2, 3, 4 }, i => this.DoSomethingLong($"btnParallel_Click_{i}"));

ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 3;
Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}"));

有沒有辦法不阻塞?

Task.Run(() =>
{
    ParallelOptions options = new ParallelOptions();
    options.MaxDegreeOfParallelism = 3;
    Parallel.For(0, 10, options, i => this.DoSomethingLong($"btnParallel_Click_{i}"));
});

  幾乎90%以上的多線程場景,以及順序控制,以上的Task的方法就可以完成,如果你的多線程場景太複雜搞不定,那麼請梳理一下你的流程,簡化一下。建議最好不要線程嵌套線程,兩三次勉強能懂,三層就hold不住了,更多的只能求神。

 

多線程異常:

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,然後記錄下來異常信息,完成需要的操作

線程取消:

                //多線程併發任務,某個失敗後,希望通知別的線程,都停下來,how?
                //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));
                    }
                    //1 準備cts  2 try-catch-cancel  3 Action要隨時判斷IsCancellationRequested
                    //儘快停止,肯定有延遲,在判斷環節才會結束

                    Task.WaitAll(taskList.ToArray());
                    //如果線程還沒啟動,能不能就別啟動了?
                    //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);
                }
View Code

臨時變數:

 for (int i = 0; i < 5; i++)
 {
     Task.Run(() =>
     {
         Console.WriteLine($"This is btnThreadCore_Click_{i} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
     });
 }

 

 為什麼運行結果後,都是5呢?

  臨時變數問題,線程是非阻塞的,延遲啟動的;線程執行的時候,i已經是5了

那麼該如何解決呢?

  每次都聲明一個變數k去接收,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")}");
    });
}

這樣再運行,結果就正常了。

 

 線程安全&lock:

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

線程安全問題一般都是有全局變數/共用變數/靜態變數/硬碟文件/資料庫的值,只要多線程都能訪問和修改

發生是因為多個線程相同操作,出現了覆蓋,怎麼解決?

1 Lock解決多線程衝突

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

推薦鎖是private static readonly object,

 A不能是Null,可以編譯不能運行;

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

//Test test = new Test();
//Task.Delay(1000).ContinueWith(t =>
//{
//    lock (test)
//    {
//        Console.WriteLine("*********Start**********");
//        Thread.Sleep(5000);
//        Console.WriteLine("*********End**********");
//    }
//});
//test.DoTest();

//C 不應該是string; string在記憶體分配上是重用的,會衝突
//D Lock裡面的代碼不要太多,這裡是單線程的
Test test = new Test();
string student = "水煮魚";
Task.Delay(1000).ContinueWith(t =>
{
    lock (student)
    {
        Console.WriteLine("*********Start**********");
        Thread.Sleep(5000);
        Console.WriteLine("*********End**********");
    }
});
test.DoTestString();
//2 線程安全集合
//System.Collections.Concurrent.ConcurrentQueue<int>

//3 數據分拆,避免多線程操作同一個數據;又安全又高效

for (int i = 0; i < 10000; i++)
{
    this.iNumSync++;
}
for (int i = 0; i < 10000; i++)
{
    Task.Run(() =>
    {
        lock (Form_Lock)//任意時刻只有一個線程能進入方法塊兒,這不就變成了單線程
        {
            this.iNumAsync++;
        }
    });
}
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} listNum={this.iListAsync.Count}");
//iNumSync 和  iNumAsync分別是多少   9981/9988  1到10000以內

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 許久未用Eclipse, Ubuntu上也沒裝Eclipse, 今天安裝發現, 好多東西都忘了. 不過經過一番查找(百度, csdn) 終於還是安裝好了. 於是寫下這篇博客記錄下來; 以後要用的時候看看, 看別人的這一塊那一塊, 這個版本, 那個版本實在是太難受了.整合一下, 一個完整的, 截止到... ...
  • 由於公司業務要求,西瓜代理已經不滿足需求,準備更換新的代理IP池,所以調研測試了一下市面上的各家付費代理(免費代理可用率低故不考慮),功能限制和價格情況等如何,以便從中挑選滿足要求的代理。 1、目標站 2、情報收集 整理套餐的價格和類型,API頻率,每秒提取上限,每天提取上限,使用時長等信息: (p ...
  • 2.1 你對軟體工程專業或者電腦科學與技術專業瞭解是怎樣? 軟體工程是一個比較熱門的行業,也是一門新行業,它是隨著電腦的產生應運而生的,現在我們走入信息化時代,此行業也會越來越熱門,我們也將成為此行業的中流砥柱。該行業所涉及的東西也十分廣泛,數學英語都是不可拉下的,既然如此,大家對此專業含金量也應 ...
  • 在原有的工程上,創建一個新的工程 創建service-zuul工程 其pom.xml文件如下: <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="ht ...
  • 一、填空題 1.true、false 2.基本數據類型、引用數據類型 3.&、&&、|、|| 4.5 5.56 二、判斷題 1.× 2.√ 3.× continue語句只能用於迴圈語句,碰到continue語句就表示不執行後面的語句,直接轉到下一次迴圈的開始。 4.√ 5.× 三、選擇題 1.AD ...
  • [TOC] 序言:這一章我們學習字元串、列表、元組、字典 等常用存儲結構 1. 字元串 所謂 字元串 ,就是由零個或多個字元組成的有限序列 ,簡單來說:雙引號或者單引號中的數據,就是字元串 通過下麵代碼,我們來瞭解一下字元串的使用 輸出結果   str = "helloworld" | 方 ...
  • //程式崩潰規避 //Windows1、設置編譯器"Enable C++ Exceptions"為"/EHa",即"Yes with SEH Exceptions", 使得應用程式可以捕獲因自身引起的大部分系統異常,少部分不可迴避的系統異常依然 會導致程式崩潰退出;2、用"try{}catch(.. ...
  • 關於限流 常用的限流演算法有漏桶演算法和令牌桶演算法,guava的RateLimiter使用的是令牌桶演算法,也就是以固定的頻率向桶中放入令牌,例如一秒鐘10枚令牌,實際業務在每次響應請求之前都從桶中獲取令牌,只有取到令牌的請求才會被成功響應,獲取的方式有兩種:阻塞等待令牌或者取不到立即返回失敗,下圖來自網 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...