C#多線程及同步示例簡析
60年代,在OS中能擁有資源和獨立運行的基本單位是進程,然而隨著計算機技術(shù)的發(fā)展,進程出現(xiàn)了很多弊端,一是由于進程是資源擁有者,創(chuàng)建、撤消與切換存在較大的時空開銷,因此需要引入輕型進程;二是由于對稱多處理機(SMP)出現(xiàn),可以滿足多個運行單位,而多個進程并行開銷過大。
因此在80年代,出現(xiàn)了能獨立運行的基本單位——線程(Threads)。
線程,有時被稱為輕量級進程(Lightweight Process,LWP),是程序執(zhí)行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。另外,線程是進程中的一個實體,是被系統(tǒng)獨立調(diào)度和分派的基本單位,線程自己不擁有系統(tǒng)資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。一個線程可以創(chuàng)建和撤消另一個線程,同一進程中的多個線程之間可以并發(fā)執(zhí)行。由于線程之間的相互制約,致使線程在運行中呈現(xiàn)出間斷性。線程也有就緒、阻塞和運行三種基本狀態(tài)。就緒狀態(tài)是指線程具備運行的所有條件,邏輯上可以運行,在等待處理機;運行狀態(tài)是指線程占有處理機正在運行;阻塞狀態(tài)是指線程在等待一個事件(如某個信號量),邏輯上不可執(zhí)行。每一個程序都至少有一個線程,若程序只有一個線程,那就是程序本身。
線程是程序中一個單一的順序控制流程。進程內(nèi)一個相對獨立的、可調(diào)度的執(zhí)行單元,是系統(tǒng)獨立調(diào)度和分派CPU的基本單位指運行中的程序的調(diào)度單位。在單個程序中同時運行多個線程完成不同的工作,稱為多線程。

