深入學習C#多線程

一、基本概念

1、進程

首先打開任務管理器,查看當前運行的進程:

從任務管理器裡面可以看到當前所有正在運行的進程。那麼究竟什麼是進程呢?

進程(Process)是Windows系統中的一個基本概念,它包含著一個運行程序所需要的資源。一個正在運行的應用程序在操作系統中被視為一個進程,進程可以包括一個或多個線程。線程是操作系統分配處理器時間的基本單元,在進程中可以有多個線程同時執行代碼。進程之間是相對獨立的,一個進程無法訪問另一個進程的數據(除非利用分佈式計算方式),一個進程運行的失敗也不會影響其他進程的運行,Windows系統就是利用進程把工作劃分為多個獨立的區域的。進程可以理解為一個程序的基本邊界。是應用程序的一個運行例程,是應用程序的一次動態執行過程。

2、線程

在任務管理器裡面查詢當前總共運行的線程數:

線程(Thread)是進程中的基本執行單元,是操作系統分配CPU時間的基本單位,一個進程可以包含若幹個線程,在進程入口執行的第一個線程被視為這個進程的主線程。在.NET應用程序中,都是以Main()方法作為入口的,當調用此方法時系統就會自動創建一個主線程。線程主要是由CPU寄存器、調用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄當前所執行線程的狀態,調用棧主要用於維護線程所調用到的內存與數據,TLS主要用於存放線程的狀態信息。

二、多線程

多線程的優點:可以同時完成多個任務;可以使程序的響應速度更快;可以讓占用大量處理時間的任務或當前沒有進行處理的任務定期將處理時間讓給別的任務;可以隨時停止任務;可以設置每個任務的優先級以優化程序性能。

那麼可能有人會問:為什麼可以多線程執行呢?總結起來有下面兩方面的原因:

1、CPU運行速度太快,硬件處理速度跟不上,所以操作系統進行分時間片管理。這樣,從宏觀角度來說是多線程並發的,因為CPU速度太快,察覺不到,看起來是同一時刻執行瞭不同的操作。但是從微觀角度來講,同一時刻隻能有一個線程在處理。

2、目前電腦都是多核多CPU的,一個CPU在同一時刻隻能運行一個線程,但是多個CPU在同一時刻就可以運行多個線程。

然而,多線程雖然有很多優點,但是也必須認識到多線程可能存在影響系統性能的不利方面,才能正確使用線程。不利方面主要有如下幾點:

  • (1)線程也是程序,所以線程需要占用內存,線程越多,占用內存也越多。
  • (2)多線程需要協調和管理,所以需要占用CPU時間以便跟蹤線程。
  • (3)線程之間對共享資源的訪問會相互影響,必須解決爭用共享資源的問題。
  • (4)線程太多會導致控制太復雜,最終可能造成很多程序缺陷。

當啟動一個可執行程序時,將創建一個主線程。在默認的情況下,C#程序具有一個線程,此線程執行程序中以Main方法開始和結束的代碼,Main()方法直接或間接執行的每一個命令都有默認線程(主線程)執行,當Main()方法返回時此線程也將終止。

一個進程可以創建一個或多個線程以執行與該進程關聯的部分程序代碼。在C#中,線程是使用Thread類處理的,該類在System.Threading命名空間中。使用Thread類創建線程時,隻需要提供線程入口,線程入口告訴程序讓這個線程做什麼。通過實例化一個Thread類的對象就可以創建一個線程。創建新的Thread對象時,將創建新的托管線程。Thread類接收一個ThreadStart委托或ParameterizedThreadStart委托的構造函數,該委托包裝瞭調用Start方法時由新線程調用的方法,示例代碼如下:

Thread thread=new Thread(new ThreadStart(method));//創建線程

thread.Start(); //啟動線程

上面代碼實例化瞭一個Thread對象,並指明將要調用的方法method(),然後啟動線程。ThreadStart委托中作為參數的方法不需要參數,並且沒有返回值。ParameterizedThreadStart委托一個對象作為參數,利用這個參數可以很方便地向線程傳遞參數,示例代碼如下:

Thread thread=new Thread(new ParameterizedThreadStart(method));//創建線程

thread.Start(3); //啟動線程

