黑馬程序員-.NET基礎之多線程

------- Windows Phone 7手機開發.Net培訓、期待與您交流! -------

   

    在黑馬論壇上提出了一個關於多線程的定時任務的問題,可能是我們Asp.Net板塊論壇太冷清了,回答的人很少,也有可能自己提出的問題too easy,別人不屑回答。現將自己關於多線程學到整理成筆記,供大家參考。

一、線程處理概述

進程是操作系統中正在執行的不同應用程序的一個實例,操作系統把不同的進程分離開來。
線程是操作系統分配處理器時間的基本單元,每個線程都維護異常處理程序、調度優先級和一組系統用於在調度該線程前保存線程上下文的結構。
每個應用程序域都是用單個線程啓動的(應用程序的入口點Main方法),應用程序域中的代碼可以創建附加應用程序域和附加線程。

線程的優缺點

線程處理使程序能夠執行併發處理,因而特別適合需要同時執行多個操作的場合。
多線程處理可解決用戶響應性能和多任務的問題,但同時引入了資源共享和同步問題等問題。

二、創建多線程應用程序

    應用程序運行時,將創建新的應用程序域。當運行環境調用應用程序的入口點(Main方法)時,將創建應用程序主線程。下面給出一段主線程示例。

using System;
using System.Threading;
namespace CSharpPractice.Threading
{
    public class WorkerThreadExample
    {
        static void Main()
        {
            Console.WriteLine("主線程:開始……");
            // 主線程睡眠5秒鐘
            Thread.Sleep(5000);
            Console.WriteLine("主線程:結束。");
            Console.ReadLine();
        }
    }
}


 

2.創建和啓動新線程

主線程以外的線程一般稱之爲工作線程。創建新線程的大致步驟如下:
創建一個將在主線程外執行的函數,即類的方法,用於執行新線程要執行的邏輯操作
在主線程(Main方法)中創建一個Thread的實例,指向步驟1中的函數。例如:Thread newThread = new Thread(anObject.AMethod)
調用創建的Thread的實例的Start()方法,以啓動新線程。例如:newThread.Start()。創建和啓動新線程示例。

using System;
using System.Threading;
namespace CSharpPractice.Threading
{
    public class Worker
    {    // 工作線程執行邏輯的實現方法.
        public void DoWork()
        {
            while (!_shouldStop)
            {
                Console.WriteLine("工作線程:working...");
            }
            Console.WriteLine("工作線程:terminating gracefully.");
        }
        public void RequestStop()
        {
            _shouldStop = true;
        }
        // 使用Volatile以啓發編譯器,本數據成員將被多線程訪問.
        private volatile bool _shouldStop;
    }
    public class WorkerThreadExample
    {
        static void Main()
        {
            Console.WriteLine("主線程:Starting worker thread...");
            // 創建工作線程對象。但不啓動線程.
            Worker workerObject = new Worker();
            Thread workerThread = new Thread(workerObject.DoWork);
            // 啓動工作線程.
            workerThread.Start();
            // 循環直至激活工作線程.
            while (!workerThread.IsAlive) ;
            // 讓主線程睡眠1毫秒,以允許工作線程完成自己的工作:
            Thread.Sleep(1);
            // 要求工作線程停止自己:
            workerObject.RequestStop();
            // 使用Join方法阻止當前線程,直至對象線程終止.
            workerThread.Join();
            Console.WriteLine("主線程:Worker thread has terminated.");
            Console.ReadLine();
        }
    }
}


 

3.暫停、中斷和銷燬線程

調用Thread.Sleep方法會導致當前線程立即阻止,阻止時間的長度等於傳遞給Thread.Sleep 的毫秒數。注意:一個線程不能針對另一個線程調用Thread.Sleep。
暫停線程示例

