無論您是為具有單個處理器的計算機還是為具有多個處理器的計算機進行開發,您都希望應用程序為用戶提供最好的響應性能,即使應用程序當前正在完成其他工作。要使應用程序能夠快速響應用戶操作,同時在用戶事件之間或者甚至在用戶事件期間利用處理器,最強大的方式之一是使用多線程技術。
多線程:線程是程序中一個單一的順序控制流程.在單個程序中同時運行多個線程完成不同的工作,稱為多線程。如果某個線程進行一次長延遲操作, 處理器就切換到另一個線程執行。這樣,多個線程的并行(并發)執行隱藏了長延遲,提高了處理器資源利用率,從而提高了整體性能。多線程是為了同步完成多項任務,不是為了提高運行效率,而是為了提高資源使用效率來提高系統的效率
一、進程與線程
進程,是操作系統進行資源調度和分配的基本單位。是由進程控制塊、程序段、數據段三部分組成。一個進程可以包含若干線程(Thread),線程可以幫助應用程序同時做幾件事(比 如一個線程向磁盤寫入文件,另一個則接收用戶的按鍵操作并及時做出反應,互相不干擾),在程序被運行后中,系統首先要做的就是為該程序進程建立一個默認線程,然后程序可 以根據需要自行添加或刪除相關的線程。它是可并發執行的程序。在一個數據集合上的運行過程,是系統進行資源分配和調度的一個獨立單位,也是稱活動、路徑或任務,它有兩方面性質:活動性、并發性。進程可以劃分為運行、阻塞、就緒三種狀態,并隨一定條件而相互轉化:就緒--運行,運行--阻塞,阻塞--就緒。
線程(thread),線程是CPU調度和執行的最小單位。有時被稱為輕量級進程(Lightweight Process,LWP),是程序執行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。另外,線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創建和撤消另一個線程,同一進程中的多個線程之間可以并發執行。由于線程之間的相互制約,致使線程在運行中呈現出間斷性。線程也有就緒、阻塞和運行三種基本狀態。
主線程,進程創建時,默認創建一個線程,這個線程就是主線程。主線程是產生其他子線程的線程,同時,主線程必須是最后一個結束執行的線程,它完成各種關閉其他子線程的操作。盡管主線程是程序開始時自動創建的,它也可以通過Thead類對象來控制,通過調用CurrentThread方法獲得當前線程的引用
多線程的優勢:進程有獨立的地址空間,同一進程內的線程共享進程的地址空間。啟動一個線程所花費的空間遠遠小于啟動一個進程所花費的空間,而且,線程間彼此切換所需的時間也遠遠小于進程間切換所需要的時間。
二、多線程優點
1、提高應用程序響應。這對圖形界面的程序尤其有意義,當一個操作耗時很長時,整個系統都會等待這個操作,此時程序不會響應鍵盤、鼠標、菜單的操作,而使用多線程技術,將耗時長的操作(time consuming)置于一個新的線程,可以避免這種尷尬的情況。
2、使多CPU系統更加有效。操作系統會保證當線程數不大于CPU數目時,不同的線程運行于不同的CPU上。
3、改善程序結構。一個既長又復雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序會利于理解和修改。。
多線程盡管優勢明顯,但是線程并發沖突、同步以及管理跟蹤,可能給系統帶來很多不確定性,這些必須引起足夠重視。
廢話不多說開始我們的多線程之旅。
三、多線程的應用場合:
簡單總結了一下,一般有兩種情況:
1)多個線程,完成同類任務,提高并發性能
2)一個任務有多個獨立的步驟,多個線程并發執行各子任務,提高任務處理效率
四、案例--搬運工
在我們現實生活中,經??吹竭@樣的場景。有一堆貨物,有幾個搬運工負責將貨物搬運到指定地點。但是搬運工能力不同,有人一次能搬多箱,有人走路比較慢,搬運一趟的時間間隔比較長。搬運工,各自搬運,無先后,互不干擾。我們如何在程序中實現這種場景呢?
案例分析:
這個就是最簡單的多線程的實際案例。每個人相當于一個線程,并發執行。當貨物搬運完畢后,每個線程自動停止。這里暫時不考慮死鎖情況。
案例代碼:
namespace MutiThreadSample.Transport
{
/// <summary>
/// 搬運工
/// </summary>
public class Mover
{
/// <summary>
/// 總數
/// </summary>
public static int GoodsTotal { get; set; }
/// <summary>
/// 間隔時間
/// </summary>
public static int IntervalTime { get; set; }
/// <summary>
/// 名稱
/// </summary>
public string Name { get; set; }
/// <summary>
/// 單位時間搬運量
/// </summary>
public int LaborAmount { get; set; }
/// <summary>
/// 搬運
/// </summary>
public void Move()
{
while (GoodsTotal > 0)
{
GoodsTotal -= LaborAmount;
Console.WriteLine("搬運者:{0} 于 {1} 搬運貨物 {2}",this.Name,DateTime.Now.Millisecond,this.LaborAmount);
Thread.Sleep(IntervalTime);
Console.WriteLine("搬運者:{0} Continue",this.Name);
}
}
/// <summary>
/// 搬運
/// </summary>
/// <param name="interval">時間間隔</param>
public void Move(object interval)
{
int tempInterval = 0;
if (!int.TryParse(interval.ToString(), out tempInterval))
{
tempInterval = IntervalTime;
}
while (GoodsTotal > 0)
{
GoodsTotal -= LaborAmount;
Console.WriteLine("搬運者:{0} 于 {1} 搬運貨物 {2}", this.Name, DateTime.Now.Millisecond, this.LaborAmount);
Thread.Sleep(tempInterval);
}
}
}
}
測試:
namespace MutiThreadSample.Transport
{
/// <summary>
/// 測試搬運
/// </summary>
public class TestMove
{
/// <summary>
/// 搬運
/// </summary>
public static void Move()
{
//測試搬運工
Mover.GoodsTotal = 200;
Mover.IntervalTime = 10;
Mover m1 = new Mover() { Name = "Tom", LaborAmount = 5 };
Mover m2 = new Mover() { Name = "Jim", LaborAmount = 10 };
Mover m3 = new Mover() { Name = "Lucy", LaborAmount = 20 };
List<Mover> movers = new List<Mover>();
movers.Add(m1);
//movers.Add(m2);
//movers.Add(m3);
if (movers != null && movers.Count > 0)
{
foreach (Mover m in movers)
{
Thread thread = new Thread(new ThreadStart(m.Move));
thread.Start();
}
}
//Main Thread continue
// validate Thread.Sleep()
//int i =0;
//int j = 0;
//while (i < 10)
//{
// while(j<10000000)
// {
// j++;
// }
// Console.WriteLine("CurrentThread:{0}", Thread.CurrentThread.Name);
// i++;
//}
}
/// <summary>
/// 搬運
/// </summary>
public static void MoveWithParamThread()
{
//測試搬運工
Mover.GoodsTotal = 1000;
Mover.IntervalTime = 100;
Mover m1 = new Mover() { Name = "Tom", LaborAmount = 5 };
Mover m2 = new Mover() { Name = "Jim", LaborAmount = 10 };
Mover m3 = new Mover() { Name = "Lucy", LaborAmount = 20 };
List<Mover> movers = new List<Mover>();
movers.Add(m1);
movers.Add(m2);
movers.Add(m3);
if (movers != null && movers.Count > 0)
{
foreach (Mover m in movers)
{
Thread thread = new Thread(new ParameterizedThreadStart(m.Move));
thread.Start(10);
}
}
}
}
}
通過案例我們也接觸了Thread,下面我們將詳細介紹Thread的功能。
五、Thread
創建并控制線程,設置其優先級并獲取其狀態。
常用方法:
Start()
導致操作系統將當前實例的狀態更改為 ThreadState.Running。
一旦線程處于 ThreadState.Running 狀態,操作系統就可以安排其執行。 線程從方法的第一行(由提供給線程構造函數的 ThreadStart 或 ParameterizedThreadStart 委托表示)開始執行。線程一旦終止,它就無法通過再次調用 Start 來重新啟動。
Thread.Sleep()
調用 Thread.Sleep 方法會導致當前線程立即阻止,阻止時間的長度等于傳遞給 Thread.Sleep 的毫秒數,這樣,就會將其時間片中剩余的部分讓與另一個線程。 一個線程不能針對另一個線程調用 Thread.Sleep。
Interrupt()
中斷處于 WaitSleepJoin 線程狀態的線程。
Suspend和Resume(已過時)
掛起和繼續
在 .NET Framework 2.0 版中,Thread.Suspend 和 Thread.Resume 方法已標記為過時,并將從未來版本中移除。
Abort()
方法用于永久地停止托管線程。一旦線程被中止,它將無法重新啟動。
Join()
阻塞調用線程,直到某個線程終止時為止。
ThreadPriority(優先級)
指定 Thread 的調度優先級。
ThreadPriority 定義一組線程優先級的所有可能值。線程優先級指定一個線程相對于另一個線程的相對優先級。
每個線程都有一個分配的優先級。在運行庫內創建的線程最初被分配 Normal 優先級,而在運行庫外創建的線程在進入運行庫時將保留其先前的優先級。可以通過訪問線程的 Priority 屬性來獲取和設置其優先級。
根據線程的優先級調度線程的執行。用于確定線程執行順序的調度算法隨操作系統的不同而不同。操作系統也可以在用戶界面的焦點在前臺和后臺之間移動時動態地調整線程的優先級。
一個線程的優先級不影響該線程的狀態;該線程的狀態在操作系統可以調度該線程之前必須為 Running。
六、創建線程方式
通過搬運工案例我們能夠了解線程的工作原理,也明白了線程的創建方式。
其實在C#中創建線程有幾種方式,這里給大家舉幾個常用例子,如下:
namespace MutiThreadSample
{
/// <summary>
/// 創建線程的方式
/// </summary>
class CreateThread
{
/// <summary>
/// 不帶參數的委托
/// </summary>
public void CreateThreadWithThreadStart()
{
Thread thread = new Thread(new ThreadStart(ThreadCallBack));
thread.Start();
}
/// <summary>
/// 帶參數的委托
/// </summary>
public void CreateThreadWithParamThreadStart()
{
Thread thread = new Thread(new ParameterizedThreadStart(ThreadCallBackWithParam));
thread.Start();
}
/// <summary>
/// 匿名函數
/// </summary>
public void CreateThreadWithAnonymousFunction()
{
Thread thread = new Thread(delegate()
{
Console.WriteLine("進入子線程1");
for (int i = 1; i < 4; ++i)
{
Thread.Sleep(50);
Console.WriteLine("/t+++++++子線程1+++++++++");
}
Console.WriteLine("退出子線程1");
});
thread.Start();
}
/// <summary>
/// 直接賦值委托
/// </summary>
public void CreateThreadWithCallBack()
{
Thread _hThread = new Thread(ThreadCallBack);
_hThread.Start();
}
/// <summary>
/// 無參數的方法調用
/// </summary>
public void ThreadCallBack()
{
// Do Something
}
/// <summary>
/// 帶參數的方法
/// </summary>
/// <param name="obj"></param>
public void ThreadCallBackWithParam(object obj)
{
// Do Something
}
}
}
時鐘線程
使用 TimerCallback 委托指定希望 Timer 執行的方法。 計時器委托在構造計時器時指定,并且不能更改。 此方法不在創建計時器的線程上執行,而是在系統提供的 ThreadPool 線程上執行。創建計時器時,可以指定在第一次執行方法之前等待的時間量(截止時間)以及此后的執行期間等待的時間量(時間周期)。 可以使用 Change 方法更改這些值或禁用計時器。
class TimerExample
{
static void Main()
{
// Create an event to signal the timeout count threshold in the
// timer callback.
AutoResetEvent autoEvent = new AutoResetEvent(false);
StatusChecker statusChecker = new StatusChecker(10);
// Create an inferred delegate that invokes methods for the timer.
TimerCallback tcb = statusChecker.CheckStatus;
// Create a timer that signals the delegate to invoke
// CheckStatus after one second, and every 1/4 second
// thereafter.
Console.WriteLine("{0} Creating timer./n",
DateTime.Now.ToString("h:mm:ss.fff"));
Timer stateTimer = new Timer(tcb, autoEvent, 1000, 250);
// When autoEvent signals, change the period to every
// 1/2 second.
autoEvent.WaitOne(5000, false);
stateTimer.Change(0, 500);
Console.WriteLine("/nChanging period./n");
// When autoEvent signals the second time, dispose of
// the timer.
autoEvent.WaitOne(5000, false);
stateTimer.Dispose();
Console.WriteLine("/nDestroying timer.");
}
}
class StatusChecker
{
private int invokeCount;
private int maxCount;
public StatusChecker(int count)
{
invokeCount = 0;
maxCount = count;
}
// This method is called by the timer delegate.
public void CheckStatus(Object stateInfo)
{
AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;
Console.WriteLine("{0} Checking status {1,2}.",
DateTime.Now.ToString("h:mm:ss.fff"),
(++invokeCount).ToString());
if(invokeCount == maxCount)
{
// Reset the counter and signal Main.
invokeCount = 0;
autoEvent.Set();
}
}
}
.Net的公用語言運行時(Common Language Runtime,CLR)能區分兩種不同類型的線程:前臺線程和后臺線程。這兩者的區別就是:應用程序必須運行完所有的前臺線程才可以退出;而對于后臺線程,應用程序則可以不考慮其是否已經運行完畢而直接退出,所有的后臺線程在應用程序退出時都會自動結束。
一個線程是前臺線程還是后臺線程可由它的IsBackground屬性來決定。這個屬性是可讀又可寫的。它的默認值為false,即意味著一個線程默認為前臺線程。
我們可以將它的IsBackground屬性設置為true,從而使之成為一個后臺線程。下面的例子是一個控制臺程序,程序一開始便啟動了10個線程,每個線程運行5秒鐘時間。由于線程的IsBackground屬性默認為false,即它們都是前臺線程,所以盡管程序的主線程很快就運行結束了,但程序要到所有已啟動的線程都運行完畢才會結束。示例代碼如下例子中的Test()所示
namespace MutiThreadSample.ThreadType
{
class ThreadTypeTest
{
/// <summary>
/// 測試前臺線程
/// </summary>
public static void Test()
{
for (int i = 0; i < 10; i++)
{
Thread thread = new Thread(new ThreadStart(ThreadFunc));
thread.Start();
}
}
/// <summary>
/// 測試后臺線程
/// </summary>
public static void TestBackgroundThread()
{
for (int i = 0; i < 10; i++)
{
Thread thread = new Thread(new ThreadStart(ThreadFunc));
thread.IsBackground = true;
thread.Start();
}
}
public static void ThreadFunc()
{
Thread.Sleep(0);
DateTime start = DateTime.Now;
while ((DateTime.Now - start).Seconds < 20);//可以停頓的時間長一點,效果更加明顯
}
}
}
接下來我們對上面的代碼進行略微修改,將每個線程的IsBackground屬性都設置為true,則每個線程都是后臺線程了。那么只要程序的主線程結束了,整個程序也就結束了。示例代碼如代碼中的TestBackgroundThread()。
這個例子直接創建一個控制臺程序即可檢驗。
前臺和后臺線程的使用原則
既然前臺線程和后臺線程有這種差別,那么我們怎么知道該如何設置一個線程的IsBackground屬性呢?下面是一些基本的原則:對于一些在后臺運行的線程,當程序結束時這些線程沒有必要繼續運行了,那么這些線程就應該設置為后臺線程。比如一個程序啟動了一個進行大量運算的線程,可是只要程序一旦結束,那個線程就失去了繼續存在的意義,那么那個線程就該是作為后臺線程的。而對于一些服務于用戶界面的線程往往是要設置為前臺線程的,因為即使程序的主線程結束了,其他的用戶界面的線程很可能要繼續存在來顯示相關的信息,所以不能立即終止它們。這里我只是給出了一些原則,具體到實際的運用往往需要編程者的進一步仔細斟酌。
八、總結
這一章主要介紹多線程技術的基本知識。涉及多線程的具體應用,包括預防死鎖、線程同步、線程池等,在今后的文章會涉及到。
新聞熱點
疑難解答