前幾篇文章我已經對異步的操作進行的詳細的解釋.異步操作也是線程的一種,當我們開始一個異步操作(新線程),完成調用後需要和其他線程通信(可能需要告知狀态資訊),這時候我們就需要線程間的通信程式設計.
線程間通信
我們看下面的圖
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIHBlSuEjL18CXn5WY1hWek5WYvwVbvN2Xzd2bsJmbj9CXt92YuM3ZvxmYuNmLzV2Zh1Wavw1LcpDc0RHaiojIsJye.jpg)
圖1
我們來看線程間通信的原理:線程(Thread B)和線程(Thread A)通信, 首先線程A 必須實作同步上下文對象(Synchronization Context), 線程B通過調用線程A的同步上下文對象來通路線程A,所有實作都是在同步上下文中完成的.線程B有兩種方式來實作線程間的通信.
第一種:調用線程A的同步上下文對象,阻礙目前線程,執行紅色箭頭調用,直到黃色箭頭傳回(同步上下文執行完畢)才釋放目前線程. (1->2->3->5)
第二種: 調用線程A的同步上下文對象(實際上是在開啟一個新線程去執行,1->2->3->5) ,執行紅色箭頭,但并不阻礙目前線程(原有線程,1->4->5),綠色箭頭繼續執行.
文章中将會通過下面幾個類來進行介紹:
1.
ISynchronizeInvoke
接口
2.
SynchronizationContext 類
3.
AsyncOperation / AsyncOperationManager 類
附: 同步上下文對象(Synchronization Context)是什麼呢? 當我們通路上下文對象時候(多個對象可以存在于一個上下文中), 是使用代理的方式引用的,而不是直接引用的.這種方式可能是當多個對象通路上下文對象時候,先到達對象先通路,鎖住,執行完畢再解鎖,排隊式通路.
1.
ISynchronizeInvoke
接口
我們先來看下面一段異步的代碼(Window Form控件下有1個Button/1個Label),但點選Button的時候,執行異步調用,完成後,告訴Window Form的 Label控件Text屬性” Asynchronous End”.
Code1.1
1
delegate void DoWork();
2
private void button1_Click( object sender, EventArgs e)
3
{
4
//輔助方法,檢視目前線程
5
Debug.WriteLine(string.Format("Window Form Method.Thread ID:#{0}",
Thread.CurrentThread.ManagedThreadId));
6
//Label lblStatus 屬于主線程的控件[1]
7
this.lblStatus.Text = "Asynchronous Start.";
8
//使用委托來調用異步方法
9
DoWork work = DoWorkMethod;
10
work.BeginInvoke(OnWorkCallback, work);
11
}
12
void OnWorkCallback(IAsyncResult asyncResult)
13
{
14
//輔助方法,檢視目前線程
15
Debug.WriteLine(string.Format("Asynchronous Callback Method.Thread ID:#{0}",
Thread.CurrentThread.ManagedThreadId));
16
DoWork work = asyncResult.AsyncState as DoWork;
17
if (work != null)
18
{
19
work.EndInvoke(asyncResult);
20
}
21
// 報錯:"線程間操作無效: 從不是建立控件“lblStatus”的線程通路它."
22
this.lblStatus.Text = "Asynchronous End"; //上面注釋[1]
23
}
24
25
void DoWorkMethod()
26
{
27
Thread.Sleep(3000);//模拟耗時工作
28
}
運作代碼,我們在第22行報錯(異步方法體内).為什麼呢?我們必須清楚的一點,在windows應用窗體應用程式中,對窗體上控件屬性的任何修改都必須在主線程中完成。不能從其他線程安全地通路控件的方法和屬性。從Debug視窗中我們也可以看出(圖1.1).執行Button Click事件的時候,運作線上程ID =#10; 在異步的方法體内,運作線上程ID=#7.不同線程間不能直接通信.
圖1.1
為了解決這個問題,實作圖1.1 中 #10 和 #7 的通信,下來開始認識
ISynchronizeInvoke
接口(此接口來自.Net Framework 1.0),提供3個方法1個屬性:
BeginInvoke / EndInvoke 方法 : 異步方法
Invoke 方法 : 同步方法
InvokeRequired 屬性 : 判讀來源的執行線程
下面我們看Code1.2的具體代碼來說明(對Code1.1改寫,其中Label 改為ListBox)
Code1.2
1
delegate void DoWork();
2
private void button1_Click( object sender, EventArgs e)
3
{
4
//更新狀态,添加到Listbox 中
5
AddValue("Asynchronous Start.");
6
//使用委托來調用異步方法
7
DoWork work = DoWorkMethod;
8
work.BeginInvoke(OnWorkCallback, work);
9
}
10
11
void OnWorkCallback(IAsyncResult asyncResult)
12
{
13
DoWork work = asyncResult.AsyncState as DoWork;
14
if (work != null)
15
{
16
work.EndInvoke(asyncResult);
17
}
18
//(1)方法:調用Control控件的Invoke
19
//Action<string> asyncUpdateState = UpdateStatus; //Action<string> 介紹=> 附1
20
//Invoke(asyncUpdateState, "1:Asynchronous End.");
21
22
//(2)方法:直接在異步調用的線程下
23
UpdateStatus("2:Asynchronous End.");
24
}
25
26
void UpdateStatus( string input)
27
{
28
//把你需要通知的控件Control 指派給ISynchronizeInvoke
29
//來實作線程間的通信
30
ISynchronizeInvoke async = this.listBoxStatus;
31
//使用(1)方法,InvokeRequired == false ,來源目前(Window Form)主線程
32
if (async.InvokeRequired == false)
33
AddValue(input);
34
else// 使用(2)方法 == true ,來源其他線程(異步)
35
{
36
Action<string> action = new Action<string>(status =>
37
{
38
AddValue(status);
39
});
40
//調用ISynchronizeInvoke 提供的Invoke 同步方法,阻礙線程,直到調用結束
41
//也可以使用ISynchronizeInvoke 提供的異步BeginInvoke/EndInvoke方法來實作調用.
42
async.Invoke(action, new object[]
{ input });
43
}
44
}
45
46
void AddValue( string input)
47
{
48
this.listBoxStatus.Items.Add(string.Format("[(#{2}){0}]Context is null:{1}", input,Thread.CurrentContext==null, Thread.CurrentThread.ManagedThreadId));
49
}
50
void DoWorkMethod()
51
{
52
Thread.Sleep(3000);//模拟耗時工作
53
}
圖1.2
在代碼中(UpdateStatus方法體内),我們可以看到主要是在ISynchronizeInvoke async = this.listBoxStatus;實作了線程間的通信,MSDN的解釋” 實作此接口的對象可以接收事件已發生的通知,并且可以響應有關該事件的查詢”. 并使Window Form(主線程) 下的ListBox 控件和來自異步方法(另外一個線程)的建立了通道. InvokeRequired 判斷線程的來源,如果使用(1)方法,來源于Window Form 自身Control 的Invoke方法, InvokeRequired将傳回false; 來源另外線程(異步)如果使用(2)傳回true.同時ISynchronizeInvoke 提供了異步(BeginInvoke+EndInvok)和同步方法(Invoke)來實作線程間通信.Invoke 就是最上面的圖1 所示的第一種 / BeginInvoke+EndInvok 是第二種.
附1:關于Action<T…> / Func (T…, TResult) (簡單的說就是”簡化後的委托”)的知識可以看MSDN的介紹.
Action<T…>: http://msdn.microsoft.com/zh-cn/library/018hxwa8.aspx
Func (T…, TResult): http://msdn.microsoft.com/zh-cn/library/bb549151.aspx
Code1.2雖然實作了線程間的通信, 回顧圖1的解釋,” 首先線程A 必須實作同步上下文對象(Synchronization Context)”, 而在Code1.2 中并沒有為Window Form(主線程)實作上下文對象,如果沒有這個對象一切都是不成立的.那麼Window Form 做了些什麼呢?
我們來看下面的代碼(使用Console程式):
Code1.3
1
static class Program
2
{
3
static void Main()
4
{
5
//1,在Main 主線程中運作,檢視線程ID和同步上下文
6
Console.WriteLine("0.ThreadID:#{1},Synchronization Context is null?{0}",
7
SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId);
8
9
//2,在Main 主線程中運作,執行個體化空對象Test,檢視線程ID和同步上下文
10
Test a = new Test();
11
Console.WriteLine("1.ThreadID:#{1},Synchronization Context is null?{0}",
12
SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId);
13
14
//3,在Main 主線程中運作,執行個體化FormTest對象(繼承Form),檢視線程ID和同步上下文
15
FormTest test = new FormTest();
16
Console.WriteLine("2.ThreadID:#{1},Synchronization Context is null?{0}",
17
SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId);
18
19
//4,在新線程中運作,檢視線程ID和同步上下文
20
new Thread(work).Start();
21
22
Console.Read();
23
}
24
static void work()
25
{
26
Console.WriteLine("3.ThreadID:#{1},Synchronization Context is null?{0}",
27
SynchronizationContext.Current == null, Thread.CurrentThread.ManagedThreadId);
28
}
29
}
30
public class FormTest : System.Windows.Forms.Form
{ }
31
public class Test
{ }
圖1.3
由代碼和圖可以看出(SynchronizationContext.Current == null 判斷同步上下文對象是否存在), 執行個體化FormTest 對象後(繼承System.Windows.Forms.Form),Form預設的幫我們建立了同步上下文對象,使主線程#9 具備了同步上下文對象,這就是為什麼Code1.2 不用聲明同步上下文對象的原因,同時也告訴我們,開啟一個新線程#10,線程本身是沒有同步上下文對象的.
2.
SynchronizationContext 類
相比ISynchronizeInvoke 接口,SynchronizationContext 類(來自.Net Framework 2.0)提供了更多的方法來操作同步上下文對象,實作線程間通信.在上面的例子中SynchronizationContext類中将由 Post/Send 方法來實作.
反編譯後我們看到:
Code2.1
1
public virtual void Post(SendOrPostCallback d, object state)
2
{
3
ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
4
}
5
6
public virtual void Send(SendOrPostCallback d, object state)
7
{
8
d(state);
9
}
Send = ISynchronizeInvoke 中的Invoke 同步調用.圖1中的第一種
Post = ISynchronizeInvoke 中的BeginInvoke + EndInvoke異步調用. 圖1中的第二種
改寫Code1.2的代碼(還是在WinForm 下程式設計).
Code2.2
1
delegate void DoWork();
2
private void button1_Click( object sender, EventArgs e)
3
{
4
//System.Windows.Forms.Form 自動的建立預設的同步上下文對象,
5
//直接的擷取目前的同步上下文對象
6
SynchronizationContext context = SynchronizationContext.Current;
7
//更新狀态,添加到Listbox 中
8
AddValue<string>("Asynchronous Start.");
9
//使用委托來調用異步方法
10
DoWork work = DoWorkMethod;
11
work.BeginInvoke(OnWorkCallback, context);
12
13
}
14
void OnWorkCallback(IAsyncResult asyncResult)
15
{
16
AsyncResult async = (AsyncResult)asyncResult;
17
DoWork work = (DoWork)async.AsyncDelegate;
18
work.EndInvoke(asyncResult);
19
20
//更新狀态
21
UpdateStatus("Asynchronous End.", asyncResult.AsyncState);
22
}
23
void UpdateStatus( object input, object syncContext)
24
{
25
//擷取主線程(Window Form)中同步上下文對象
26
SynchronizationContext context = syncContext as SynchronizationContext;
27
//使用SynchronizationContext 類中異步Post 方法
28
SendOrPostCallback callback = new SendOrPostCallback(p =>
{
29
AddValue<object>(p);
30
});
31
context.Post(callback, input);//Post 為異步,Send 為同步
32
33
}
34
void AddValue < T > (T input)
35
{
36
this.listBoxStatus.Items.Add(string.Format("[(#{2}){0}]Context is null:{1}", input, Thread.CurrentContext == null, Thread.CurrentThread.ManagedThreadId));
37
}
38
void DoWorkMethod()
39
{
40
Thread.Sleep(3000);//模拟耗時工作
41
}
上面我們已經說過在主線程中System.Windows.Forms.Form 自動的建立預設的同步上下文對象, 這時候我們把目前的同步上下文對象通過參數的形式指派到異步線程中,調用Post 方法來實作, Post 方法接收 SendOrPostCallback 委托和額外object state參數,在Post方法體内調用線程池的線程來實作(Code2.1).當然我們也可以直接使用Send方法.
下面我們看看線程中的代碼(在Console 下程式設計).
Code2.3
1
static class Program
2
{
3
static void Main()
4
{
5
Output("Main Thread Start.");
6
//為主線程建立Synchronization Context
7
var context = new SynchronizationContext();
8
//開始一個新線程
9
Thread threadB = new Thread(work);
10
threadB.Start(context);
11
12
Console.Read();
13
}
14
static void work(object context)
15
{
16
Output("Thread B");
17
18
//擷取主線程中的同步上下文對象
19
SynchronizationContext sc = context as SynchronizationContext;
20
21
//異步的方式和主線程通信,并發送"Hello World".
22
sc.Post(new SendOrPostCallback(p =>
23
{
24
Output(p);
25
}), "Hello World");
26
}
27
static void Output(object value)
28
{
29
Console.WriteLine("[ThreadID:#{0}]{1}", Thread.CurrentThread.ManagedThreadId, value);
30
}
31
}
圖2.3
在主線程中因為沒有同步上下文對象,是以開始我們new SynchronizationContext(); 對象,其他和Code2.2 基本一樣.從圖2.3很好的解釋圖1的第二種調用,也說明了Post 是開啟新線程(線程池)運作的.
3.
AsyncOperation / AsyncOperationManager 類
AsyncOperation / AsyncOperationManager 類是
SynchronizationContext 類的進一步封裝和實作, AsyncOperationManager在建立AsyncOperation對象的時候會取得目前線程的同步上下文對象,并存儲在AsyncOperation之中,使我們通路同步上下文對象更加容易.
Code3.1
1
public class MySynchronizedClass
2
{
3
private AsyncOperation operation;
4
public event EventHandler somethingHappened;
5
public MySynchronizedClass()
6
{
7
//建立AsyncOperation 對象,并把目前線程的同步上下文保持到AsyncOperation中.
8
operation = AsyncOperationManager.CreateOperation(null);
9
10
Thread workerThread = new Thread(new ThreadStart(DoWork));
11
workerThread.Start();
12
}
13
14
private void DoWork()
15
{
16
SendOrPostCallback callback = new SendOrPostCallback(state =>
17
{
18
EventHandler handler = somethingHappened;
19
20
if (handler != null)
21
{
22
handler(this, EventArgs.Empty);
23
}
24
});
25
26
operation.Post(callback, null);
27
//注意1
28
operation.OperationCompleted();
29
}
30
}
代碼很簡單,我也不在解釋,可以參照上面所有代碼. 注意1:
AsyncOperation類中實作了
OperationCompleted的方法. SynchronizationContext 類中這個方法是沒有具體的代碼實作的.
總結:
文章中也非常适合線程的程式設計(除了異步)來實作通信, SynchronizationContext是最重要的一個,其他的擴充類,如SynchronizationContextSwitcher 等更進階的主題,具體可參考下面的連結. 在Winform中有個非常重要的
BackgroundWorker 類,關于
BackgroundWorker的文章很多,在此不做解釋了.
下一篇文章中,我将會讨論Web 上的異步方法.
技術參考:
http://www.codeproject.com/KB/cpp/SyncContextTutorial.aspx
http://www.codeproject.com/KB/threads/SynchronizationContext.aspx
http://www.codeproject.com/KB/threads/SynchronizationContext2.aspx
http://www.codeproject.com/KB/threads/SynchronizationContext3.aspx
http://www.code-magazine.com/Article.aspx?quickid=0403071
以上有word 文檔直接粘貼,排版可能不太好看,你可以通過下面來下載下傳相應的代碼/文檔
1,文檔
2,代碼(VS2008開發,.Net Framework 3.5(C Sharp)編寫)