目錄
什麼是 .NET 同步環境?
WCF 和同步環境
服務自定義 同步環境
線程關聯同步環境
優先級處理
回調和同步環境
為什麼要使用同步環境?
Windows® Communication Foundation (WCF) 的一個比較有用的功能是它依賴 Microsoft® .NET Framework 同步環境來封送對服務實例(或回調對象)的調用。此機制同時為面 向生產力的開發和強大的可擴展性提供支持。在本專欄中,我將簡要說明什麼是 同步環境,以及 WCF 如何使用它們,然後會演示可用於擴展 WCF 的各個選項, 從而以編程的方式或聲明的方式使用自定義的同步環境。除了解自定義同步環境 的優點之外,您還將看到一些高級的 .NET 編程,以及 WCF 可擴展性技術。
什麼是 .NET 同步環境?
.NET Framework 2.0 引入了一個鮮為 人知的功能,稱為同步環境,它由 System.Threading 命名空間中的 SynchronizationContext 類定義:
public delegate void SendOrPostCallback(object state);
public class SynchronizationContext
{
public virtual void Post (SendOrPostCallback callback,object state);
public virtual void Send(SendOrPostCallback callback,object state);
public static void SetSynchronizationContext
(SynchronizationContext context);
public static SynchronizationContext Current
{get;}
//More members
}
同步環境存儲在線程本 地存儲 (TLS) 中。使用 .NET Framework 2.0 創建的每個線程都可以有一個可 通過 SynchronizationContext 的靜態 Current 屬性獲得的同步環境(與環境 事務類似)。如果當前的線程沒有同步環境,則 Current 可能會返回空值。同 步環境可用來在調用線程和目標線程之間傳遞方法調用(萬一該方法無法在原來 的調用線程上執行)。調用線程會封裝其准備封送到另一個(些)線程的方法( 通過 SendOrPostCallback 類型的委托),然後分別針對同步或異步執行,將其 提供給 Send 或 Post 方法。通過調用靜態方法 SetSynchronizationContext 可將同步環境與當前的線程關聯起來。
迄今為止,最常見的同步環境用 法與 UI 更新有關。在所有的多線程 Windows 技術中(從 MFC 到 Windows Forms,再到 WPF),只有創建了窗口的線程才可以通過處理其消息來更新它。 這個限制與 Windows 消息循環和線程消息傳遞體系結構的基礎用法有關。在開 發多線程 UI 應用程序時,消息傳遞體系結構會產生一個問題 — 您會希 望在執行耗時較長的操作或接收回調時避免阻止 UI。當然,這樣就必須使用工 作線程,然而那些線程卻無法直接更新 UI,因為它們不是 UI 線程。
在 .NET Framework 2.0 中,為了解決這個問題,任何基於 Windows Forms 的控件 或窗體的構造函數都會查看它正在其上運行的線程是否有同步環境,如果沒有, 該構造函數就會附加一個新同步環境(稱為 WindowsFormsSynchronizationContext)。這個專用的同步環境可以將所有對其 Post 或 Send 方法的調用轉換成 Windows 消息,並將它們發布給要在正確的線 程上處理的 UI 線程消息隊列。Windows Forms 同步環境是常用的 BackgroundWorker 幫助器控件背後的基礎技術。
WCF 和同步環境
默認情況下,所有的 WCF 服務調用(以及回調)都會在 I/O Completion 線程池中的線程上執行。該線程池在默認情況下有 1,000 個線程, 但是沒有一個處於您應用程序的控制之下。現在假設有一個需要更新某些用戶界 面的服務。該服務因為所處的線程不正確而不能直接訪問窗體、控件或窗口。為 了解決這個問題,ServiceBehaviorAttribute 提供了 UseSynchronizationContext 屬性,定義如下:
[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : ...
{
public bool UseSynchronizationContext
{get;set;}
//More members
}
UseSynchronizationContext 的默認值設為 true。為了確定 該服務應使用哪個同步環境,WCF 會查看打開了主機的線程。如果該線程有同步 環境,並且 UseSynchronizationContext 設為 true,那麼 WCF 會自動將所有 對該服務的調用封送到這個同步環境。開發人員不需要與同步環境進行顯式交互 。WCF 實現這一點的方式是為每個終結點的調度程序提供對同步環境的引用,然 後調度程序使用該同步環境來調度所有的調用。
例如,假設服務 MyService(在圖 1 中定義)需要更新窗體 MyForm 上的一些 UI。由於主機是 在構建窗體之後打開的,因此打開的線程已經有一個同步環境,這樣對該服務的 所有調用都會被自動封送到正確的 UI 線程。
Figure1使用 UI 同步環境
[ServiceContract]
interface IMyContract
{...}
class MyService : IMyContract
{
/* some code to update MyForm */
}
class MyForm : Form
{...}
static class Program
{
static void Main()
{
Form form = new MyForm ();//Sync context established here
ServiceHost host = new ServiceHost(typeof(MyService));
host.Open();
Application.Run(form);
host.Close();
}
}
如果您只想在 Main 中打開主機並使用 Windows Forms 設計器 生成的代碼,那麼就無法從同步環境中受益,因為主機是在它不存在的情況下打 開的:
//No automatic use of synchronization context
static void Main()
{
ServiceHost host = new ServiceHost(typeof(MyService));
host.Open();
Application.Run(new MyForm());//Synchronization context
//established here
host.Close();
}
服務自定義同步環境
服務或資源使用自定義同步環境 需要完成兩個方面的工作:實現同步環境,然後安裝它。本專欄隨附的源代碼包 含我的 ThreadPoolSynchronizer 類,定義如下:
public class ThreadPoolSynchronizer : SynchronizationContext,IDisposable
{
public ThreadPoolSynchronizer(uint poolSize);
public ThreadPoolSynchronizer(uint poolSize,string poolName);
public void Dispose();
public void Close();
public void Abort();
}
ThreadPoolSynchronizer 會將所有調用封送到 一個自定義的線程池中,在該線程池中,這些調用先排入隊列,然後在可用線程 上多路復用。池的大小由構造參數提供。如果達到了池的極限,調用會在隊列中 掛起,直到線程可用為止。您還可以提供一個池名稱(將成為池中線程名稱的前 綴)。釋放或關閉 ThreadPoolSynchronizer 會正常終止池中的所有線程,也就 是在允許使用中的線程完成其任務之後。Abort 方法會引起不太正常的關閉,因 為它是突然終止所有的線程。實現 ThreadPoolSynchronizer 與 WCF 無關,因 此我選擇不將該代碼包括在本專欄中。自定義線程池的典型用法與服務器應用程 序有關,該應用程序需要通過控制基礎工作線程及其任務來最大化其吞吐量。
若要將服務和自定義線程池關聯起來,可以手動將 ThreadPoolSynchronizer 附加到打開主機的線程:
SynchronizationContext syncContext = new ThreadPoolSynchronizer(3);
SynchronizationContext.SetSynchronizationContext(syncContext);
using(syncContext as IDisposable)
{
ServiceHost host = new ServiceHost(typeof(MyService));
host.Open();
/* Some blocking operations */
host.Close();
}
該線程池將有三個線程。通過使用和圖 1 中相同的服務定義,MyService 將與 這些線程關聯。對該服務的所有調用都將被傳送給這些線程(與服務並發模式或 實例化模式無關),並且跨越該服務支持的所有終結點和約定。
上述代 碼的問題在於該服務處於宿主代碼的控制之下。如果設計要求該服務在池上執行 怎麼辦?更為合適的辦法是以聲明的方式來應用線程池,並作為服務定義的一部 分。為此,我編寫了 ThreadPoolBehaviorAttribute:
[AttributeUsage(AttributeTargets.Class)]
public class ThreadPoolBehaviorAttribute : Attribute,
IContractBehavior,
IServiceBehavior
{
public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType);
public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType,
string poolName);
}
直接對該服務應用此屬性,同時提供服務類型作為構造函數參數 :
[ThreadPoolBehavior(3,typeof(MyService))]
class MyService : IMyContract
{...}
該屬性向服務終結點的調 度程序提供了一個 ThreadPoolSynchronizer 實例。實現 ThreadPoolBehaviorAttribute 的關鍵就在於了解如何將調度程序與同步環境關 聯起來以及何時關聯。為幫助實現這一點,ThreadPoolBehaviorAttribute 支持 特殊的 WCF 可擴展接口 IContractBehavior:
public interface IContractBehavior
{
void ApplyDispatchBehavior (ContractDescription description,
ServiceEndpoint endpoint,
DispatchRuntime dispatch);
// More members
}
當服務帶有支持 IContractBehavior 的屬性時,在打開主機之 後(但在將調用轉發給服務之前),WCF 會為每個服務終結點調用 ApplyDispatchBehavior 方法,並為它提供 DispatchRuntime 參數,使其能夠 影響調度程序。圖 2 列出了 ThreadPoolBehaviorAttribute 的實現。
Figure2實現 ThreadPoolBehaviorAttribute
[AttributeUsage(AttributeTargets.Class)]
public class ThreadPoolBehaviorAttribute : Attribute,IContractBehavior,
IServiceBehavior
{
//Store values in matching members
protected string PoolName
{get{...};set{...};}
protected uint PoolSize
{get{...};set{...};}
protected Type ServiceType
{get{...};set{...};}
public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType)
: this(poolSize,serviceType,null)
{}
public ThreadPoolBehaviorAttribute(uint poolSize,Type serviceType,
string poolName)
{
PoolName = poolName;
ServiceType = serviceType;
PoolSize = poolSize;
}
protected virtual ThreadPoolSynchronizer ProvideSynchronizer()
{
if (ThreadPoolHelper.HasSynchronizer(ServiceType) == false)
return new ThreadPoolSynchronizer(PoolSize,PoolName);
else
return ThreadPoolHelper.GetSynchronizer (ServiceType);
}
void IContractBehavior.ApplyDispatchBehavior
(ContractDescription description,ServiceEndpoint endpoint,
DispatchRuntime dispatch)
{
PoolName = PoolName ?? "Pool executing endpoints of " + ServiceType;
ThreadPoolHelper.ApplyDispatchBehavior(ProvideSynchronizer(),
PoolSize,ServiceType,PoolName,dispatchRuntime);
}
void IServiceBehavior.Validate(ServiceDescription description,
ServiceHostBase serviceHostBase)
{
serviceHostBase.Closed += delegate
{
ThreadPoolHelper.CloseThreads(ServiceType);
};
}
//Rest of the implementation
}
public static class ThreadPoolHelper
{
static Dictionary<Type,ThreadPoolSynchronizer> m_Synchronizers =
new Dictionary<Type,ThreadPoolSynchronizer>();
[MethodImpl(MethodImplOptions.Synchronized)]
internal static bool HasSynchronizer(Type type)
{
return m_Synchronizers.ContainsKey(type);
}
[MethodImpl (MethodImplOptions.Synchronized)]
internal static ThreadPoolSynchronizer GetSynchronizer(Type type)
{
return m_Synchronizers[type];
}
[MethodImpl (MethodImplOptions.Synchronized)]
internal static void ApplyDispatchBehavior(
ThreadPoolSynchronizer synchronizer,uint poolSize,Type type,
string poolName,DispatchRuntime dispatch)
{
if(HasSynchronizer(type) == false)
{
m_Synchronizers[type] = synchronizer;
}
dispatch.SynchronizationContext = m_Synchronizers [type];
}
[MethodImpl (MethodImplOptions.Synchronized)]
public static void CloseThreads(Type type)
{
if (m_Synchronizers.ContainsKey(type))
{
m_Synchronizers[type].Dispose();
m_Synchronizers.Remove (type);
}
}
}
將 WCF 自定義行為屬 性的實現與實際行為分離開來是最佳的做法。只讓該屬性決定事件的順序,而讓 幫助器類提供實際行為。這樣做可單獨使用行為,比如由自定義主機使用。這就 是 ThreadPoolBehaviorAttribute 沒有做很多事情的原因 — 它將它大部 分的工作委托給了稱為 ThreadPoolHelper 的靜態幫助器類。ThreadPoolHelper 類提供了 HasSynchronizer 方法和 GetSynchronizer 方法,前者會告訴我們指 定的服務類型是否已經有同步環境,後者會返回與該類型關聯的同步環境。 ThreadPoolBehaviorAttribute 會在虛擬的 ProvideSynchronizer 方法中使用 這兩種方法,以確保它為該服務類型正好創建一次池。這項檢查是必需的,因為 ApplyDispatchBehavior 可以被調用多次 — 實際上是每個終結點一次。 ThreadPoolBehaviorAttribute 也是一個自定義的服務行為,因為它實現了 IServiceBehavior:
public interface IServiceBehavior
{
void Validate(ServiceDescription description,ServiceHostBase host);
//More members
}
IServiceBehavior 的 Validate 方法使用服務主機實例來訂閱 主機的 Closed 事件,在此它要求 ThreadPoolHelper 終止池中的所有線程。
ThreadPoolHelper 所起的作用是將該服務類型的所有終結點的所有調度 程序與 ThreadPoolSynchronizer 的同一實例關聯起來。這可以確保所有的調用 都被路由到同一個池中。ThreadPoolHelper 必須能夠將服務類型映射到特定的 ThreadPoolSynchronizer,因此它聲明了稱為 m_Synchronizers 的靜態字典, 該字典將服務類型用作鍵,將 ThreadPoolSynchronizer 實例用作值。
在 ApplyDispatchBehavior 中,ThreadPoolHelper 會查看 m_Synchronizers 是否已包含提供的服務類型。如果找不到類型,ThreadPoolHelper 會將提供的 ThreadPoolSynchronizer 添加到 m_Synchronizers,從而將其與服務類型關聯 起來。隨後,它將 ThreadPoolSynchronizer 實例分配給調度程序:
dispatch.SynchronizationContext = m_Synchronizers [type];
這一行代碼即可完全讓 WCF 從現在開始使用自定義同步 環境。在 CloseThreads 方法中,ThreadPoolHelper 會從字典中查找 ThreadPoolSynchronizer 實例並釋放它(從而正常地終止池中的所有工作線程 )。對字典的所有訪問都會通過 MethodImpl 屬性和 MethodImplOptions.Synchronized 標志進行自動同步。ThreadPoolHelper 還會 進行驗證以確保所提供池的大小不會超過調度程序中止值的最大並發調用數值( 圖 2 中未顯示)。
線程關聯同步環境
大小為 1 的池實際上會創 建特定線程和所有服務調用之間的關聯,而不管服務並發和實例化模式如何。如 果需要該服務創建一些 UI(例如彈出窗口),並定期顯示、隱藏和更新它,這 會特別有用。作為創建窗口的一方,該服務必須對其在同一線程上被調用這一事 實進行強制實施。線程關聯的另一個應用是一種服務,它訪問或創建使用 TLS 的資源。為了正式表達這些要求,我創建了特殊化的 AffinitySynchronizer, 如下所示:
public class AffinitySynchronizer : ThreadPoolSynchronizer
{
public AffinitySynchronizer() :
this("AffinitySynchronizer Worker Thread")
{}
public AffinitySynchronizer(string threadName): base(1,threadName)
{}
}
雖然可以像在之前引用 的代碼中一樣安裝 AffinitySynchronizer,我也定義了 ThreadAffinityBehaviorAttribute,如圖 3 所示。此屬性的用法如下:
Figure3ThreadAffinityBehaviorAttribute
[AttributeUsage (AttributeTargets.Class)]
public class ThreadAffinityBehaviorAttribute :
ThreadPoolBehaviorAttribute
{
public ThreadAffinityBehaviorAttribute(Type serviceType)
: this(serviceType,"Affinity Worker Thread")
{}
public ThreadAffinityBehaviorAttribute(
Type serviceType,string threadName):base(1,serviceType,
threadName)
{}
}
[ThreadAffinityBehavior(typeof(MyService))]
class MyService : IMyContract
{...}
當依賴線程關聯時,所有 的服務實例始終是線程安全的,因為只有單一(而且相同的)線程可以訪問它們 。使用 ConcurrencyMode .Multiple 配置服務相當於創建單線程訪問。另請注 意,使用 ConcurrencyMode.Reentrant 進行配置實際上不會為重新進入做准備 ,因為當單個線程在調用時被阻止就不可能發生重新進入。在依賴線程關聯時, 最好選擇 ConcurrencyMode.Single 的默認值。
優先級處理
默認 情況下,對 WCF 服務的所有調用都將按到達順序得到處理。在使用 I/O Completion Ports 線程池或自定義線程池的情況下均是如此。通常,這就是您 真正需要的。但是,如果某些調用具有更高的優先級,而且您希望它們一到達就 得到處理,而不是按順序來處理它們,那怎麼辦呢?再糟一點,如果服務上的負 載是如此之大以致於基礎服務資源已耗盡,那又怎麼辦呢?如果中止值達到了最 大極限呢?在這些情況下,高優先級調用會和其他調用一樣排隊,等候服務或其 資源變為可用。同步環境為此問題提供了一個妥善的解決方案:為調用分配優先 級,讓同步環境在調用到達時對其排序,然後將它們調度給線程池以供執行。這 就是我的 PrioritySynchronizer 類(在圖 4 中定義)所完成的工作。
Figure4PrioritySynchronizer
public enum CallPriority
{
Low,
Normal,
High
}
public class PrioritySynchronizer : ThreadPoolSynchronizer
{
public PrioritySynchronizer(uint poolSize);
public PrioritySynchronizer(uint poolSize,string poolName);
public static CallPriority Priority
{get;set;}
}
PrioritySynchronizer 派生自 ThreadPoolSynchronizer,並且 增加了我剛剛提到的排序功能。由於 SynchronizationContext 的 Send 和 Post 方法不接受優先級參數,因此 PrioritySynchronizer 的客戶端有兩種傳 遞調用優先級的方法:第一種方法是通過 Priority 屬性,該方法可將優先級( 枚舉類型 CallPriority 的值)存儲在調用線程的 TLS 中。第二個選擇是通過 消息頭,我會在本專欄稍後部分中講到。如果不指定,Priority 默認為 CallPriority.Normal。
我還提供了匹配的 PriorityCallsBehaviorAttribute,如圖 5 所示。使用 PriorityCallsBehaviorAttribute 非常簡單,如下所示:
Figure5實現 PriorityCallsBehaviorAttribute
[AttributeUsage(AttributeTargets.Class)]
public class PriorityCallsBehaviorAttribute : ThreadPoolBehaviorAttribute
{
public PriorityCallsBehaviorAttribute(uint poolSize,Type serviceType)
: this(poolSize,serviceType,null)
{}
public PriorityCallsBehaviorAttribute(uint poolSize,Type serviceType,
string poolName) : base (poolSize,serviceType,poolName)
{}
protected override ThreadPoolSynchronizer ProvideSynchronizer()
{
if (ThreadPoolHelper.HasSynchronizer(ServiceType) == false)
return new PrioritySynchronizer(PoolSize,PoolName);
else
return ThreadPoolHelper.GetSynchronizer(ServiceType);
}
}
[PriorityCallsBehavior(3,typeof(MyService))]
class MyService : IMyContract
{...}
PriorityCallsBehaviorAttribute 會覆蓋 ProvideSynchronizer,並提供 PrioritySynchronizer 而不是 ThreadPoolSynchronizer。由於 PrioritySynchronizer 派生自 ThreadPoolSynchronizer,所以就 ThreadPoolHelper 來說,這是透明的。
實現和支持優先級處理的真正挑戰在於提供從客戶端到服務,最終到 PrioritySynchronizer 的調用優先級。使用 PrioritySynchronizer 的 Priority 屬性僅對直接與同步環境交互的非 WCF 客戶端有用;它不適用於 WCF 客戶端,WCF 客戶端的線程從不用來訪問該服務。雖然您可以提供優先級作為每 個方法中的顯式參數,但我還是想要可應用於任何約定和服務的通用機制。為此 ,您必須通過消息頭帶外傳遞調用的優先級。我在 2007 年 2 月名為“構 建排隊 WCF 響應服務”的“基礎”專欄(請參閱 msdn.microsoft.com/msdnmag/issues/07/02/Foundations)中遇到了類似的問 題 — 將帶外響應服務的地址傳遞給該服務 — 我詳細闡述了傳入和 傳出標頭的用法。雖然在這裡我完全可以使用同一技術,但這會是一個重復的工 作。因此,最好是擴展一下該專欄中所介紹的技術,並在 WCF 中增加對外來信 息(從客戶端到服務)的通用管理,實際上也就是一種通用但類型安全,並且特 定於應用程序的自定義環境。這就是 GenericContext<T> 類所提供的功 能。圖 6 顯示了已刪除了錯誤處理的實現。
Figure6GenericContext<T> 類
[DataContract]
public class GenericContext<T>
{
[DataMember]
public readonly T Value;
public GenericContext(T value)
{
Value = value;
}
public GenericContext() : this(default(T))
{}
public static GenericContext<T> Current
{
get
{
OperationContext context = OperationContext.Current;
if(context == null)
return null;
return context.IncomingMessageHeaders.GetHeader<GenericContext<T>> ;
typeof(T).ToString(), typeof(T).Namespace);
}
set
{
OperationContext context = OperationContext.Current;
MessageHeader<GenericContext<T>> genericHeader =
new MessageHeader<GenericContext<T>>(value);
context.OutgoingMessageHeaders.Add (genericHeader.GetUntypedHeader(
typeof(T).ToString (), typeof(T).Namespace));
}
}
}
GenericContext<T> 會完全封裝與消息頭的交互。要將其 他信息傳遞給服務,客戶端需要做的全部工作就是在新的 OperationContextScope 內部設置靜態 Current 屬性。例如,要在標頭中傳遞 數字 4,可使用圖 7 中顯示的代碼。可通過特定於類型的派生(例如圖 8 中顯 示的 IntContext)進一步封裝 GenericContext<T>。
Figure8整數環境
[DataContract]
public class IntContext : GenericContext<int>
{
public IntContext()
{}
public IntContext(int number) : base (number)
{}
public static int Number
{
get
return Current.Value;
set
Current = new IntContext(value);
}
}
Figure7使用 GenericContext<T>
MyContractClient proxy = new MyContractClient();
using(OperationContextScope contextScope =
new OperationContextScope (proxy.InnerChannel))
{
GenericContext<int>.Current = new GenericContext<int>(4);
proxy.MyMethod();
}
使用 IntContext,圖 7 會包含下面這樣一行:
IntContext.Current = 4;
嚴格說來,任何數據約 定(或可序列化)類型都可用於自定義環境中的類型參數。在服務端,要讀取自 定義標頭以外的值,任何下游方都可以編寫以下代碼:
int number = GenericContext<int>.Current.Value;
或使用 特定於類型的自定義環境:
int number = IntContext.Number;
這就是 PrioritySynchronizer 在尋找調用 優先級時所起到的作用。它等待客戶端在 TLS 中(通過 Priority 屬性)或以 自定義環境的形式提供優先級,後者在消息頭中存儲優先級,使用的代碼類似於 您在圖 7 中所見的代碼:
GenericContext<CallPriority>.Current =
new GenericContext<CallPriority>(CallPriority.High);
proxy.MyMethod();
但是,我對這樣的代碼並不是十分滿意, 因為它被公開了,它將客戶端和優先級機制結合在一起,並且強制客戶端在每次 使用代理之前都要設置優先級。為了解決這些問題,我創建了 HeaderClientBase<T,H> 代理基類,如圖 9 所示。
Figure9HeaderClientBase<T,H>
public abstract partial class HeaderClientBase<T,H> :
ClientBase<T> where T : class
{
protected H Header;
public HeaderClientBase() : this(default(H))
{}
public HeaderClientBase(string endpointName)
: this(default(H),endpointName)
{}
public HeaderClientBase(H header)
{
Header = header;
}
public HeaderClientBase(H header,string endpointName)
: base (endpointName)
{
Header = header;
}
/* More constructors */
protected virtual object Invoke(string operation,params object[] args)
{
using (OperationContextScope contextScope =
new OperationContextScope(InnerChannel))
{
GenericContext<H>.Current = new GenericContext<H> (Header);
Type contract = typeof(T);
MethodInfo methodInfo = contract.GetMethod(operation);
return methodInfo.Invoke(Channel,args);
}
}
}
和它的基類一樣,HeaderClientBase<T,H> 接受約定類型 參數 T,也接受自定義標頭類型 H。HeaderClientBase<T,H> 使用 H 的 默認值或構造時提供的值為所有調用執行傳遞。因為缺少代碼生成器(例如 SvcUtil)的支持,我借助反射來調用實際的服務操作,同時完全封裝與通用環 境和操作環境作用域的交互。您需要從 HeaderClientBase<T,H> 派生, 並提供需要的標頭類型,如圖 10 所示,並提供一個整數。
Figure10使用 HeaderClientBase<T,H>
[ServiceContract]
interface IMyContract
{
[OperationContract]
void MyMethod(string text);
}
class MyContractClient : HeaderClientBase<IMyContract,int>,IMyContract
{
public MyContractClient(int number) : base(number)
{}
public void MyMethod(string text)
{
Invoke ("MyMethod",text);
}
}
使用圖 10 中的代碼,即可看到圖 7 已簡化為:
MyContractClient proxy = new MyContractClient(4);
proxy.MyMethod();
雖然用圖 10 中顯示的同一技術可以傳遞調用優先級,但為了通用起見,最好還是定義一 個啟用通用優先級的代理 PriorityClientBase<T>,如圖 11 所示。
Figure11定義 PriorityClientBase<T>
public abstract partial class PriorityClientBase<T> :
HeaderClientBase<T,CallPriority> where T : class
{
public PriorityClientBase() : this(PrioritySynchronizer.Priority)
{}
public PriorityClientBase(string endpointName) :
this(PrioritySynchronizer.Priority,endpointName)
{}
public PriorityClientBase(Binding binding,
EndpointAddress remoteAddress) :
this (PrioritySynchronizer.Priority,binding,remoteAddress)
{}
public PriorityClientBase(CallPriority priority) : base (priority)
{}
public PriorityClientBase(CallPriority priority,string endpointName) :
base (priority,endpointName)
{}
public PriorityClientBase (CallPriority priority,Binding binding,
EndpointAddress remoteAddress) :
base(priority,binding,remoteAddress)
{}
/* More constructors */
}
PriorityClientBase<T> 會對類型參數 H 的 CallPriority 使用進行硬編碼。同時,PriorityClientBase<T> 默認為 從 TLS 中讀取優先級(如果找不到則生成 CallPriority.Normal),因此可以 像其他任何代理類那樣使用它。對現有的代理類略加修改,便可添加優先級處理 支持,如圖 12 所示。
Figure12優先級處理支持
class MyContractClient : PriorityClientBase<IMyContract>,IMyContract
{
//Reads priority from TLS
public MyContractClient()
{}
public MyContractClient(CallPriority priority) : base (priority)
{}
public void MyMethod(string text)
{
Invoke("MyMethod",text);
}
}
MyContractClient proxy = new MyContractClient(CallPriority.High);
proxy.MyMethod();
回調和同步環境
談及同步環境, WCF 為回調對象提供了類似的支持(CallbackBehaviorAttribute 應用於回調類 型):
[AttributeUsage(AttributeTargets.Class)]
public sealed class CallbackBehaviorAttribute : Attribute,...
{
public bool UseSynchronizationContext
{get;set;}
//More members
}
對於 ServiceBehaviorAttribute ,UseSynchronizationContext 默認為 true。回調對象有意思的地方在於與同 步環境的關聯被鎖定的時候。WCF 會檢查打開代理(建立了雙工通信)的線程。 如果該線程有同步環境,那麼該環境就將用於所有的回調。雖然可以在打開代理 之前通過顯式設置自定義同步環境來手動安裝它,但最好還是使用屬性以聲明的 方式來安裝它。為了影響回調終結點調度程序,該屬性需要實現 IEndpointBehavior 接口:
public interface IEndpointBehavior
{
void ApplyClientBehavior (ServiceEndpoint endpoint,
ClientRuntime clientRuntime);
//More members
}
在 ApplyClientBehavior 方法中,ClientRuntime 參數包含了 對具有 CallbackDispatchRuntime 屬性的終結點調度程序的引用:
public sealed class ClientRuntime
{
public DispatchRuntime CallbackDispatchRuntime
{get;}
//More members
}
其余的部分和服務端屬性相同,如圖 13 中 CallbackThreadPoolBehaviorAttribute 所演示的一樣。
Figure13CallbackThreadPoolBehaviorAttribute
[AttributeUsage(AttributeTargets.Class)]
public class CallbackThreadPoolBehaviorAttribute :
ThreadPoolBehaviorAttribute,IEndpointBehavior
{
public CallbackThreadPoolBehaviorAttribute(uint poolSize,
Type clientType) : this(poolSize,clientType,null)
{}
public CallbackThreadPoolBehaviorAttribute(uint poolSize,
Type clientType,string poolName) :
base(poolSize,clientType,poolName)
{
AppDomain.CurrentDomain.ProcessExit += delegate
{
ThreadPoolHelper.CloseThreads (ServiceType);
};
}
void IEndpointBehavior.ApplyClientBehavior
(ServiceEndpoint serviceEndpoint,ClientRuntime clientRuntime)
{
IContractBehavior contractBehavior = this;
contractBehavior.ApplyDispatchBehavior(null,serviceEndpoint,
clientRuntime.CallbackDispatchRuntime);
}
//Rest of the implementation
}
實際上,在回調屬性中,我想盡 可能多地重用服務屬性。為此,CallbackThreadPoolBehaviorAttribute 會派生 自 ThreadPoolBehaviorAttribute。它的構造函數將客戶端類型作為服務類型傳 遞給基礎構造函數。ApplyClientBehavior 的 CallbackThreadPoolBehaviorAttribute 實現會在其基類中查找 IContractBehavior(這就是子類使用其基類的顯式私有接口實現的方式),並 將實現委托給 ApplyDispatchBehavior。
客戶端回調屬性和服務屬性之 間最大的差別在於回調方案沒有訂閱到其 Closed 事件的主機對象。作為補償, CallbackThreadPoolBehaviorAttribute 會監視進程退出事件,以關閉池中的所 有線程。如果客戶端想加速關閉這些線程,則可以使用 ThreadPoolBehavior.CloseThreads,如圖 14 所示。
Figure14使用 CallbackThreadPoolBehaviorAttribute
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{
[OperationContract]
void MyMethod();
}
interface IMyContractCallback
{
[OperationContract]
void OnCallback();
}
[CallbackThreadPoolBehavior(3,typeof(MyClient))]
class MyClient : IMyContractCallback,IDisposable
{
MyContractClient m_Proxy;
public void CallService()
{
m_Proxy = new MyContractClient(new InstanceContext(this));
m_Proxy.MyMethod();
}
//Called by threads from the custom pool
public void OnCallback()
{...}
public void Dispose()
{
m_Proxy.Close();
ThreadPoolHelper.CloseThreads(typeof(MyClient));
}
}
就像服務端一樣,要讓所有回調都在同一個線程上執行(也許要 在回調端創建一些 UI),您可以將回調配置成擁有大小為 1 的池。但更好的做 法是,可以為這類同線程關聯定義專用的回調屬性,如圖 15 所示。
Figure15CallbackThreadAffinityBehaviorAttribute
[AttributeUsage(AttributeTargets.Class)]
public class CallbackThreadAffinityBehaviorAttribute :
CallbackThreadPoolBehaviorAttribute
{
public CallbackThreadAffinityBehaviorAttribute(Type clientType) :
this(clientType,"Callback Worker Thread")
{}
public CallbackThreadAffinityBehaviorAttribute(Type clientType,
string threadName) : base(1,clientType,threadName)
{}
}
為什麼要使用同步環境?
.NET Framework 2.0 中的同步環境是一個功能強大的構造,與 WCF 結合後,它們可以生成一個出色 而高效的編程模型。WCF 固有的可擴展性使這個組合的功能異常強大,從而以相 對較少的代碼行完成一些困難的任務(例如線程關聯和優先級處理),而且所有 這些都在服務的范圍之外。我在本專欄中演示的自定義標頭和通用環境技術在整 個 WCF 編程領域中都十分有用。在任何需要帶外參數的地方,它們都會提供妥 善、簡潔而且類型安全的解決方案。我還想與您分享我的思考過程以及各種折衷 考慮,可能有助於完善您自己的自定義 WCF 擴展。
請將您想詢問的問題 和提出的意見發送至[email protected].
Juval Lowy是 IDesign 的一名軟件架構師,該公司提供 WCF 培訓和 WCF 體系結構咨詢。他最近編寫了 一本名為《Programming WCF Services》(WCF 服務編程技術)(O'Reilly, 2007) 的新書。另外,他還是 Microsoft 硅谷地區的區域總監。您可以通過 www.idesign.net 與 Juval 聯系。
本文配套源碼