創建多線程的步驟:

  • 1、編寫線程所要執行的方法
  • 2、實例化Thread類,並傳入一個指向線程所要執行方法的委托。(這時線程已經產生,但還沒有運行)
  • 3、調用Thread實例的Start方法,標記該線程可以被CPU執行瞭,但具體執行時間由CPU決定

2.1 System.Threading.Thread類

Thread類是是控制線程的基礎類,位於System.Threading命名空間下,具有4個重載的構造函數:

名稱 說明
Thread(ParameterizedThreadStart)

初始化 Thread 類的新實例,指定允許對象在線程啟動時傳遞給線程的委托。要執行的方法是有參的。

Thread(ParameterizedThreadStart, Int32) 初始化 Thread 類的新實例,指定允許對象在線程啟動時傳遞給線程的委托,並指定線程的最大堆棧大小
Thread(ThreadStart)

初始化 Thread 類的新實例。要執行的方法是無參的。

Thread(ThreadStart, Int32)

初始化 Thread 類的新實例,指定線程的最大堆棧大小。

ThreadStart是一個無參的、返回值為void的委托。委托定義如下:

public delegate void ThreadStart()

通過ThreadStart委托創建並運行一個線程:

class Program
    {
        static void Main(string[] args)
        {
            //創建無參的線程
            Thread thread1 = new Thread(new ThreadStart(Thread1));
            //調用Start方法執行線程
            thread1.Start();

            Console.ReadKey();
        }

        /// <summary>
        /// 創建無參的方法
        /// </summary>
        static void Thread1()
        {
            Console.WriteLine("這是無參的方法");
        }
    }

運行結果

除瞭可以運行靜態的方法,還可以運行實例方法

class Program
    {
        static void Main(string[] args)
        {
            //創建ThreadTest類的一個實例
            ThreadTest test=new ThreadTest();
            //調用test實例的MyThread方法
            Thread thread = new Thread(new ThreadStart(test.MyThread));
            //啟動線程
            thread.Start();
            Console.ReadKey();
        }
    }

    class ThreadTest
    {
        public void MyThread()
        {
            Console.WriteLine("這是一個實例方法");
        }
    }

運行結果:

如果為瞭簡單,也可以通過匿名委托或Lambda表達式來為Thread的構造方法賦值

static void Main(string[] args)
 {
       //通過匿名委托創建
       Thread thread1 = new Thread(delegate() { Console.WriteLine("我是通過匿名委托創建的線程"); });
       thread1.Start();
       //通過Lambda表達式創建
       Thread thread2 = new Thread(() => Console.WriteLine("我是通過Lambda表達式創建的委托"));
       thread2.Start();
       Console.ReadKey();
 }

運行結果:

ParameterizedThreadStart是一個有參的、返回值為void的委托,定義如下:

public delegate void ParameterizedThreadStart(Object obj)

class Program
    {
        static void Main(string[] args)
        {
            //通過ParameterizedThreadStart創建線程
            Thread thread = new Thread(new ParameterizedThreadStart(Thread1));
            //給方法傳值
            thread.Start("這是一個有參數的委托");
            Console.ReadKey();
        }

        /// <summary>
        /// 創建有參的方法
        /// 註意:方法裡面的參數類型必須是Object類型
        /// </summary>
        /// <param name="obj"></param>
        static void Thread1(object obj)
        {
            Console.WriteLine(obj);
        }
    }

註意:ParameterizedThreadStart委托的參數類型必須是Object的。如果使用的是不帶參數的委托,不能使用帶參數的Start方法運行線程,否則系統會拋出異常。但使用帶參數的委托,可以使用thread.Start()來運行線程,這時所傳遞的參數值為null。

2.2 線程的常用屬性

屬性名稱 說明
CurrentContext 獲取線程正在其中執行的當前上下文。
CurrentThread 獲取當前正在運行的線程。
ExecutionContext 獲取一個 ExecutionContext 對象,該對象包含有關當前線程的各種上下文的信息。
IsAlive 獲取一個值,該值指示當前線程的執行狀態。
IsBackground 獲取或設置一個值,該值指示某個線程是否為後臺線程。
IsThreadPoolThread 獲取一個值,該值指示線程是否屬於托管線程池。
ManagedThreadId 獲取當前托管線程的唯一標識符。
Name 獲取或設置線程的名稱。
Priority 獲取或設置一個值,該值指示線程的調度優先級。
ThreadState 獲取一個值,該值包含當前線程的狀態。

