C#-多執行緒

一码事發表於2024-04-05

執行緒 被定義為程式的執行路徑。每個執行緒都定義了一個獨特的控制流。如果您的應用程式涉及到複雜的和耗時的操作,那麼設定不同的執行緒執行路徑往往是有益的,每個執行緒執行特定的工作。

執行緒是**輕量級程序**。一個使用執行緒的常見例項是現代作業系統中並行程式設計的實現。使用執行緒節省了 CPU 週期的浪費,同時提高了應用程式的效率。

到目前為止我們編寫的程式是一個單執行緒作為應用程式的執行例項的單一的過程執行的。但是,這樣子應用程式同時只能執行一個任務。為了同時執行多個任務,它可以被劃分為更小的執行緒。

執行緒生命週期

  • 一般認為,執行緒有五種狀態:

新建(new 物件) 、就緒(等待CPU排程)、執行(CPU正在執行)、阻塞(等待阻塞、同步阻塞等)、死亡(物件釋放)

img

主執行緒

在 中,**System.Threading.Thread** 類用於執行緒的工作。它允許建立並訪問多執行緒應用程式中的單個執行緒。程序中第一個被執行的執行緒稱為**主執行緒**。

當 程式開始執行時,主執行緒自動建立。使用 Thread 類建立的執行緒被主執行緒的子執行緒呼叫。您可以使用 Thread 類的 CurrentThread 屬性訪問執行緒。

下面的程式演示了主執行緒的執行:

例項

using System;
using System.Threading;

namespace MultithreadingApplication
{
    class MainThreadProgram
    {
        static void Main(string[] args)
        {
            Thread th = Thread.CurrentThread;
            th.Name = "MainThread";
            Console.WriteLine("This is {0}", th.Name);
            Console.ReadKey();
        }
    }
}

當上面的程式碼被編譯和執行時,它會產生下列結果:

This is MainThread

Thread 類常用的屬性和方法

下表列出了 Thread 類的一些常用的 屬性:

屬性描述
CurrentContext 獲取執行緒正在其中執行的當前上下文。
CurrentCulture 獲取或設定當前執行緒的區域性。
CurrentPrincipal 獲取或設定執行緒的當前負責人(對基於角色的安全性而言)。
CurrentThread 獲取當前正在執行的執行緒。
CurrentUICulture 獲取或設定資源管理器使用的當前區域性以便在執行時查詢區域性特定的資源。
ExecutionContext 獲取一個 ExecutionContext 物件,該物件包含有關當前執行緒的各種上下文的資訊。
IsAlive 獲取一個值,該值指示當前執行緒的執行狀態。
IsBackground 獲取或設定一個值,該值指示某個執行緒是否為後臺執行緒。
IsThreadPoolThread 獲取一個值,該值指示執行緒是否屬於託管執行緒池。
ManagedThreadId 獲取當前託管執行緒的唯一識別符號。
Name 獲取或設定執行緒的名稱。
Priority 獲取或設定一個值,該值指示執行緒的排程優先順序。
ThreadState 獲取一個值,該值包含當前執行緒的狀態。

下表列出了 Thread 類的一些常用的 方法:

序號方法名 & 描述
1 public void Abort() 在呼叫此方法的執行緒上引發 ThreadAbortException,以開始終止此執行緒的過程。呼叫此方法通常會終止執行緒。
2 public static LocalDataStoreSlot AllocateDataSlot() 在所有的執行緒上分配未命名的資料槽。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位。
3 **public static LocalDataStoreSlot AllocateNamedDataSlot( string name) ** 在所有執行緒上分配已命名的資料槽。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位。 |
| 4 | public static void BeginCriticalRegion() 通知主機執行將要進入一個程式碼區域,在該程式碼區域內執行緒中止或未經處理的異常的影響可能會危害應用程式域中的其他任務。 |
| 5 | public static void BeginThreadAffinity() 通知主機託管程式碼將要執行依賴於當前物理作業系統執行緒的標識的指令。 |
| 6 | public static void EndCriticalRegion() 通知主機執行將要進入一個程式碼區域,在該程式碼區域內執行緒中止或未經處理的異常僅影響當前任務。 |
| 7 | public static void EndThreadAffinity() 通知主機託管程式碼已執行完依賴於當前物理作業系統執行緒的標識的指令。 |
| 8 | public static void FreeNamedDataSlot(string name) 為程序中的所有執行緒消除名稱與槽之間的關聯。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位。 |
| 9 | **public static Object GetData(LocalDataStoreSlot slot) ** 在當前執行緒的當前域中從當前執行緒上指定的槽中檢索值。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位。 |
| 10 | public static AppDomain GetDomain() 返回當前執行緒正在其中執行的當前域。 |
| 11 | public static AppDomain GetDomainID() 返回唯一的應用程式域識別符號。 |
| 12 | **public static LocalDataStoreSlot GetNamedDataSlot(string name) ** 查詢已命名的資料槽。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位。 |
| 13 | public void Interrupt() 中斷處於 WaitSleepJoin 執行緒狀態的執行緒。 |
| 14 | public void Join() 在繼續執行標準的 COM 和 SendMessage 訊息泵處理期間,阻塞呼叫執行緒,直到某個執行緒終止為止。此方法有不同的過載形式。 |
| 15 | public static void MemoryBarrier() 按如下方式同步記憶體存取:執行當前執行緒的處理器在對指令重新排序時,不能採用先執行 MemoryBarrier 呼叫之後的記憶體存取,再執行 MemoryBarrier 呼叫之前的記憶體存取的方式。 |
| 16 | public static void ResetAbort() 取消為當前執行緒請求的 Abort。 |
| 17 | **public static void SetData(LocalDataStoreSlot slot,Object data) ** 在當前正在執行的執行緒上為此執行緒的當前域在指定槽中設定資料。為了獲得更好的效能,請改用以 ThreadStaticAttribute 屬性標記的欄位。 |
| 18 | public void Start() 開始一個執行緒。 |
| 19 | **public static void Sleep(int millisecondsTimeout) ** 讓執行緒暫停一段時間。 |
| 20 | **public static void SpinWait(int iterations) ** 導致執行緒等待由 iterations 引數定義的時間量。 |
| 21 | **public static byte VolatileRead(ref byte address) public static double VolatileRead(ref double address) public static int VolatileRead(ref int address) public static Object VolatileRead(ref Object address) ** 讀取欄位值。無論處理器的數目或處理器快取的狀態如何,該值都是由計算機的任何處理器寫入的最新值。此方法有不同的過載形式。這裡只給出了一些形式。 |
| 22 | **public static void VolatileWrite(ref byte address, byte value) public static void VolatileWrite(ref double address, double value) public static void VolatileWrite(ref int address,int value) public static void VolatileWrite(ref Object address,Object value) ** 立即向欄位寫入一個值,以使該值對計算機中的所有處理器都可見。此方法有不同的過載形式。這裡只給出了一些形式。 |
| 23 | public static bool Yield() 導致呼叫執行緒執行準備好在當前處理器上執行的另一個執行緒。由作業系統選擇要執行的執行緒。 |

建立執行緒

執行緒是透過擴充套件 Thread 類建立的。擴充套件的 Thread 類呼叫 Start() 方法來開始子執行緒的執行。

下面的程式演示了這個概念:

例項


using System;
using System.Threading;

namespace MultithreadingApplication
{
    class ThreadCreationProgram
    {
        public static void CallToChildThread()
        {
            Console.WriteLine("Child thread starts");
        }
        
        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
            Console.ReadKey();
        }
    }
}

當上面的程式碼被編譯和執行時,它會產生下列結果:

1
2
In Main: Creating the Child thread
Child thread starts

Thread方式