using System;
using System.Threading;
namespace CSharpPractice.Threading
{
    class PauseThreadTest
    {
        static void Main()
        {
            Console.WriteLine("主線程啓動...");
            //創建並啓動工作線程
            Thread newThread = new Thread(ThreadMethod);
            //newThread.SetApartmentState(ApartmentState.MTA);
            newThread.Start();
            Console.WriteLine("主線程睡眠300ms...");
            Thread.Sleep(300);
            Console.WriteLine("主線程終止...");
            Console.ReadKey();
        }
        static void ThreadMethod()
        {
            Console.WriteLine("工作線程啓動...");
            Console.WriteLine("工作線程睡眠1000ms...");
            Thread.Sleep(1000);
            Console.WriteLine("工作線程終止...");
        }
    }
}

 

通過對被阻止的線程調用Thread.Interrupt,可以中斷正在等待的線程並引發ThreadInterruptedException,從而使該線程脫離造成阻止的調用。
中斷線程示例

using System;
using System.Security.Permissions;
using System.Threading;
namespace CSharpPractice.Threading
{
    class ThreadInterrupt
    {
        static void Main()
        {
            StayAwake stayAwake = new StayAwake();
            Thread newThread = new Thread(stayAwake.ThreadMethod);
            newThread.Start();
            // 如果newThread 當前正被阻止或者將被阻止
            // 下面的代碼將在ThreadMethod中產生一個異常
            Console.WriteLine("主線程調用newThread的Interrupt方法.");
            newThread.Interrupt();
            // 等待newThread結束.
            newThread.Join();
            Console.ReadKey();
        }
    }

    class StayAwake
    {
        public void ThreadMethod()
        {
            Console.WriteLine("newThread正在執行ThreadMethod……");
            try
            {
                Console.WriteLine("newThread 將進入睡眠……");
                Thread.Sleep(Timeout.Infinite);
            }
            catch (ThreadInterruptedException e)
            {
                Console.WriteLine("newThread 被主線程中斷.");
            }
        }
    }
}


Abort 方法用於永久地停止也即銷燬託管線程。調用Abort時,公共語言運行庫在目標線程中引發ThreadAbortException,目標線程可捕捉此異常。
銷燬線程示例

using System;
using System.Threading;
namespace CSharpPractice.Threading
{
    class Test
    {
        public static void Main()
        {
            Thread newThread = new Thread(new ThreadStart(TestMethod));
            newThread.Start();
            Thread.Sleep(1000);
            // 銷燬newThread.
            Console.WriteLine("主程序銷燬新線程……");
            newThread.Abort("來自於主程序的信息.");
            // 等待線程終止.
            newThread.Join();
            Console.WriteLine("新線程終止 – 退出主程序.");
            Console.ReadLine();
        }
        static void TestMethod()
        {
            try
            {
                while (true)
                {
                    Console.WriteLine("新線程運行中……");
                    Thread.Sleep(1000);
                }
            }
            catch (ThreadAbortException abortException)
            {
                Console.WriteLine((string)abortException.ExceptionState);
            }
        }
    }
}


 

三、線程優先級和線程調度

每個線程都有一個分配的優先級,在運行庫內創建的線程最初被分配 Normal 優先級。通過線程的Priority屬性可以獲取和設置其優先級。下面是線程優先級和線程調度示例。

using System;
using System.Threading;
namespace CSharpPractice.Threading
{
    class PriorityTest
    {
        bool loopSwitch;
        public PriorityTest()
        {
            loopSwitch = true;
        }
        public bool LoopSwitch
        {
            set { loopSwitch = value; }
        }
        public void ThreadMethod()
        {
            long threadCount = 0;
            while (loopSwitch)
            {
                threadCount++;
            }
            Console.WriteLine("{0} with {1,11} priority " +
                "has a count = {2,13}", Thread.CurrentThread.Name,
                Thread.CurrentThread.Priority.ToString(),
                threadCount.ToString("N0"));
        }
    }
    class Test
    {
        static void Main()
        {
            PriorityTest priorityTest = new PriorityTest();
            ThreadStart startDelegate =
                new ThreadStart(priorityTest.ThreadMethod);
            Thread threadOne = new Thread(startDelegate);
            threadOne.Name = "ThreadOne";
            Thread threadTwo = new Thread(startDelegate);
            threadTwo.Name = "ThreadTwo";
            threadTwo.Priority = ThreadPriority.BelowNormal;
            Console.WriteLine("啓動線程1(優先級默認爲Normal)......");
            threadOne.Start();
            Console.WriteLine("啓動線程2(優先級設置爲BelowNormal)......");
            threadTwo.Start();
            // 允許數10 seconds.
            Console.WriteLine("請耐心等待5秒......");
            Thread.Sleep(5000);
            priorityTest.LoopSwitch = false;
            Console.ReadLine();
        }
    }
}


 

