首頁 > 軟體

深入學習C#多執行緒

2022-02-24 19:00:55

一、基本概念

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#多執行緒的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com