1、不帶引數


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(new ThreadStart(CreateThread));
            thread.Name = "執行緒一";
            thread.IsBackground = true;
            thread.Start();
        }

        static void CreateThread()
        {
            for (int i=1;i<=20;i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name+":"+i);
            }
        }
    }
}

2、帶引數ParameterizedThreadStart


using System;
using System.Threading;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(new ParameterizedThreadStart(CreateThread));
            thread.IsBackground = true;
            thread.Start("執行緒二");
            Console.ReadLine();
        }

        static void CreateThread(Object threadName)
        {
            Thread.CurrentThread.Name = threadName.ToString();
            for (int i = 1; i <= 20; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + i);
            }
        }
    }
}

Task方式

Task類的表示單個操作不會返回一個值,通常以非同步方式執行

1、直接New建立

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task = new Task(() =>
            {
                Console.WriteLine("可以直接寫程式碼,也可以呼叫方法");
                CreateTask();
            });
            task.Start();
            Console.ReadLine();
        }

        static void CreateTask()
        {
            Thread.CurrentThread.Name = "執行緒一";
            for (int i = 1; i <= 20; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + i);
            }
        }
    }
}

2、使用Factory工廠建立


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            //Factory提供對用於建立Task的工廠方法的訪問
            Task.Factory.StartNew(() =>
            {
                Console.WriteLine("可以直接寫程式碼,也可以呼叫方法");
                CreateTask();
            });
            Console.ReadLine();
        }

        static void CreateTask()
        {
            Thread.CurrentThread.Name = "執行緒一";
            for (int i = 1; i <= 20; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + i);
            }
        }
    }
}

3、 使用Run方法


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static void Main(string[] args)
        {
            //將線上程池上執行的指定工作排隊,並返回代表該工作的Task物件
            Task.Run(() =>
            {
                Console.WriteLine("可以直接寫程式碼,也可以呼叫方法");
                CreateTask();
            });
            Console.ReadLine();
        }

        static void CreateTask()
        {
            Thread.CurrentThread.Name = "執行緒一";
            for (int i = 1; i <= 20; i++)
            {
                Console.WriteLine(Thread.CurrentThread.Name + ":" + i);
            }
        }
    }
}

執行緒常用的方法

1、Sleep

當我們建立一個執行緒後,我們需要呼叫執行緒物件的Start()方法來排程那個執行緒。在這時,CLR將會為作為建構函式引數傳遞給執行緒物件的方法地址分配一個時間片。一旦執行緒開始執行,它就可以在作業系統處理其他執行緒時回到睡眠狀態或者退出狀態。我們可以使用執行緒類的Sleep()方法讓一個執行緒進入睡眠狀態。如果你正在等待一個資源並且你想在稍後繼續嘗試訪問這個資源時,Sleep()方法是很重要的。舉個例子,假設你的程式由於無法訪問需要的資源而導致其不能繼續執行時,你可能想要在幾毫秒之後嘗試繼續訪問資源,在這種情況下讓執行緒在再次嘗試訪問資源之前睡眠一段時間是一個很好的方式。

Sleep()方法有兩種過載方式。第一種過載方法有一個整型引數,並會按照指定的毫秒時間暫停執行緒執行。例如,如果你向執行緒傳遞值100,那麼執行緒將會暫停100毫秒。這個方法將會讓執行緒進入WaitSleepJoin狀態。


using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace SimpleThread
{
    public class ThreadSleep
    {
        public static Thread worker;
        public static Thread worker2;

        public static void Main()
        {
            Console.WriteLine("Entering the void Main!");

            worker = new Thread(new ThreadStart(Counter));
            worker2 = new Thread(new ThreadStart(Counter2));

            //Make the worker2 Object as highest priority
            worker2.Priority = ThreadPriority.Highest;

            worker.Start();
            worker2.Start();

            Console.WriteLine("Exiting the void Main!");
            Console.ReadLine();
        }

        public static void Counter()
        {
            Console.WriteLine("Entering Counter");
            for (int i = 1; i < 50; i++)
            {
                Console.Write(i + " ");
                if (i == 10)
                {
                    Console.WriteLine();
                    Thread.Sleep(1000);
                }
            }
            Console.WriteLine("Exiting Counter");
        }

        public static void Counter2()
        {
            Console.WriteLine("Entering Counter2");
            for (int i = 51; i < 100; i++)
            {
                Console.Write(i + " ");
                if (i == 70)
                {
                    Console.WriteLine();
                    Thread.Sleep(5000);
                }
            }
            Console.WriteLine("Exiting Counter2");
        }
    }
}

