.NET異步和多線程系列(三)- Task和Parallel


一、Task類

Task是.NET Framework 3.0出現的,線程是基於線程池的,然后提供了豐富的API。Task被稱之為多線程的最佳實踐

首先我們來看下如何使用Task來啟動線程:

/// <summary>
/// 一個比較耗時耗資源的私有方法
/// </summary>
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}***************");
}
/// <summary>
/// Task是.NET Framework 3.0出現的,線程是基於線程池的,然后提供了豐富的API。
/// </summary>
private void btnTask_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnTask_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

    {
        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"));
    }

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

Task的線程是源於線程池,線程池是單例的,全局唯一的。

//Task的線程是源於線程池
//線程池是單例的,全局唯一的
{
    //設置的最大值,必須大於CPU核數,否則設置無效
    //全局的,請不要這樣設置!!!此處設置只是為了演示
    ThreadPool.SetMaxThreads(12, 12);

    //設置后,同時並發的Task只有12個;而且線程是復用的;
    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);
        });
    }
    //假如說我想控制下Task的並發數量,該怎么做?
}

注意:線程池的線程數量,設置的最大值,必須大於CPU核數,否則設置無效。

運行結果如下:

從結果中可以看出同時並發的Task只有12個(線程ID從03到14);而且線程是復用的;

同步等待:

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

異步等待(Task.Delay方法,一般結合ContinueWith一起用),如下所示:

{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Console.WriteLine("在Delay之前");
    Task task = Task.Delay(2000) //異步等待--等待2s后啟動新任務
        .ContinueWith(t =>
        {
            stopwatch.Stop();
            Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}");

            Console.WriteLine($"This is ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        });
    Console.WriteLine("在Delay之后");
    //stopwatch.Stop();
    //Console.WriteLine($"Delay耗時{stopwatch.ElapsedMilliseconds}");
}

什么時候能用多線程? 任務能並發的時候。多線程能干嘛?提升速度/優化用戶體驗。

ContinueWhenAny:同時並發多個任務,當其中任意一個任務完成的時候,也可能是多個任務同時第一時間完成的時候,執行后續的任務。非阻塞式的回調

ContinueWhenAll:同時並發多個任務,當所有的任務都完成的時候,執行后續的任務。非阻塞式的回調

WaitAny:阻塞當前線程,等着任意一個任務完成。

WaitAll:阻塞當前線程,等着全部任務完成。

/// <summary>
/// 模擬講課
/// </summary>
private void Teach(string lesson)
{
    Console.WriteLine($"{lesson}開始講。。。");
    long lResult = 0;
    for (int i = 0; i < 1_000_000_000; i++)
    {
        lResult += i;
    }
    Console.WriteLine($"{lesson}講完了。。。");
}

/// <summary>
/// 模擬編程過程
/// </summary>
private void Coding(string name, string projectName)
{
    Console.WriteLine($"****************Coding Start  {name} {projectName}  {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;
    }

    Console.WriteLine($"****************Coding   End  {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
}
{
    //什么時候能用多線程? 任務能並發的時候
    //多線程能干嘛?提升速度/優化用戶體驗
    Console.WriteLine("Eleven開啟了一學期的課程");
    this.Teach("Lesson1");
    this.Teach("Lesson2");
    this.Teach("Lesson3");
    //不能並發,因為有嚴格順序(只有Eleven講課)
    Console.WriteLine("部署一下項目實戰作業,需要多人合作完成");

    //開發可以多人合作---多線程--提升性能
    TaskFactory taskFactory = new TaskFactory();
    List<Task> taskList = new List<Task>();
    taskList.Add(taskFactory.StartNew(() => this.Coding("張三", "Portal")));
    taskList.Add(taskFactory.StartNew(() => this.Coding("李四", "DBA ")));
    taskList.Add(taskFactory.StartNew(() => this.Coding("王五", "Client")));
    taskList.Add(taskFactory.StartNew(() => this.Coding("趙六", "BackService")));
    taskList.Add(taskFactory.StartNew(() => this.Coding("錢七", "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("Eleven准備環境開始部署");
    //需要能夠等待全部線程完成任務再繼續  阻塞當前線程,等着全部任務完成
    Task.WaitAll(taskList.ToArray());
    Console.WriteLine("5個模塊全部完成后,Eleven集中點評");
    //Task.WaitAny  WaitAll都是阻塞當前線程,等任務完成后執行操作

    //阻塞卡界面,是為了並發以及順序控制
    //網站首頁:A數據庫 B接口 C分布式服務 D搜索引擎,適合多線程並發,都完成后才能返回給用戶,需要等待WaitAll
    //列表頁:核心數據可能來自數據庫/接口服務/分布式搜索引擎/緩存,多線程並發請求,哪個先完成就用哪個結果,其他的就不管了
}

啟動帶有回調帶有返回值的線程:

{
    //帶有回調
    Task.Run(() => this.DoSomethingLong("btnTask_Click")).ContinueWith(t => Console.WriteLine($"btnTask_Click已完成" +
        $"{Thread.CurrentThread.ManagedThreadId.ToString("00")}"));
}

{
    //帶有返回值
    Task<int> result = Task.Run<int>(() =>
        {
            Thread.Sleep(2000);
            return DateTime.Now.Year;
        });
    int i = result.Result;//會阻塞
}

控制Task的並發數量,該怎么做?(下文會介紹更好的解決方案)

{
    //假如說我想控制下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);
        }));
    }
}

想使用返回值但又不想阻塞線程怎么辦?

//想使用返回值但又不想阻塞線程怎么辦?
{
    Task.Run<int>(() =>
    {
        Thread.Sleep(2000);
        return DateTime.Now.Year;
    }).ContinueWith(tInt =>
    {
        int i = tInt.Result; //不會阻塞
    });

    //不會阻塞
    Task<int> result = Task.Run<int>(() =>
    {
        Thread.Sleep(2000);
        return DateTime.Now.Year;
    });
    Task.Run(() =>
    {
        int i = result.Result;
    });
}

如果在啟動線程的時候想帶一些參數信息,則可以使用對應的重載方法:

{
    TaskFactory taskFactory = new TaskFactory();
    List<Task> taskList = new List<Task>();
    taskList.Add(taskFactory.StartNew(o => this.Coding("張三", "Portal"), "張三"));
    taskList.Add(taskFactory.StartNew(o => this.Coding("李四", "  DBA "), "李四"));
    taskList.Add(taskFactory.StartNew(o => this.Coding("王五", "Client"), "王五"));
    taskList.Add(taskFactory.StartNew(o => this.Coding(" 趙六", "BackService"), " 趙六"));
    taskList.Add(taskFactory.StartNew(o => this.Coding("錢七", "Wechat"), "錢七"));

    //誰第一個完成,獲取一個紅包獎勵
    taskFactory.ContinueWhenAny(taskList.ToArray(), t => Console.WriteLine($"{t.AsyncState}開發完成,獲取個紅包獎勵" +
        $"{Thread.CurrentThread.ManagedThreadId.ToString("00")}"));
}

幾乎90%以上的多線程場景,以及順序控制,以上的Task的方法就可以完成。

如果你的多線程場景太復雜搞不定,那么請梳理一下你的流程,簡化一下。

建議最好不要線程嵌套線程,兩層勉強能懂,三層hold不住的,更多只能求神。

 

二、Parallel類

Parallel可以啟動多個線程去並發執行多個Action,它是多線程的。Parallel最直觀的特點是主線程(當前線程)也會參與計算---阻塞界面(主線程忙於計算,無暇他顧)

Parallel是在Task的基礎上做了一個封裝,它的效果等於TaskWaitAll+主線程(當前線程)參與計算

/// <summary>
/// Parallel
/// </summary>
private void btnParallel_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnParallel_Click Start   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

    {
        //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"));
    }

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

運行結果如下:

可以看出線程ID為01的主線程(當前線程)也參與計算了。

Parallel其他的API如下所示:

//主線程(當前線程)會參與計算
{
    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}"));
}

如何控制並發(線程)數量?上面我們在介紹Task的時候已經寫了一種解決方案,其實還有一種更好的解決方案,就是使用Parallel來實現。

//控制線程數量(並發數量)
//會阻塞當前線程(主線程)-- 卡界面
{
    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}"));
    });
}

 

Demo源碼:

鏈接:https://pan.baidu.com/s/1SpbyPnohojyakxCOu4S9DA 
提取碼:mwd1

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

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


免責聲明!

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



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