四、線程同步

    當多個線程可以調用單個對象的屬性和方法時,一個線程可能會中斷另一個線程正在執行的任務,使該對象處於一種無效狀態。因此必須針對這些調用進行同步處理。可以使用lock語句同步代碼塊,或使用監視器同步代碼塊。

 

2.使用lock語句同步代碼塊

lock語句使用lock關鍵字將語句塊標記爲臨界區,方法是獲取給定對象的互斥鎖,執行語句,然後釋放該鎖。lock關鍵字可以確保當一個線程位於代碼的臨界區時,另一個線程不會進入該臨界區。如果其他線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。代碼塊完成運行,而不會被其他線程中斷。
lock語句以關鍵字lock開頭,並以一個對象作爲參數,在該參數的後面爲線程互斥的代碼塊。
使用lock語句同步代碼塊示例

using System;
using System.Threading;
namespace CSharpPractice.Threading
{
    class Account                   //賬戶類
    {
        private Object thisLock = new Object();
        int balance;
        Random r = new Random();   //準備生成隨機數
        public Account(int initial)     //賬戶構造函數
        {
            balance = initial;
        }
        int Withdraw(int amount)     //從賬戶中取款
        {
            // 本條件永遠不會爲True,除非註釋掉lock語句
            if (balance < 0)           //賬戶餘額不足,<=0
            {
                throw new Exception("賬戶餘額不足(<=0)!");
            }
            // 註釋掉下面的lock語句,測試lock關鍵字的效果 
            // lock (thisLock)
            {
                if (balance >= amount)   //賬戶餘額>取款額
                {
                    Console.WriteLine("取款前賬戶餘額: " + balance);  //取款前賬戶餘額
                    Console.WriteLine("取款額(-)     : -" + amount);   //取款額
                    balance = balance - amount;
                    Console.WriteLine("取款後賬戶餘額: " + balance);  //取款後賬戶餘額
                    return amount;
                }
                else
                {
                    return 0;   // 拒絕交易
                }
            }
        }
        public void DoTransactions()  // 執行交易DoTransactions()
        {  // 從賬戶中取100次錢款,每次取款額爲1~100中的隨機數
            for (int i = 0; i < 100; i++)
            {
                Withdraw(r.Next(1, 100));
            }
        }
    }
    class Test
    {
        static void Main()
        {
            Thread[] threads = new Thread[10];    //線程數組(10個元素)
            Account acc = new Account(1000);      //新建賬戶對象,初始存款額爲1000
            for (int i = 0; i < 10; i++)          //循環10次
            {   //每次開始新的線程,執行賬戶交易DoTransactions()
                Thread t = new Thread(new ThreadStart(acc.DoTransactions));
                threads[i] = t;
            }
            for (int i = 0; i < 10; i++)          //循環10次
            {
                threads[i].Start();               //開始線程
            }
            Console.ReadLine();
        }
    }
}

 

3.使用監視器同步代碼塊

    使用監視器(Monitor)也可以防止多個線程同時執行代碼塊。調用Monitor.Enter方法,允許一個且僅一個線程繼續執行後面的語句;其他所有線程都將被阻止,直到執行語句的線程調用Exit。

 

4.同步事件和等待句柄

同步事件允許線程通過發信號互相通信,從而實現線程需要獨佔訪問的資源的同步處理控制
同步事件有兩種:AutoResetEvent(自動重置的本地事件)和 ManualResetEvent(手動重置的本地事件)
每種事件又包括兩種狀態:收到信號狀態(signaled)和未收到信號狀態(unsignaled)。下面是同步事件和等待句柄示例。