第二種過載方法有一個TimeSpan型別引數,當前執行緒會按照TimeSpan的值暫停一段時間。TimeSpan是System名稱空間中的一個類。TimeSpan有一些很有用的屬性並會返回基於時鐘時間間隔。

我們可以使用FromSeconds()和FromMinutes()來確定睡眠時間。


public static void Counter()
{
    Console.WriteLine("Entering Counter");
    for (int i = 1; i < 50; i++)
    {
        Console.Write(i + " ");
        if (i == 10)
        {
            Console.WriteLine();
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
    }
    Console.WriteLine("Exiting Counter");
}

public static void Counter2()
{
    Console.WriteLine("Entering Counter2");
    for (int i = 51; i < 100; i++)
    {
        Console.Write(i + " ");
        if (i == 70)
        {
            Console.WriteLine();
            Thread.Sleep(TimeSpan.FromSeconds(5));
        }
    }
    Console.WriteLine("Exiting Counter2");
}

2、Interrupt


using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace SimpleThread
{
    public class Interrupt
    {
        public static Thread sleeper;
        public static Thread worker;

        public static void Main()
        {
            Console.WriteLine("Entering the void Main!");

            sleeper = new Thread(new ThreadStart(SleepingThread));
            worker = new Thread(new ThreadStart(AwakeThread));

            sleeper.Start();
            worker.Start();

            Console.WriteLine("Exiting the void Main!");
            Console.ReadLine();
        }
        public static void SleepingThread()
        {
            for (int i = 1; i < 50; i++)
            {
                Console.Write(i + " ");
                if (i == 10 || i == 20 || i == 30)
                {
                    Console.WriteLine("Going to sleep at: " + i);
                    try
                    {
                        Thread.Sleep(20);
                    }
                    catch (ThreadInterruptedException ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }
        }

        public static void AwakeThread()
        {
            for (int i = 51; i < 100; i++)
            {
                Console.Write(i + " ");
                if (sleeper.ThreadState == ThreadState.WaitSleepJoin)
                {
                    Console.WriteLine("Interrupting the sleeping thread.");
                    sleeper.Interrupt();
                }
            }
        }
    }
}

在上面的例子中,當計數器的值為10, 20 和 30 時第一個執行緒會睡眠。第二個執行緒會檢查第一個執行緒是否已經進入睡眠狀態。如果是的話,它將中斷第一個執行緒並使它回到排程佇列中去。Interrupt()方法是讓睡眠執行緒重新醒來的最好方式,當執行緒等待的資源可用且你想讓執行緒繼續執行時你可以使用這個方法。輸出結果與下面顯示的類似:

3、Join

Join方法主要是用來阻塞呼叫執行緒,直到某個執行緒終止或經過了指定時間為止。官方的解釋比較乏味,通俗的說就是建立一個子執行緒,給它加了這個方法,其它執行緒就會暫停執行,直到這個執行緒執行完為止才去執行(包括主執行緒)。


class Program
{
    static void Main(string[] args)
    {
        Thread threadA = new Thread(ThreadMethod);      
        threadA.Name = "執行緒A";
        Thread threadB = new Thread(ThreadMethod);      
        threadB.Name = "執行緒B";
        threadA.Start();       
        //threadA.Join();      
        threadB.Start();       
        //threadB.Join();

        for (int i = 1; i <=3; i++)
        {
            Console.WriteLine("我是:主執行緒,迴圈了{1}次", Thread.CurrentThread.Name, i);
            Thread.Sleep(300);          //休眠300毫秒                                                
        }
        Console.ReadKey();
    }
    public static void ThreadMethod(object parameter)
    {
        for (int i = 1; i <=3; i++)
        {
            Console.WriteLine("我是:{0},我迴圈{1}次", Thread.CurrentThread.Name, i);
            Thread.Sleep(300);         //休眠300毫秒              
        }
    }
}

前臺執行緒和後臺執行緒

前臺執行緒:只有所有的前臺執行緒都結束,應用程式才能結束。預設情況下建立的執行緒都是前臺執行緒
後臺執行緒:只要所有的前臺執行緒結束,後臺執行緒自動結束。透過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());
            //執行緒休眠1000毫秒
            Thread.Sleep(1000);
        }
        Console.WriteLine("{0}完成計數", threadName);

    }
}

