1. 程式人生 > >細說C#多執行緒那些事

細說C#多執行緒那些事

一、Task類

上次我們說了執行緒池,執行緒池的QueueUserWorkItem()方法發起一次非同步的執行緒執行很簡單

但是該方法最大的問題是沒有一個內建的機制讓你知道操作什麼時候完成,有沒有一個內建的機制在操作完成後獲得一個返回值。為此,可以使用System.Threading.Tasks中的Task類。

Task類在名稱空間System.Threading.Tasks下,通過Task的Factory返回TaskFactory類,以TaskFactory.StartNew(Action)方法可以建立一個新的非同步執行緒,所建立的執行緒預設為後臺執行緒,不會影響前臺UI視窗的執行。

如果要取消執行緒,可以利用CancellationTakenSource物件。如果要在取消任務後執行一個回撥方法,則可以使用Task的()方法。

簡單程式碼實現:

 Task類示例程式碼 複製程式碼
using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      Task t = Task.Factory.StartNew( () => {
                                  int ctr = 0;
                                  
for (ctr = 0; ctr <= 1000000; ctr++) {} Console.WriteLine("Finished {0} loop iterations", ctr); } ); t.Wait(); } }
複製程式碼

二、非同步執行

委託的非同步執行程式碼:BeginInvoke() 和 EndInvoke()

 委託非同步執行示例程式碼

三、執行緒同步

執行緒同步:指多個執行緒協同、協助、互相配合。一些敏感資料不允許被多個執行緒同時訪問,此時就使用同步訪問技術,保證資料在任何時刻,最多有一個執行緒訪問,以保證資料的完整性。

1、互斥鎖lock()語句

同步訪問共享資源的首選技術是C#的 lock 關鍵字,lock 允許定義一段執行緒同步的程式碼語句,它需要定義一個標記(即一個物件引用),執行緒在進入鎖定範圍的時候必須獲得這個標記,在退出鎖定範圍時需要釋放鎖。當試圖鎖定的是一個例項級的私有方法時,使用方法本身所在物件的引用就可以了。然而,如需鎖定公共成員中的一段程式碼,比較安全的做法是宣告私有的object成員作為鎖標識。

複製程式碼
public class DemoClass
{
    private readonly object threadLock = new object();

    public void Method()
    {
        // 使用鎖標識
        lock (threadLock)
        {
            //……
        }
    }
}
複製程式碼

再來一個混合執行緒同步鎖的例子:

複製程式碼
using System;
using System.Threading;

namespace Threading
{
    public sealed class SimpleHybirdLock : IDisposable
    {
        //Int32由基元使用者模式構造(Interlocked的方法)使用
        private Int32 m_waiters = 0;
        // AutoResetEvent是基元核心模式構造
        private AutoResetEvent m_waiterLock = new AutoResetEvent(false);

        public void Enter()
        {
            //指出這個執行緒想要獲得的鎖
            if (Interlocked.Increment(ref m_waiters) == 1)
                return; //鎖可以自由使用

            //另一個執行緒擁有鎖(發生競爭),使這個執行緒等待
            m_waiterLock.WaitOne(); //這裡產生較大的效能
            //WaitOne 返回後,這個執行緒拿到鎖了
        }

        public void Leave()
        {
            //這個執行緒準備釋放鎖
            if (Interlocked.Increment(ref m_waiters) == 0)
                return; //沒有其他執行緒等待直接返回

            //有其他執行緒正在阻塞,喚醒其中一個
            m_waiterLock.Set(); //這裡產生較大的效能
            //WaitOne 返回後,這個執行緒拿到鎖了
        }

        public void Dispose()
        {
            m_waiterLock.Dispose();
        }
    }
}
複製程式碼

2、Monitor實現執行緒同步

通過Monitor.Enter() 和 Monitor.Exit()實現排它鎖的獲取和釋放,獲取之後獨佔資源,不允許其他執行緒訪問。

還有一個TryEnter方法,請求不到資源時不會阻塞等待,可以設定超時時間,獲取不到直接返回false。

複製程式碼
public class DemoClass
{
    private readonly object threadLock = new object();

    public void Method()
    {
        Monitor.Enter(threadLock);
        try
        {
            //……
        }
        finally
        {
            Monitor.Exit(threadLock);
        }
    }
}
複製程式碼

3、維護自由鎖(System.Threading.Interlocked)實現執行緒同步,Interlocked允許我們對資料進行一些原子操作:CompareExchange(), Decrement(), Exchange(), Increment()。這些靜態方法需要以引用方式傳入變數。如:注意newVal 和 intVal 的值都是遞增之後的值。