using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
namespace CSharpPractice.Threading
{
    public class SyncEvents
    {
        public SyncEvents()
        {
            _newItemEvent = new AutoResetEvent(false);
            _exitThreadEvent = new ManualResetEvent(false);
            _eventArray = new WaitHandle[2];
            _eventArray[0] = _newItemEvent;
            _eventArray[1] = _exitThreadEvent;
        }
        public EventWaitHandle ExitThreadEvent
        {
            get { return _exitThreadEvent; }
        }
        public EventWaitHandle NewItemEvent
        {
            get { return _newItemEvent; }
        }
        public WaitHandle[] EventArray
        {
            get { return _eventArray; }
        }
        private EventWaitHandle _newItemEvent;
        private EventWaitHandle _exitThreadEvent;
        private WaitHandle[] _eventArray;
    }
    public class Producer             //製造者線程
    {
        public Producer(Queue<int> q, SyncEvents e)  //構造函數
        {
            _queue = q;
            _syncEvents = e;
        }
        // Producer.ThreadRun方法一直循環,直到“退出線程”事件變爲終止狀態。
        // 此事件的狀態由 WaitOne 方法使用 SyncEvents 類定義的 ExitThreadEvent 屬性進行測試。
        // 在這種情況下,檢查該事件的狀態不會阻止當前線程,
        // 因爲 WaitOne 使用的第一個參數爲零,這表示該方法應立即返回。
        // 如果 WaitOne 返回 true,則說明該事件當前處於終止狀態。
        // ThreadRun 方法將返回,其效果相當於終止執行此方法的輔助線程。
        public void ThreadRun()
        {
            int count = 0;
            Random r = new Random();
            // 在“退出線程”事件終止前,Producer.ThreadStart 方法將嘗試在隊列中保留 20 項。
            // 項只是 0 到 100 之間的一個整數。在添加新項前,必須鎖定該集合,
            // 以防止使用者線程和主線程同時訪問該集合。這一點是使用 lock 關鍵字完成的。
            // 傳遞給 lock 的參數是通過 ICollection 接口公開的 SyncRoot 字段。
            // 此字段專門爲同步線程訪問而提供。對該集合的獨佔訪問權限
            // 被授予 lock 後面的代碼塊中包含的所有指令。對於製造者添加
            // 到隊列中的每個新項,都將調用“新項”事件的 Set 方法。
            // 這將通知使用者線程離開掛起狀態並開始處理新項。
            while (!_syncEvents.ExitThreadEvent.WaitOne(0, false))
            {
                lock (((ICollection)_queue).SyncRoot)
                {
                    while (_queue.Count < 20)
                    {
                        _queue.Enqueue(r.Next(0, 100));
                        _syncEvents.NewItemEvent.Set();
                        count++;
                    }
                }
            }
            Console.WriteLine("製造者線程: produced {0} items", count);
        }
        private Queue<int> _queue;
        private SyncEvents _syncEvents;
    }
    public class Consumer       //使用者線程
    {
        public Consumer(Queue<int> q, SyncEvents e)
        {
            _queue = q;
            _syncEvents = e;
        }
        // Consumer.ThreadRun
        // Consumer 對象還定義名爲 ThreadRun 的方法。
        // 與製造者的 ThreadRun 類似,此方法由 Main 方法創建的輔助線程執行。
        // 然而,使用者的 ThreadStart 必須響應兩個事件。
        public void ThreadRun()
        {
            int count = 0;
            // 使用 WaitAny 阻止使用者線程,直到所提供的數組中的任意一個
            // 等待句柄變爲終止狀態。在這種情況下,數組中有兩個句柄,
            // 一個用來終止輔助線程,另一個用來指示有新項添加到集合中。
            // WaitAny 返回變爲終止狀態的事件的索引。“新項”事件是數組中的
            // 第一個事件,因此索引零表示新項。在這種情況下,檢查索引 1
            //(它指示“退出線程”事件),並使用它來確定此方法是否繼續使用項。
            // 如果“新項”事件處於終止狀態,將通過 lock 獲得對集合的
            // 獨佔訪問權限並使用新項。因爲此示例生成並使用數千個項,
            // 所以不顯示使用的每個項,而是使用 Main 定期顯示隊列中的內容
            while (WaitHandle.WaitAny(_syncEvents.EventArray) != 1)
            {
                lock (((ICollection)_queue).SyncRoot)
                {
                    int item = _queue.Dequeue();
                }
                count++;
            }
            Console.WriteLine("使用者線程: consumed {0} items", count);
        }
        private Queue<int> _queue;
        private SyncEvents _syncEvents;
    }
    public class ThreadSyncSample
    {   // 線程同步示例:使用 lock 獲得對隊列的獨佔訪問權限
        private static void ShowQueueContents(Queue<int> q) //此方法由主線程執行的(被Main調用)
        {
            lock (((ICollection)q).SyncRoot)
            {   //  對整個集合進行枚舉
                foreach (int item in q)
                {
                    Console.Write("{0} ", item);
                }
            }
            Console.WriteLine();
        }
        static void Main()
        {   // 創建一個隊列(該隊列的內容將被生成和使用)
            Queue<int> queue = new Queue<int>();
            // 創建SyncEvents 的一個實例
            SyncEvents syncEvents = new SyncEvents();
            Console.WriteLine("配置製造者線程和使用者線程......");
            // 配置 Producer 和 Consumer 對象以供輔助線程使用
            Producer producer = new Producer(queue, syncEvents);
            Consumer consumer = new Consumer(queue, syncEvents);
            Thread producerThread = new Thread(producer.ThreadRun);
            Thread consumerThread = new Thread(consumer.ThreadRun);
            // 調用 Start 方法來啓動兩個輔助線程。
            // 獨立於當前正在執行 Main 方法的主線程開始異步執行過程
            Console.WriteLine("啓動製造者線程和使用者線程......");
            producerThread.Start();
            consumerThread.Start();
            // Main通過調用 Sleep 方法將主線程掛起。
            for (int i = 0; i < 4; i++)            //重複四次
            {  // 將當前正在執行的線程掛起2500毫秒
                Thread.Sleep(2500);
                // 2500毫秒後,Main 將重新激活,顯示隊列的內容
                ShowQueueContents(queue);
            }
            // Main通過調用“退出線程”事件的 Set 方法通知輔助線程終止
            Console.WriteLine("發信號,告知終止線程......");
            syncEvents.ExitThreadEvent.Set();
            //對每個輔助線程調用 Join 方法以阻止主線程,
            //直到每個輔助線程都響應該事件並終止。
            producerThread.Join();
            consumerThread.Join();
            Console.ReadLine();
        }
    }

 

5.使用Mutex同步代碼塊

mutex(mutually exclusive,互斥體)由 Mutex 類表示,與監視器(Monitor)類似,用於防止多個線程在某一時間同時執行某個代碼塊。
與監視器不同的是,mutex 可以用來使跨進程的線程同步。儘管mutex可以用於進程內的線程同步,但是它會消耗更多的計算資源,所以進程內的線程同步建議使用監視器(Monitor)。
當用於進程間同步時,mutex 稱爲“命名mutex”,因爲它將用於另一個應用程序,因此它不能通過全局變量或靜態變量共享。必須給它指定一個名稱,才能使兩個應用程序訪問同一個mutex 對象。
Mutex 有兩種類型:未命名的局部 mutex 和已命名的系統 mutex。下面是使用Mutex同步代碼塊示例。

// 本例顯示如何使用“命名mutex”在進程或線程間發送信號。
// 在2個或多個命令行窗口運行本程序。
// 每個進程將創建一個Mutex對象:命名互斥體"MyMutex".
// 命名mutex是一個系統對象,其生命週期
// 由其所代表的Mutex 對象的生命週期所確定。
// 當第一個進程創建其局部Mutex時創建命名mutex。
// 本例中,命名mutex屬於第一個進程。
// 當銷燬所有Mutex對象時,釋放此命名mutex
using System;
using System.Threading;
namespace CSharpPractice.Threading
{
    public class Test
    {
        public static void Main()
        {
            // 創建命名mutex。只能存在一個名爲"MyMutex"的系統對象。
            Mutex m = new Mutex(false, "MyMutex");
            // 試圖獲取對命名mutex的控制權。 
            // 如果命名mutex被另一個線程所控制,則等待直至其被釋放 
            Console.WriteLine("等待Mutex. . . . . .");
            m.WaitOne();
            // 保持對mutex的控制,直至用戶按ENTER鍵.
            Console.WriteLine("本應用擁有mutex。請按ENTER鍵釋放之並退出!");
            Console.ReadLine();
            m.ReleaseMutex();
        }
    }
}

 

五、線程池

線程池是可以用來在後臺執行多個任務的線程集合,這使主線程可以自由地異步執行其他任務。線程池通常用於服務器應用程序。每個傳入請求都將分配給線程池中的一個線程,因此可以異步處理請求,而不會佔用主線程,也不會延遲後續請求的處理。
一旦線程池中的某個線程完成任務,它將返回到等待線程隊列中,等待被再次使用。這種重用使應用程序可以避免爲每個任務創建新線程的開銷。

using System;
using System.Threading;
namespace CSharpPractice.Threading
{
    public class Fibonacci
    {
        public Fibonacci(int n, ManualResetEvent doneEvent)
        {
            _n = n;
            _doneEvent = doneEvent;
        }