多執行緒之執行緒同步

1、不使用執行緒同步的例項

我們先來看下面一個例子:


using System;
using System.Threading;

namespace ThreadSynchDemo
{
    class Program
    {
        private static int Counter = 0;
        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    Counter++;
                    Thread.Sleep(1);
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    Counter++;
                    Thread.Sleep(1);
                }
            });
            t2.Start();

            Thread.Sleep(3000);
            Console.WriteLine(Counter);
            Console.ReadKey();
        }
    }
}

分析:在上面的例子中,t1和t2兩個執行緒裡面都是讓變數Counter的值自增1,假設這時t1執行緒讀取到Counter的值為200,可能t2執行緒執行非常快,t1執行緒讀取Counter值的時候,t2執行緒已經把Counter的值改為了205,等t1執行緒執行完畢以後,Counter的值又被變為了201,這樣就會出現執行緒同步的問題了。那麼該如何解決這個問題呢?

2、解決執行緒同步問題

(1)Lock

解決執行緒同步問題最簡單的是使用lock。lock可以解決多個執行緒同時操作一個資源引起的問題。lock是C#中的關鍵字,它要鎖定一個資源,lock的特點是:同一時刻只能有一個執行緒進入lock的物件的範圍,其它lock的執行緒都要等待。我們看下面最佳化後的程式碼:


using System;
using System.Threading;

namespace ThreadSynchDemo
{
    class Program
    {
        private static int Counter = 0;
        // 定義一個locker物件
        private static Object locker = new Object();
        static void Main(string[] args)
        {
            #region 存線上程同步問題
            //Thread t1 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        Counter++;
            //        Thread.Sleep(1);
            //    }
            //});
            //t1.Start();

            //Thread t2 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        Counter++;
            //        Thread.Sleep(1);
            //    }
            //});
            //t2.Start(); 
            #endregion

            #region 使用Lock解決執行緒同步問題
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    lock(locker)
                    {
                        Counter++;
                    }
                    Thread.Sleep(1);
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    lock (locker)
                    {
                        Counter++;
                    }
                    Thread.Sleep(1);
                }
            });
            t2.Start();
            #endregion

            Thread.Sleep(3000);
            Console.WriteLine(Counter);
            Console.ReadKey();
        }
    }
}

注意:lock只能鎖住同一個物件,如果是不同的物件,還是會有執行緒同步的問題。lock鎖定的物件必須是引用型別的物件。

我們在定義一個Object型別的物件,lock分別鎖住兩個物件,看看是什麼結果:


using System;
using System.Threading;