2.2.1 線程的標識符

ManagedThreadId是確認線程的唯一標識符,程序在大部分情況下都是通過Thread.ManagedThreadId來辨別線程的。而Name是一個可變值,在默認時候,Name為一個空值 Null,開發人員可以通過程序設置線程的名稱,但這隻是一個輔助功能。

2.2.2 線程的優先級別

當線程之間爭奪CPU時間時,CPU按照線程的優先級給予服務。高優先級的線程可以完全阻止低優先級的線程執行。.NET為線程設置瞭Priority屬性來定義線程執行的優先級別,裡面包含5個選項,其中Normal是默認值。除非系統有特殊要求,否則不應該隨便設置線程的優先級別。

成員名稱 說明
Lowest 可以將 Thread 安排在具有任何其他優先級的線程之後。
BelowNormal 可以將 Thread 安排在具有 Normal 優先級的線程之後,在具有 Lowest 優先級的線程之前。
Normal 默認選擇。可以將 Thread 安排在具有 AboveNormal 優先級的線程之後,在具有 BelowNormal 優先級的線程之前。
AboveNormal 可以將 Thread 安排在具有 Highest 優先級的線程之後,在具有 Normal 優先級的線程之前。
Highest 可以將 Thread 安排在具有任何其他優先級的線程之前。

2.2.3 線程的狀態

通過ThreadState可以檢測線程是處於Unstarted、Sleeping、Running 等等狀態,它比 IsAlive 屬性能提供更多的特定信息。

前面說過,一個應用程序域中可能包括多個上下文,而通過CurrentContext可以獲取線程當前的上下文。

CurrentThread是最常用的一個屬性,它是用於獲取當前運行的線程。

2.2.4 System.Threading.Thread的方法

Thread 中包括瞭多個方法來控制線程的創建、掛起、停止、銷毀,以後來的例子中會經常使用。

方法名稱 說明
Abort()     終止本線程。
GetDomain() 返回當前線程正在其中運行的當前域。
GetDomainId() 返回當前線程正在其中運行的當前域Id。
Interrupt() 中斷處於 WaitSleepJoin 線程狀態的線程。
Join() 已重載。 阻塞調用線程,直到某個線程終止時為止。
Resume() 繼續運行已掛起的線程。
Start()   執行本線程。
Suspend() 掛起當前線程,如果當前線程已屬於掛起狀態則此不起作用
Sleep()   把正在運行的線程掛起一段時間。

線程示例

static void Main(string[] args)
        {
            //獲取正在運行的線程
            Thread thread = Thread.CurrentThread;
            //設置線程的名字
            thread.Name = "主線程";
            //獲取當前線程的唯一標識符
            int id = thread.ManagedThreadId;
            //獲取當前線程的狀態
            ThreadState state= thread.ThreadState;
            //獲取當前線程的優先級
            ThreadPriority priority= thread.Priority;
            string strMsg = string.Format("Thread ID:{0}\n" + "Thread Name:{1}\n" +
                "Thread State:{2}\n" + "Thread Priority:{3}\n", id, thread.Name,
                state, priority);

            Console.WriteLine(strMsg);

            Console.ReadKey();
        }

運行結果:

2.3 前臺線程和後臺線程

前臺線程:隻有所有的前臺線程都結束,應用程序才能結束。默認情況下創建的線程都是前臺線程

後臺線程:隻要所有的前臺線程結束,後臺線程自動結束。通過Thread.IsBackground設置後臺線程。必須在調用Start方法之前設置線程的類型,否則一旦線程運行,將無法改變其類型。

通過BeginXXX方法運行的線程都是後臺線程。

class Program
    {
        static void Main(string[] args)
        {
            //演示前臺、後臺線程
            BackGroundTest background = new BackGroundTest(10);
            //創建前臺線程
            Thread fThread = new Thread(new ThreadStart(background.RunLoop));
            //給線程命名
            fThread.Name = "前臺線程";


            BackGroundTest background1 = new BackGroundTest(20);
            //創建後臺線程
            Thread bThread = new Thread(new ThreadStart(background1.RunLoop));
            bThread.Name = "後臺線程";
            //設置為後臺線程
            bThread.IsBackground = true;

            //啟動線程
            fThread.Start();
            bThread.Start();
        }
    }

    class BackGroundTest
    {
        private int Count;
        public BackGroundTest(int count)
        {
            this.Count = count;
        }
        public void RunLoop()
        {
            //獲取當前線程的名稱
            string threadName = Thread.CurrentThread.Name;
            for (int i = 0; i < Count; i++)
            {
                Console.WriteLine("{0}計數:{1}",threadName,i.ToString());
                //線程休眠500毫秒
                Thread.Sleep(1000);
            }
            Console.WriteLine("{0}完成計數",threadName);

        }
    }