        // 線程池回調:計算 Fibonacci 結果.
        public void ThreadPoolCallback(Object threadContext)
        {
            int threadIndex = (int)threadContext;
            Console.WriteLine("線程 {0} 開始......", threadIndex);
            _fibOfN = Calculate(_n);
            Console.WriteLine("線程 {0} 結果計算......", threadIndex);
            _doneEvent.Set();
        }
        // 遞歸方法計算第N個Fibonacci數:F (n ) = F (n - 1) + F(n - 2).
        public int Calculate(int n)
        {
            if (n <= 1)           // F(0)=0; F(1)=1
            {
                return n;
            }
            return Calculate(n - 1) + Calculate(n - 2);
        }
        public int N { get { return _n; } }
        private int _n;
        public int FibOfN { get { return _fibOfN; } }
        private int _fibOfN;
        private ManualResetEvent _doneEvent;
    }
    public class ThreadPoolExample    // 線程池
    {
        static void Main()
        {
            const int FibonacciCalculations = 5;    // 5個Fibonacci數
            // 每個事件用於每個Fibonacci對象
            ManualResetEvent[] doneEvents = new ManualResetEvent[FibonacciCalculations];
            Fibonacci[] fibArray = new Fibonacci[FibonacciCalculations];
            Random r = new Random();
            // 使用ThreadPool配置和啓動線程:
            Console.WriteLine("啓動 {0} 個任務......", FibonacciCalculations);
            for (int i = 0; i < FibonacciCalculations; i++)
            {
                doneEvents[i] = new ManualResetEvent(false);
                Fibonacci f = new Fibonacci(r.Next(1, 10), doneEvents[i]);
                fibArray[i] = f;
                ThreadPool.QueueUserWorkItem(f.ThreadPoolCallback, i);
            }
            // 等待線程池中的所有線程的計算...
            WaitHandle.WaitAll(doneEvents);
            Console.WriteLine("完成所有計算!");
            // 顯示結果:第1項到第10項之間的5個Fibonacci數...
            Console.WriteLine("第1項[F(0)]到第10項[F(9)]之間的任意5個Fibonacci數爲----");
            for (int i = 0; i < FibonacciCalculations; i++)
            {
                Fibonacci f = fibArray[i];
                Console.WriteLine("Fibonacci({0}) = {1}", f.N, f.FibOfN);
            }
            Console.ReadLine();
        }
    }
}


 

 

------- Windows Phone 7手機開發.Net培訓、期待與您交流! -------

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章