4、[Synchronization]特性可以有效地使物件的所以例項的成員都保持執行緒安全。當CLR分配帶[Synchronization]特性的物件時,它會把這個物件放在同步上下文中。這是編寫執行緒安全程式碼的一種偷懶方式,因為它不需要我們實際深入執行緒控制敏感資料的細節,但這種方式對效能有影響,因為即使一個方法沒有使用共享資源,CLR仍然會鎖定對該方法的呼叫。

5、系統內建物件

互斥(Mutex), 訊號量(Semaphore), 事件(AutoResetEvent/ManualResetEvent),執行緒池

四、執行緒優先順序

系統會為每一個執行緒分配一個優先級別。.NET執行緒優先順序,是指定一個執行緒的相對於其他執行緒的相對優先順序,它規定了執行緒的執行順序,對於在CLR中建立的執行緒,其優先級別預設為Normal,而在CLR之外建立的執行緒進入CLR時,將會保留其先前的優先順序,可以通過訪問執行緒的Priority屬性來獲取或設定執行緒的優先級別。

System.Threading名稱空間中的ThreadPriority列舉定義了一組執行緒優先順序的所有可能值,我這裡按級別由高到低排列出來常用的,具體的說明就不在這裡解釋。

Highest  , AboveNormal ,  Normal  ,  BelowNormal ,  Lowest

除了這些還有Realtime,但Realtime優先順序儘量避免,他的優先順序相當高,甚至會干擾作業系統的任務,比如阻礙一些必要的磁碟I/O和網路傳輸。也可能會造成不及時處理鍵盤和滑鼠的輸入,導致使用者會感覺宕機了。

程式碼示例:

複製程式碼
using System;
using System.Threading;

namespace Threading
{
    class Program
    {
        public static void Thread1()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.Write("1 ");
            }

        }
        public static void Thread2()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.Write("2 ");
            }
        }
        public static void Thread3()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.Write("3 ");
            }
        }
        public static void Thread4()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.Write("4 ");
            }
        }
        public static void Thread5()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.Write("5 ");
            }
        }
        static void Main(string[] args)
        {
            var t1 = new Thread(Thread1);
            var t2 = new Thread(Thread2);
            var t3 = new Thread(Thread3);
            var t4 = new Thread(Thread4);
            var t5 = new Thread(Thread5);

            t1.Priority = ThreadPriority.Highest;
            t2.Priority = ThreadPriority.AboveNormal;
            t3.Priority = ThreadPriority.Normal;
            t4.Priority = ThreadPriority.BelowNormal;
            t5.Priority = ThreadPriority.Lowest;


            t1.Start();
            t2.Start();
            t3.Start();
            t4.Start();
            t5.Start();

            Console.ReadKey();
        }
    }
}
複製程式碼

執行結果:

 很明顯,根據執行緒的優先順序高低順序執行的。

五、阻塞呼叫執行緒

Join阻塞呼叫執行緒,直到該執行緒終止。

複製程式碼
using System;
using System.Threading;

namespace Threading
{
    class Program
    {
        static void Main(string[] args)
        {
            var threadStartA = new ThreadStart(delegate()
            { 
                for (int i = 0; i < 1000000; i++)
                {
                    if (i % 10000 == 0)
                        Console.Write("A");
                }
            });
            var threadA = new Thread(threadStartA);



            var threadStartB = new ThreadStart(delegate()
            {  
                for (int i = 0; i < 500000; i++)
                {
                    if (i % 10000 == 0)
                        Console.Write("B");
                }
                threadA.Join();  //阻塞執行緒threadB,插入threadA進行執行
                for (int i = 0; i < 500000; i++)
                {
                    if (i % 10000 == 0)
                        Console.Write("B1");
                }
            });
            var threadB = new Thread(threadStartB);

            //啟動執行緒
            threadA.Start();
            threadB.Start();

            Console.ReadKey();
        }
    }
}
複製程式碼

執行結果:

從執行結果可以看出:一開始,ThreadA和ThreadB交替執行,當ThreadB執行到ThreadA.Join()方法時,ThreadB被阻塞,ThreadA插入進來單獨執行,當ThreadA執行完畢以後,ThreadB繼續執行。

除了ThreadA和ThreadB外,程式中還有一個主執行緒(Main Thread)。現在我們在主執行緒中新增一些輸出程式碼,看看主執行緒和工作執行緒A、B是如何併發執行的。

六、Parallel

這個類提供了For,Foreach,Invoke靜態方法。它內部封裝了Task類。主要用於平行計算。

複製程式碼
        private void ParallelTest2()
        {
            for (int i = 1; i < 5; i++)
            {
                Console.WriteLine(DoWork(i));
            }
            var plr = Parallel.For(1, 5, i => Console.WriteLine(DoWork(i)));
        }

        private int DoWork(int num)
        {
            int sum = 0;
            for (int i = 0; i <= num; i++)
            {
                sum += i;
            }
            return sum;
        }
複製程式碼