運行結果:前臺線程執行完,後臺線程未執行完,程序自動結束。

把bThread.IsBackground = true註釋掉,運行結果:主線程執行完畢後(Main函數),程序並未結束,而是要等所有的前臺線程結束以後才會結束。

後臺線程一般用於處理不重要的事情,應用程序結束時,後臺線程是否執行完成對整個應用程序沒有影響。如果要執行的事情很重要,需要將線程設置為前臺線程。

2.4 線程同步

所謂同步:是指在某一時刻隻有一個線程可以訪問變量。

如果不能確保對變量的訪問是同步的,就會產生錯誤。

c#為同步訪問變量提供瞭一個非常簡單的方式,即使用c#語言的關鍵字Lock,它可以把一段代碼定義為互斥段,互斥段在一個時刻內隻允許一個線程進入執行,而其他線程必須等待。在c#中,關鍵字Lock定義如下:

Lock(expression)
{
   statement_block
}

expression代表你希望跟蹤的對象:

  • 如果你想保護一個類的實例,一般地,你可以使用this;
  • 如果你想保護一個靜態變量(如互斥代碼段在一個靜態方法內部),一般使用類名就可以瞭

而statement_block就算互斥段的代碼,這段代碼在一個時刻內隻可能被一個線程執行。

以書店賣書為例

class Program
    {
        static void Main(string[] args)
        {
            BookShop book = new BookShop();
            //創建兩個線程同時訪問Sale方法
            Thread t1 = new Thread(new ThreadStart(book.Sale));
            Thread t2 = new Thread(new ThreadStart(book.Sale));
            //啟動線程
            t1.Start();
            t2.Start();
            Console.ReadKey();
        }
    }



    class BookShop
    {
        //剩餘圖書數量
        public int num = 1;
        public void Sale()
        {
            int tmp = num;
            if (tmp > 0)//判斷是否有書,如果有就可以賣
            {
                Thread.Sleep(1000);
                num -= 1;
                Console.WriteLine("售出一本圖書,還剩餘{0}本", num);
            }
            else
            {
                Console.WriteLine("沒有瞭");
            }
        }
    }

運行結果:

從運行結果可以看出,兩個線程同步訪問共享資源,沒有考慮同步的問題,結果不正確。

考慮線程同步,改進後的代碼:

class Program
    {
        static void Main(string[] args)
        {
            BookShop book = new BookShop();
            //創建兩個線程同時訪問Sale方法
            Thread t1 = new Thread(new ThreadStart(book.Sale));
            Thread t2 = new Thread(new ThreadStart(book.Sale));
            //啟動線程
            t1.Start();
            t2.Start();
            Console.ReadKey();
        }
    }



    class BookShop
    {
        //剩餘圖書數量
        public int num = 1;
        public void Sale()
        {
            //使用lock關鍵字解決線程同步問題
            lock (this)
            {
                int tmp = num;
                if (tmp > 0)//判斷是否有書,如果有就可以賣
                {
                    Thread.Sleep(1000);
                    num -= 1;
                    Console.WriteLine("售出一本圖書,還剩餘{0}本", num);
                }
                else
                {
                    Console.WriteLine("沒有瞭");
                }
            }
        }
    }

運行結果:

2.5 跨線程訪問

點擊“測試”,創建一個線程,從0循環到10000給文本框賦值,代碼如下:

private void btn_Test_Click(object sender, EventArgs e)
        {
            //創建一個線程去執行這個方法:創建的線程默認是前臺線程
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法標記這個線程就緒瞭,可以隨時被執行,具體什麼時候執行這個線程,由CPU決定
            //將線程設置為後臺線程
            thread.IsBackground = true;
            thread.Start();
        }

        private void Test()
        {
            for (int i = 0; i < 10000; i++)
            {
                this.textBox1.Text = i.ToString();
            }
        }