一、線程簡義
1、進程與線程:進程作為操作系統(tǒng)執(zhí)行程序的基本單位,擁有應(yīng)用程序的資源,進程包含線程,進程的資源被線程共享,線程不擁有資源。
2、前臺線程和后臺線程:通過Thread類新建線程默認為前臺線程。當所有前臺線程關(guān)閉時,所有的后臺線程也會被直接終止,不會拋出異常。
3、掛起(Suspend)和喚醒(Resume):由于線程的執(zhí)行順序和程序的執(zhí)行情況不可預(yù)知,所以使用掛起和喚醒容易發(fā)生死鎖的情況,在實際應(yīng)用中應(yīng)該盡量少用。
4、阻塞線程:Join,阻塞調(diào)用線程,直到該線程終止。
5、終止線程:Abort:拋出 ThreadAbortException 異常讓線程終止,終止后的線程不可喚醒。Interrupt:拋出 ThreadInterruptException 異常讓線程終止,通過捕獲異常可以繼續(xù)執(zhí)行。
6、線程優(yōu)先級:AboveNormal BelowNormal Highest Lowest Normal,默認為Normal。
二、線程的使用
線程函數(shù)通過委托傳遞,可以不帶參數(shù),也可以帶參數(shù)(只能有一個參數(shù)),可以用一個類或結(jié)構(gòu)體封裝參數(shù)。
namespace Test
{
class Program
{
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(TestMethod));
Thread t2 = new Thread(new ParameterizedThreadStart(TestMethod));
t1.IsBackground = true;
t2.IsBackground = true;
t1.Start();
t2.Start("hello");
Console.ReadKey();
}
public static void TestMethod()
{
Console.WriteLine("不帶參數(shù)的線程函數(shù)");
}
public static void TestMethod(object data)
{
string datastr = data as string;
Console.WriteLine("帶參數(shù)的線程函數(shù),參數(shù)為:{0}", datastr);
}
}
}
三、線程池
由于線程的創(chuàng)建和銷毀需要耗費一定的開銷,過多的使用線程會造成內(nèi)存資源的浪費,出于對性能的考慮,于是引入了線程池的概念。線程池維護一個請求隊列,線程池的代碼從隊列提取任務(wù),然后委派給線程池的一個線程執(zhí)行,線程執(zhí)行完不會被立即銷毀,這樣既可以在后臺執(zhí)行任務(wù),又可以減少線程創(chuàng)建和銷毀所帶來的開銷。
線程池線程默認為后臺線程(IsBackground)。
class Program
{
static void Main(string[] args)
{
//將工作項加入到線程池隊列中,這里可以傳遞一個線程參數(shù)
ThreadPool.QueueUserWorkItem(TestMethod, "Hello");
Console.ReadKey();
}
public static void TestMethod(object data)
{
string datastr = data as string;
Console.WriteLine(datastr);
}
}
四、Task類
使用ThreadPool的QueueUserWorkItem()方法發(fā)起一次異步的線程執(zhí)行很簡單,但是該方法最大的問題是沒有一個內(nèi)建的機制讓你知道操作什么時候完成,有沒有一個內(nèi)建的機制在操作完成后獲得一個返回值。為此,可以使用System.Threading.Tasks中的Task類。
構(gòu)造一個Task<TResult>對象,并為泛型TResult參數(shù)傳遞一個操作的返回類型。
class Program
{
static void Main(string[] args)
{
Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000);
t.Start();
t.Wait();
Console.WriteLine(t.Result);
Console.ReadKey();
}
private static Int32 Sum(Int32 n)
{
Int32 sum = 0;
for (; n > 0; --n)
checked{ sum += n;} //結(jié)果太大,拋出異常
return sum;
}
}
一個任務(wù)完成時,自動啟動一個新任務(wù)。
一個任務(wù)完成后,它可以啟動另一個任務(wù),下面重寫了前面的代碼,不阻塞任何線程。
class Program
{
static void Main(string[] args)
{
Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000);
t.Start();
//t.Wait();
Task cwt = t.ContinueWith(task => Console.WriteLine("The result is {0}",t.Result));
Console.ReadKey();
}
private static Int32 Sum(Int32 n)
{
Int32 sum = 0;
for (; n > 0; --n)
checked{ sum += n;} //結(jié)果溢出,拋出異常
return sum;
}
}
五、委托異步執(zhí)行
委托的異步調(diào)用:BeginInvoke() 和 EndInvoke()
public delegate string MyDelegate(object data);
class Program
{
static void Main(string[] args)
{
MyDelegate mydelegate = new MyDelegate(TestMethod);
IAsyncResult result = mydelegate.BeginInvoke("Thread Param", TestCallback, "Callback Param");
//異步執(zhí)行完成
string resultstr = mydelegate.EndInvoke(result);
}
//線程函數(shù)
public static string TestMethod(object data)
{
string datastr = data as string;
return datastr;
}
//異步回調(diào)函數(shù)
public static void TestCallback(IAsyncResult data)
{
Console.WriteLine(data.AsyncState);
}
}
六、線程同步
1)原子操作(Interlocked):幫助保護免受計劃程序切換上下文時某個線程正在更新可以由其他線程訪問的變量或者在單獨的處理器上同時執(zhí)行兩個線程就可能出現(xiàn)的錯誤。 此類的成員不會引發(fā)異常。
class Program
{
static int counter = 1;
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(F1));
Thread t2 = new Thread(new ThreadStart(F2));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
System.Console.ReadKey();
}
static void F1()
{
for (int i = 0; i < 5; i++)
{
Interlocked.Increment(ref counter);
System.Console.WriteLine("Counter++ {0}", counter);
Thread.Sleep(10);
}
}
static void F2()
{
for (int i = 0; i < 5; i++)
{
Interlocked.Decrement(ref counter);
System.Console.WriteLine("Counter-- {0}", counter);
Thread.Sleep(10);
}
}
}
2)lock()語句:避免鎖定public類型,否則實例將超出代碼控制的范圍,定義private對象來鎖定。而自定義類推薦用私有的只讀靜態(tài)對象,比如:private static readonly object obj = new object();為什么要設(shè)置成只讀的呢?這時因為如果在lock代碼段中改變obj的值,其它線程就暢通無阻了,因為互斥鎖的對象變了,object.ReferenceEquals必然返回false。Array 類型提供 SyncRoot。許多集合類型也提供 SyncRoot。
3)Monitor實現(xiàn)線程同步
通過Monitor.Enter() 和 Monitor.Exit()實現(xiàn)排它鎖的獲取和釋放,獲取之后獨占資源,不允許其他線程訪問。
還有一個TryEnter方法,請求不到資源時不會阻塞等待,可以設(shè)置超時時間,獲取不到直接返回false。
public void MonitorSomeThing()
{
try
{
Monitor.Enter(obj);
dosomething();
}
catch(Exception ex)
{
}
finally
{
Monitor.Exit(obj);
}
}
4)ReaderWriterLock
當對資源操作讀多寫少的時候,為了提高資源的利用率,讓讀操作鎖為共享鎖,多個線程可以并發(fā)讀取資源,而寫操作為獨占鎖,只允許一個線程操作。
class SynchronizedCache
{
private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary<int, string> innerCache = new Dictionary<int, string>();
public string Read(int key)
{
cacheLock.EnterReadLock();
try
{
return innerCache[key];
}
finally
{
cacheLock.ExitReaderLock();
}
}
public void Add(int key, string value)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public bool AddWithTimeout(int key, string value, int timeout)
{
if (cacheLock.TryEnterWriteLock(timeout))
{
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitReaderLock();
}
return true;
}
else
{
return false;
}
}
public AddOrUpdateStatus AddOrUpdate(int key, string value)
{
cacheLock.EnterUpgradeableReadLock();
try
{
string result = null;
if (innerCache.TryGetValue(key, out result))
{
if (result == value)
{
return AddOrUpdateStatus.Unchanged;
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache[key] = value;
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Updated;
}
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Added;
}
}
finally
{
cacheLock.ExitUpgradeableReadLock();
}
}
public void Delete(int key)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Remove(key);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public enum AddOrUpdateStatus
{
Added,
Updated,
Unchanged
};
}
5)事件(Event)類實現(xiàn)同步
事件類有兩種狀態(tài),終止狀態(tài)和非終止狀態(tài),終止狀態(tài)時調(diào)用WaitOne可以請求成功,通過Set將時間狀態(tài)設(shè)置為終止狀態(tài)。
1).AutoResetEvent(自動重置事件)
2).ManualResetEvent(手動重置事件)
AutoResetEvent和ManualResetEvent這兩個類經(jīng)常用到, 他們的用法很類似,但也有區(qū)別。Set方法將信號置為發(fā)送狀態(tài),Reset方法將信號置為不發(fā)送狀態(tài),WaitOne等待信號的發(fā)送??梢酝ㄟ^構(gòu)造函數(shù)的參數(shù)值來決定其初始狀態(tài),若為true則非阻塞狀態(tài),為false為阻塞狀態(tài)。如果某個線程調(diào)用WaitOne方法,則當信號處于發(fā)送狀態(tài)時,該線程會得到信號, 繼續(xù)向下執(zhí)行。其區(qū)別就在調(diào)用后,AutoResetEvent.WaitOne()每次只允許一個線程進入,當某個線程得到信號后,AutoResetEvent會自動又將信號置為不發(fā)送狀態(tài),則其他調(diào)用WaitOne的線程只有繼續(xù)等待.也就是說,AutoResetEvent一次只喚醒一個線程;而ManualResetEvent則可以喚醒多個線程,因為當某個線程調(diào)用了ManualResetEvent.Set()方法后,其他調(diào)用WaitOne的線程獲得信號得以繼續(xù)執(zhí)行,而ManualResetEvent不會自動將信號置為不發(fā)送。也就是說,除非手工調(diào)用了ManualResetEvent.Reset()方法,則ManualResetEvent將一直保持有信號狀態(tài),ManualResetEvent也就可以同時喚醒多個線程繼續(xù)執(zhí)行。
6)信號量(Semaphore)
信號量是由內(nèi)核對象維護的int變量,為0時,線程阻塞,大于0時解除阻塞,當一個信號量上的等待線程解除阻塞后,信號量計數(shù)+1。
線程通過WaitOne將信號量減1,通過Release將信號量加1,使用很簡單。
public Thread thrd;
//創(chuàng)建一個可授權(quán)2個許可證的信號量,且初始值為2
static Semaphore sem = new Semaphore(2, 2);
public mythread(string name)
{
thrd = new Thread(this.run);
thrd.Name = name;
thrd.Start();
}
void run()
{
Console.WriteLine(thrd.Name + "正在等待一個許可證……");
//申請一個許可證
sem.WaitOne();
Console.WriteLine(thrd.Name + "申請到許可證……");
for (int i = 0; i < 4 ; i++)
{
Console.WriteLine(thrd.Name + ": " + i);
Thread.Sleep(1000);
}
Console.WriteLine(thrd.Name + " 釋放許可證……");
//釋放
sem.Release();
}
}
class mysemaphore
{
public static void Main()
{
mythread mythrd1 = new mythread("Thrd #1");
mythread mythrd2 = new mythread("Thrd #2");
mythread mythrd3 = new mythread("Thrd #3");
mythread mythrd4 = new mythread("Thrd #4");
mythrd1.thrd.Join();
mythrd2.thrd.Join();
mythrd3.thrd.Join();
mythrd4.thrd.Join();
}
}
7)互斥體(Mutex)
獨占資源,可以把Mutex看作一個出租車,乘客看作線程。乘客首先等車,然后上車,最后下車。當一個乘客在車上時,其他乘客就只有等他下車以后才可以上車。而線程與C# Mutex對象的關(guān)系也正是如此,線程使用Mutex.WaitOne()方法等待C# Mutex對象被釋放,如果它等待的C# Mutex對象被釋放了,它就自動擁有這個對象,直到它調(diào)用Mutex.ReleaseMutex()方法釋放這個對象,而在此期間,其他想要獲取這個C# Mutex對象的線程都只有等待。
class Test
{
/// <summary>
/// 應(yīng)用程序的主入口點。
/// </summary>
[STAThread]
static void Main(string[] args)
{
bool flag = false;
System.Threading.Mutex mutex = new System.Threading.Mutex(true, "Test", out flag);
//第一個參數(shù):true--給調(diào)用線程賦予互斥體的初始所屬權(quán)
//第一個參數(shù):互斥體的名稱
//第三個參數(shù):返回值,如果調(diào)用線程已被授予互斥體的初始所屬權(quán),則返回true
if (flag)
{
Console.Write("Running");
}
else
{
Console.Write("Another is Running");
System.Threading.Thread.Sleep(5000);//線程掛起5秒鐘
Environment.Exit(1);//退出程序
}
Console.ReadLine();
}
}
8)跨進程間的同步
通過設(shè)置同步對象的名稱就可以實現(xiàn)系統(tǒng)級的同步,不同應(yīng)用程序通過同步對象的名稱識別不同同步對象。
static void Main(string[] args)
{
string MutexName = "InterProcessSyncName";
Mutex SyncNamed; //聲明一個已命名的互斥對象
try
{
SyncNamed = Mutex.OpenExisting(MutexName); //如果此命名互斥對象已存在則請求打開
}
catch (WaitHandleCannotBeOpenedException)
{
SyncNamed = new Mutex(false, MutexName); //如果初次運行沒有已命名的互斥對象則創(chuàng)建一個
}
Task MulTesk = new Task
(
() => //多任務(wù)并行計算中的匿名方法,用委托也可以
{
for (; ; ) //為了效果明顯而設(shè)計
{
Console.WriteLine("當前進程等待獲取互斥訪問權(quán)......");
SyncNamed.WaitOne();
Console.WriteLine("獲取互斥訪問權(quán),訪問資源完畢,按回車釋放互斥資料訪問權(quán).");
Console.ReadLine();
SyncNamed.ReleaseMutex();
Console.WriteLine("已釋放互斥訪問權(quán)。");
}
}
);
MulTesk.Start();
MulTesk.Wait();
}
9)分布式的同步
可以使用redis任務(wù)隊列或者redis相關(guān)特性
Parallel.For(0, 1000000, i =>
{
Stopwatch sw1 = new Stopwatch();
sw1.Start();
if (redisHelper.GetRedisOperation().Lock(key))
{
var tt = int.Parse(redisHelper.GetRedisOperation().StringGet("calc"));
tt++;
redisHelper.GetRedisOperation().StringSet("calc", tt.ToString());
redisHelper.GetRedisOperation().UnLock(key);
}
var v = sw1.ElapsedMilliseconds;
if (v >= 10 * 1000)
{
Console.Write("f");
}
sw1.Stop();
});
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
深入分析C#中WinForm控件之Dock順序調(diào)整的詳解
本篇文章是對C#中WinForm控件之Dock順序調(diào)整進行了詳細的分析介紹,需要的朋友參考下2013-05-05
超簡單C#獲取帶漢字的字符串真實長度(單個英文長度為1,單個中文長度為2)
正常情況下,我們是直接去string的length的,但是漢字是有兩個字節(jié)的,所以直接用length是錯的2018-03-03