namespace ThreadSynchDemo
{
    class Program
    {
        private static int Counter = 0;
        // 定義一個locker物件
        private static Object locker = new Object();
        // 定義locker2
        private static Object locker2 = new Object();
        static void Main(string[] args)
        {
            #region 存線上程同步問題
            //Thread t1 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        Counter++;
            //        Thread.Sleep(1);
            //    }
            //});
            //t1.Start();

            //Thread t2 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        Counter++;
            //        Thread.Sleep(1);
            //    }
            //});
            //t2.Start(); 
            #endregion

            #region 使用Lock解決執行緒同步問題
            //Thread t1 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        lock(locker)
            //        {
            //            Counter++;
            //        }
            //        Thread.Sleep(1);
            //    }
            //});
            //t1.Start();

            //Thread t2 = new Thread(() => {
            //    for (int i = 0; i < 1000; i++)
            //    {
            //        lock (locker)
            //        {
            //            Counter++;
            //        }
            //        Thread.Sleep(1);
            //    }
            //});
            //t2.Start();
            #endregion

            #region 使用lock鎖住不同的物件也會有執行緒同步問題
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    lock (locker)
                    {
                        Counter++;
                    }
                    Thread.Sleep(1);
                }
            });
            t1.Start();

            Thread t2 = new Thread(() => {
                for (int i = 0; i < 1000; i++)
                {
                    lock (locker2)
                    {
                        Counter++;
                    }
                    Thread.Sleep(1);
                }
            });
            t2.Start();
            #endregion
            Thread.Sleep(3000);
            Console.WriteLine(Counter);
            Console.ReadKey();
        }
    }
}

可以看到,這時還是會有執行緒同步的問題。雖然使用了lock,但是我們鎖住的是不同的物件,這樣也會有執行緒同步問題。lock必須鎖住同一個物件才可以。

我們下面在來看一個多執行緒同步問題的例子:


using System;
using System.Threading;

namespace ThreadSynchDemo2
{
    class Program
    {
        static int Money = 100;

        /// <summary>
        /// 定義一個取錢的方法
        /// </summary>
        /// <param name="name"></param>
        static void QuQian(string name)
        {
            Console.WriteLine(name + "檢視一下餘額" + Money);
            int yue = Money - 1;
            Console.WriteLine(name + "取錢");
            Money = yue;
            Console.WriteLine(name + "取完了,剩" + Money);
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    QuQian("t2");
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("餘額" + Money);
            Console.ReadKey();
        }
    }
}

可以看到,最終的餘額並不是80,這也是執行緒同步帶來的問題,如何解決。解決思路就是使用同步的技術避免兩個執行緒同時修改一個餘額。

(2)最大粒度——同步方法

在方法上面使用[MethodImpl(MethodImplOptions.Synchronized)],標記該方法是同步方法,這樣一個方法只能同時被一個執行緒訪問。我們在QuQian的方法上面標記,修改後的程式碼如下:


using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ThreadSynchDemo2
{
    class Program
    {
        static int Money = 100;

        /// <summary>
        /// 定義一個取錢的方法,在上面標記為同步方法
        /// </summary>
        /// <param name="name"></param>
        [MethodImpl(MethodImplOptions.Synchronized)]
        static void QuQian(string name)
        {
            Console.WriteLine(name + "檢視一下餘額" + Money);
            int yue = Money - 1;
            Console.WriteLine(name + "取錢");
            Money = yue;
            Console.WriteLine(name + "取完了,剩" + Money);
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    QuQian("t2");
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("餘額" + Money);
            Console.ReadKey();
        }
    }
}

現在的方法就是“執行緒安全”的了。什麼是“執行緒安全”呢?“執行緒安全”是指方法可以被多個執行緒隨意呼叫,而不會出現混亂。如果出現了混亂,那麼就是“執行緒不安全”的。“執行緒安全”的方法可以在多執行緒裡面隨意的使用。

(3)物件互斥鎖

物件互斥鎖就是我們上面講的lock。我們在用lock來修改上面QuQian的例子:


using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ThreadSynchDemo2
{
    class Program
    {
        static int Money = 100;

        /// <summary>
        /// 定義一個取錢的方法,在上面標記為同步方法
        /// </summary>
        /// <param name="name"></param>
        //[MethodImpl(MethodImplOptions.Synchronized)]
        //static void QuQian(string name)
        //{
        //    Console.WriteLine(name + "檢視一下餘額" + Money);
        //    int yue = Money - 1;
        //    Console.WriteLine(name + "取錢");
        //    Money = yue;
        //    Console.WriteLine(name + "取完了,剩" + Money);
        //}

        private static object locker = new object();
        static void QuQian(string name)
        {
            Console.WriteLine(name + "檢視一下餘額" + Money);
            int yue = Money - 1;
            Console.WriteLine(name + "取錢");
            Money = yue;
            Console.WriteLine(name + "取完了,剩" + Money);
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    // 使用物件互斥鎖
                    lock(locker)
                    {
                        QuQian("t1");
                    }
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    lock (locker)
                    {
                        QuQian("t2");
                    }
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("餘額" + Money);
            Console.ReadKey();
        }
    }
}

可以看到,最終的輸出結果還是80。

同一時刻只能有一個執行緒進入同一個物件的lock程式碼塊。必須是同一個物件才能起到互斥的作用。lock後必須是引用型別,不一定是object,只要是物件就行。

鎖物件選擇很重要,選不對就起不到同步的作用;選不對還有可能會造成其他地方被鎖,比如用字串做鎖(因為字串緩衝池導致導致可能用的是其他地方正在使用的鎖),所以不建議使用字串做鎖。下面的程式碼就是不允許的:

lock("locker")

兩個方法如果都用一個物件做鎖,那麼訪問A的時候就不能訪問B,因此鎖選擇很重要。

(4)Monitor

其實lock關鍵字就是對Monitor的簡化呼叫,lock最終會被編譯成Monitor,因此一般不直接使用Monitor類,看下面程式碼:


using System;
using System.Threading;

namespace MonitorDemo
{
    class Program
    {
        static int Money = 100;
        private static object locker = new object();
        static void QuQian(string name)
        {
            // 等待沒有人鎖定locker物件,就鎖定它,然後繼續執行
            Monitor.Enter(locker);
            try
            {
                Console.WriteLine(name + "檢視一下餘額" + Money);
                int yue = Money - 1;
                Console.WriteLine(name + "取錢");
                Money = yue;
                Console.WriteLine(name + "取完了,剩" + Money);
            }
            finally
            {
                // 釋放locker物件的鎖
                Monitor.Exit(locker);
            }
        }

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                        QuQian("t1");
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                        QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("餘額" + Money);
            Console.ReadKey();
        }
    }
}

Monitor類裡面還有TryEnter方法,如果Enter的時候有人在佔用鎖,它不會等待,而是會返回false。看下面的示例程式碼:


using System;
using System.Threading;

namespace MonitorDemo
{
    class Program
    {
        static int Money = 100;
        private static object locker = new object();
        static void QuQian(string name)
        {
            // 等待沒有人鎖定locker物件,就鎖定它,然後繼續執行
            Monitor.Enter(locker);
            try
            {
                Console.WriteLine(name + "檢視一下餘額" + Money);
                int yue = Money - 1;
                Console.WriteLine(name + "取錢");
                Money = yue;
                Console.WriteLine(name + "取完了,剩" + Money);
            }
            finally
            {
                // 釋放locker物件的鎖
                Monitor.Exit(locker);
            }
        }

        static void F1(int i)
        {
            if (!Monitor.TryEnter(locker))
            {
                Console.WriteLine("有人在鎖著呢");
                return;
            }
            Console.WriteLine(i);
            Monitor.Exit(locker);
        }


        static void Main(string[] args)
        {
            //Thread t1 = new Thread(() => {
            //    for (int i = 0; i < 10; i++)
            //    {
            //            QuQian("t1");
            //    }
            //});
            //Thread t2 = new Thread(() => {
            //    for (int i = 0; i < 10; i++)
            //    {
            //            QuQian("t2");
            //    }
            //});
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    F1(i);
                }
            });
            Thread t2 = new Thread(() => {
                for (int i = 0; i < 10; i++)
                {
                    F1(i);
                }
            });

            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("餘額" + Money);
            Console.ReadKey();
        }
    }
}

相關文章