運行結果:

產生錯誤的原因:textBox1是由主線程創建的,thread線程是另外創建的一個線程,在.NET上執行的是托管代碼,C#強制要求這些代碼必須是線程安全的,即不允許跨線程訪問Windows窗體的控件。

解決方案:

1、在窗體的加載事件中,將C#內置控件(Control)類的CheckForIllegalCrossThreadCalls屬性設置為false,屏蔽掉C#編譯器對跨線程調用的檢查。

 private void Form1_Load(object sender, EventArgs e)
 {
        //取消跨線程的訪問
        Control.CheckForIllegalCrossThreadCalls = false;
 }

使用上述的方法雖然可以保證程序正常運行並實現應用的功能,但是在實際的軟件開發中,做如此設置是不安全的(不符合.NET的安全規范),在產品軟件的開發中,此類情況是不允許的。如果要在遵守.NET安全標準的前提下,實現從一個線程成功地訪問另一個線程創建的空間,要使用C#的方法回調機制。

2、使用回調函數

回調實現的一般過程:

C#的方法回調機制,也是建立在委托基礎上的,下面給出它的典型實現過程。

(1)、定義、聲明回調。

//定義回調
private delegate void DoSomeCallBack(Type para);
//聲明回調
DoSomeCallBack doSomaCallBack;

可以看出,這裡定義聲明的“回調”(doSomaCallBack)其實就是一個委托。

(2)、初始化回調方法。

doSomeCallBack=new DoSomeCallBack(DoSomeMethod);

所謂“初始化回調方法”實際上就是實例化剛剛定義瞭的委托,這裡作為參數的DoSomeMethod稱為“回調方法”,它封裝瞭對另一個線程中目標對象(窗體控件或其他類)的操作代碼。

(3)、觸發對象動作

Opt  obj.Invoke(doSomeCallBack,arg);

其中Opt obj為目標操作對象,在此假設它是某控件,故調用其Invoke方法。Invoke方法簽名為:

object Control.Invoke(Delegate method,params object[] args);

它的第一個參數為委托類型,可見“觸發對象動作”的本質,就是把委托doSomeCallBack作為參數傳遞給控件的Invoke方法,這與委托的使用方式是一模一樣的。

最終作用於對象Opt obj的代碼是置於回調方法體DoSomeMethod()中的,如下所示:

private void DoSomeMethod(type para)
{
     //方法體
    Opt obj.someMethod(para);
}

如果不用回調,而是直接在程序中使用“Opt obj.someMethod(para);”,則當對象Opt obj不在本線程(跨線程訪問)時就會發生上面所示的錯誤。

從以上回調實現的一般過程可知:C#的回調機制,實質上是委托的一種應用。在C#網絡編程中,回調的應用是非常普遍的,有瞭方法回調,就可以在.NET上寫出線程安全的代碼瞭。

使用方法回調,實現給文本框賦值:

namespace MultiThreadDemo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        //定義回調
        private delegate void setTextValueCallBack(int value);
        //聲明回調
        private setTextValueCallBack setCallBack;

        private void btn_Test_Click(object sender, EventArgs e)
        {
            //實例化回調
            setCallBack = new setTextValueCallBack(SetValue);
            //創建一個線程去執行這個方法:創建的線程默認是前臺線程
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法標記這個線程就緒瞭,可以隨時被執行,具體什麼時候執行這個線程,由CPU決定
            //將線程設置為後臺線程
            thread.IsBackground = true;
            thread.Start();
        }

        private void Test()
        {
            for (int i = 0; i < 10000; i++)
            {
                //使用回調
                textBox1.Invoke(setCallBack, i);
            }
        }

        /// <summary>
        /// 定義回調使用的方法
        /// </summary>
        /// <param name="value"></param>
        private void SetValue(int value)
        {
            this.textBox1.Text = value.ToString();
        }
    }
}

2.6 終止線程

若想終止正在運行的線程,可以使用Abort()方法。

三、同步和異步

同步和異步是對方法執行順序的描述。

同步:等待上一行完成計算之後,才會進入下一行。

例如:請同事吃飯,同事說很忙,然後就等著同事忙完,然後一起去吃飯。

異步:不會等待方法的完成,會直接進入下一行,是非阻塞的。

例如:請同事吃飯,同事說很忙,那同事先忙,自己去吃飯,同事忙完瞭他自己去吃飯。

下面通過一個例子講解同步和異步的區別

1、新建一個winform程序,上面有兩個按鈕,一個同步方法、一個異步方法,在屬性裡面把輸出類型改成控制臺應用程序,這樣可以看到輸出結果,代碼如下:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MyAsyncThreadDemo
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 異步方法
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAsync_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}");
            Action<string> action = this.DoSomethingLong;
            // 調用委托(同步調用)
            action.Invoke("btnAsync_Click_1");
            // 異步調用委托
            action.BeginInvoke("btnAsync_Click_2",null,null);
            Console.WriteLine($"***************btnAsync_Click End    {Thread.CurrentThread.ManagedThreadId}");
        }

        /// <summary>
        /// 同步方法
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSync_Click(object sender, EventArgs e)
        {
            Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
            int j = 3;
            int k = 5;
            int m = j + k;
            for (int i = 0; i < 5; i++)
            {
                string name = string.Format($"btnSync_Click_{i}");
                this.DoSomethingLong(name);
            }
        }


        private void DoSomethingLong(string name)
        {
            Console.WriteLine($"****************DoSomethingLong {name} Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
            long lResult = 0;
            for (int i = 0; i < 1000000000; i++)
            {
                lResult += i;
            }
            Console.WriteLine($"****************DoSomethingLong {name}   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
        }
    }
}

2、啟動程序,點擊同步,結果如下:

從上面的截圖中能夠很清晰的看出:同步方法是等待上一行代碼執行完畢之後才會執行下一行代碼。

點擊異步,結果如下:

從上面的截圖中看出:當執行到action.BeginInvoke("btnAsync_Click_2",null,null);這句代碼的時候,程序並沒有等待這段代碼執行完就執行瞭下面的End,沒有阻塞程序的執行。

在剛才的測試中,如果點擊同步,這時winform界面不能拖到,界面卡住瞭,是因為主線程(即UI線程)在忙於計算。

點擊異步的時候,界面不會卡住,這是因為主線程已經結束,計算任務交給子線程去做。

在仔細檢查上面兩個截圖,可以看出異步的執行速度比同步執行速度要快。同步方法執行完將近16秒,異步方法執行完將近6秒。

在看下面的一個例子,修改異步的方法,也和同步方法一樣執行循環,修改後的代碼如下:

private void btnAsync_Click(object sender, EventArgs e)
{
      Console.WriteLine($"***************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId}");
      //Action<string> action = this.DoSomethingLong;
      //// 調用委托(同步調用)
      //action.Invoke("btnAsync_Click_1");
      //// 異步調用委托
      //action.BeginInvoke("btnAsync_Click_2",null,null);
      Action<string> action = this.DoSomethingLong;
      for (int i = 0; i < 5; i++)
      {
           //Thread.Sleep(5);
           string name = string.Format($"btnAsync_Click_{i}");
           action.BeginInvoke(name, null, null);
      }
      Console.WriteLine($"***************btnAsync_Click End    {Thread.CurrentThread.ManagedThreadId}");
}

結果如下:

從截圖中能夠看出:同步方法執行是有序的,異步方法執行是無序的。異步方法無序包括啟動無序和結束無序。啟動無序是因為同一時刻向操作系統申請線程,操作系統收到申請以後,返回執行的順序是無序的,所以啟動是無序的。結束無序是因為雖然線程執行的是同樣的操作,但是每個線程的耗時是不同的,所以結束的時候不一定是先啟動的線程就先結束。從上面同步方法中可以清晰的看出:btnSync_Click_0執行時間耗時不到3秒,而btnSync_Click_1執行時間耗時超過瞭3秒。可以想象體育比賽中的跑步,每位運動員聽到發令槍起跑的順序不同,每位運動員花費的時間不同,最終到達終點的順序也不同。

總結一下同步方法和異步方法的區別:

  • 1、同步方法由於主線程忙於計算,所以會卡住界面。
    異步方法由於主線程執行完瞭,其他計算任務交給子線程去執行,所以不會卡住界面,用戶體驗性好。
  • 2、同步方法由於隻有一個線程在計算,所以執行速度慢。
    異步方法由多個線程並發運算,所以執行速度快,但並不是線性增長的(資源可能不夠)。多線程也不是越多越好,隻有多個獨立的任務同時運行,才能加快速度。
  • 3、同步方法是有序的。
    異步多線程是無序的:啟動無序,執行時間不確定,所以結束也是無序的。一定不要通過等待幾毫秒的形式來控制線程啟動/執行時間/結束。

四、回調

先來看看異步多線程無序的例子:

在界面上新增一個按鈕,實現代碼如下:

private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
      Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
      Action<string> action = this.DoSomethingLong;
      action.BeginInvoke("btnAsyncAdvanced_Click", null, null);
      // 需求:異步多線程執行完之後再打印出下面這句
      Console.WriteLine($"到這裡計算已經完成瞭。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
      Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}

運行結果:

從上面的截圖中看出,最終的效果並不是我們想要的效果,而且打印輸出的還是主線程。

既然異步多線程是無序的,那我們有沒有什麼辦法可以解決無序的問題呢?辦法當然是有的,那就是使用回調,.NET框架已經幫我們實現瞭回調:

BeginInvoke的第二個參數就是一個回調,那麼AsyncCallback究竟是什麼呢?F12查看AsyncCallback的定義:

發現AsyncCallback就是一個委托,參數類型是IAsyncResult,明白瞭AsyncCallback是什麼以後,將上面的代碼進行如下的改造:

private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    Action<string> action = this.DoSomethingLong;
    // 定義一個回調
    AsyncCallback callback = p =>
    {
       Console.WriteLine($"到這裡計算已經完成瞭。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
    };
    // 回調作為參數
    action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);
    Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
 }

運行結果:

上面的截圖中可以看出,這就是我們想要的效果,而且打印是子線程輸出的,但是程序究竟是怎麼實現的呢?我們可以進行如下的猜想:

程序執行到BeginInvoke的時候,會申請一個基於線程池的線程,這個線程會完成委托的執行(在這裡就是執行DoSomethingLong()方法),在委托執行完以後,這個線程又會去執行callback回調的委托,執行callback委托需要一個IAsyncResult類型的參數,這個IAsyncResult類型的參數是如何來的呢?鼠標右鍵放到BeginInvoke上面,查看返回值:

發現BeginInvoke的返回值就是IAsyncResult類型的。那麼這個返回值是不是就是callback委托的參數呢?將代碼進行如下的修改:

private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
            // 需求:異步多線程執行完之後再打印出下面這句
            Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
            Action<string> action = this.DoSomethingLong;
            // 無序的
            //action.BeginInvoke("btnAsyncAdvanced_Click", null, null);

            IAsyncResult asyncResult = null;
            // 定義一個回調
            AsyncCallback callback = p =>
            {
                // 比較兩個變量是否是同一個
                Console.WriteLine(object.ReferenceEquals(p,asyncResult));
                Console.WriteLine($"到這裡計算已經完成瞭。{Thread.CurrentThread.ManagedThreadId.ToString("00")}。");
            };
            // 回調作為參數
            asyncResult= action.BeginInvoke("btnAsyncAdvanced_Click", callback, null);
            Console.WriteLine($"****************btnAsyncAdvanced_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}

結果:

這裡可以看出BeginInvoke的返回值就是callback委托的參數。

現在我們可以使用回調解決異步多線程無序的問題瞭。

獲取委托異步調用的返回值

使用EndInvoke可以獲取委托異步調用的返回值,請看下面的例子:

private void btnAsyncReturnVlaue_Click(object sender, EventArgs e)
{
       // 定義一個無參數、int類型返回值的委托
       Func<int> func = () =>
       {
             Thread.Sleep(2000);
             return DateTime.Now.Day;
       };
       // 輸出委托同步調用的返回值
       Console.WriteLine($"func.Invoke()={func.Invoke()}");
       // 委托的異步調用
       IAsyncResult asyncResult = func.BeginInvoke(p =>
       {
            Console.WriteLine(p.AsyncState);
       },"異步調用返回值");
       // 輸出委托異步調用的返回值
       Console.WriteLine($"func.EndInvoke(asyncResult)={func.EndInvoke(asyncResult)}");
}

結果:

到此這篇關於深入學習C#多線程的文章就介紹到這瞭。希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: