WCF:双工操作和 UI 线程






4.91/5 (160投票s)
本文探讨了 WCF 中双工操作的实现方式,以及在处理 UI 线程时可能出现的一些问题。
引言
简单来说,双工操作提供了一种允许服务回调客户端的机制。当为服务定义契约时,可以指定相应的回调契约。标准服务契约定义了客户端可以调用的服务操作。回调契约定义了服务可以调用的客户端操作。客户端有责任实现回调契约并托管回调对象。每次客户端调用与回调契约关联的服务操作时,客户端都会提供服务定位和调用客户端回调操作所需的必要信息。
双工操作最常见的用途似乎是用于事件。这通常通过使用发布-订阅模式来实现。一个或多个客户端将订阅该服务。当发生感兴趣的事件时,服务会将信息发布给已订阅的客户端。
演示采用的就是这种方法。它基于这样一个概念:通过跟踪客人及其啤酒消费来协助派对主人。虽然故意幽默,但它有效地说明了双工操作涉及的关键概念。每个客人通过加入派对来订阅服务。当发生有趣的事情时,例如其他客人加入/离开派对以及啤酒的消耗,服务会将信息发布给订阅的客户端。
让我们更仔细地看看这个魔法是如何实现的。
服务契约和回调契约
通常,一个接口会用 ServiceContract
特性进行装饰,以告知 WCF 运行时该接口包含服务操作。ServiceContract
特性的 CallbackContract
属性用于指定定义回调操作的接口。这会在两个接口之间创建关联。为了使用服务操作,客户端必须实现回调契约并托管该对象以便服务调用。
[ServiceContract(
SessionMode = SessionMode.Required,
CallbackContract = typeof(IBeerInventoryCallback))]
public interface IBeerInventory
{
[OperationContract()]
int JoinTheParty(string guestName);
[OperationContract(IsOneWay = true)]
void MakeBeerRun(string guestName, int numberOfBeers);
[OperationContract(IsOneWay = true)]
void DrinkBeer(string guestName);
[OperationContract(IsOneWay = true)]
void LeaveTheParty(string guestName);
}
public interface IBeerInventoryCallback
{
[OperationContract(IsOneWay = true)]
void NotifyGuestJoinedParty(string guestName);
[OperationContract(IsOneWay = true)]
void NotifyBeerInventoryChanged(string guestName, int numberOfBeers);
[OperationContract(IsOneWay = true)]
void NotifyGuestLeftParty(string guestName);
}
需要注意的是,IBeerInventoryCallback
接口没有用 ServiceContract
特性进行装饰。由于它被指定为回调契约,WCF 运行时将隐式地为该接口添加一个 ServiceContract
特性。但是,如果愿意,您仍然可以显式地添加它。
服务实现
为了在服务中调用客户端回调,必须获取对回调对象的引用。每次客户端调用与回调契约关联的服务操作时,它都会提供一个可用于与回调对象通信的回调通道。回调通道可以在 OperationContext
中找到,如下所示:
private List<IBeerInventoryCallback> _callbackList;
public int JoinTheParty(string guestName)
{
IBeerInventoryCallback guest =
OperationContext.Current.GetCallbackChannel<IBeerInventoryCallback>();
if (!_callbackList.Contains(guest))
{
_callbackList.Add(guest);
}
_callbackList.ForEach(
delegate(IBeerInventoryCallback callback)
{ callback.NotifyGuestJoinedParty(guestName); });
return _beerInventory;
}
服务将回调通道的引用存储在一个集合中。当服务准备好将通知发布给订阅者时,将调用集合中的每个回调通道。现在,如果您的服务未正确配置和/或实现,您可能会遇到一些问题。特别是,并发模型成为一个因素。
并发模式
默认情况下,WCF 服务配置为单线程。这意味着在任何给定时间,只有一个线程会处理服务实例的消息。因此,如果在处理当前消息时收到其他消息,它们将被阻塞,直到当前消息处理完成并释放锁。
如果在单线程服务操作的中间尝试调用客户端回调,将会发生一个称为死锁的糟糕情况。这是由于并发模型所需的锁定造成的。当在客户端上调用回调操作时,服务必须等待回复返回以进行处理。但是,服务已经被当前操作的处理锁定。因此,会发生死锁。幸运的是,WCF 可以检测到您尝试执行此类操作,并抛出 InvalidOperationException
,这比死锁要好。
那么,如何在服务操作的中间调用回调?这基本上归结为三个选择:
- 在
ServiceBehavior
特性中,将ConcurrencyMode
属性设置为Reentrant
。这仍然使用单线程模型,但 WCF 在您调用客户端回调时会释放锁。这允许服务实例处理来自回调的回复。但是,您必须确保在调用回调之前和之后任何本地数据的状态都是有效的。 - 在
ServiceBehavior
特性中,将ConcurrencyMode
属性设置为Multiple
。这启用了多线程模型,需要服务实现来处理必要的锁定。 - 在回调方法的
OperationContract
特性中,将IsOneWay
属性设置为 true。这允许 WCF 调用客户端上的回调操作,而无需锁定,因为不会有回复需要处理。
演示使用了第三种选项。
客户端实现
如前所述,客户端必须实现回调契约并托管回调对象。对于演示,回调契约已直接在客户端窗体中实现。窗体加载时,将实例化一个 InstanceContext
并提供给服务代理。InstanceContext
是一个 WCF 对象,负责处理托管回调对象所需的底层基础结构,并大大简化了所需代码量。一旦实例化了 InstanceContext
和服务代理,就可以与服务建立连接以调用操作。
_proxy = new BeerInventoryServiceClient(new InstanceContext(this));
_proxy.Open();
UI 线程的困扰
现在,当用户与窗体交互时,将调用相应的服务操作。其中一些操作会导致回调已订阅的客户端。然而,这会给 Windows 窗体(和 WPF)应用程序带来一个特定问题。如果 UI 线程调用导致回调操作的服务操作,将会发生死锁情况。
考虑单击“加入派对”按钮时执行的以下代码行:
this.BeerInventory = _proxy.JoinTheParty(this.txtGuestName.Text);
起初这似乎无伤大雅,但请考虑正在发生的事情。当从 UI 线程调用服务操作时,它将阻塞直到收到返回值。然而,服务操作在发送返回值之前会回调到客户端。回调操作将被编排到 UI 线程。由于 UI 线程仍在等待返回值,因此会发生死锁。此问题的根源在于同步上下文。
默认情况下,WCF 会将线程亲和性提供给用于与服务建立连接的当前同步上下文。所有服务请求和回复都将在原始同步上下文的线程上执行。在某些情况下,这可能是期望的行为。而在其他情况下,可能并非如此。在我们的例子中,这导致了一个问题。
同步上下文
幸运的是,WCF 提供了一种简单的机制来覆盖同步上下文的自动关联。可以通过将 CallbackBehavior
特性的 UseSynchronizationContext
属性设置为 false
来关闭此行为。这样做之后,WCF 将不再保证特定线程负责处理服务请求。相反,操作将被委派给工作线程。
[CallbackBehavior(UseSynchronizationContext = false)]
不幸的是,这解决了一个问题,但又为我们的演示带来了另一个问题。客户端上的回调操作需要更新 UI 的属性。但是,只有 UI 线程可以直接操作窗体控件的属性。由于回调操作将在工作线程上运行,因此需要一些额外的努力才能在 UI 线程上操作 UI。有多种方法可以完成此任务,但演示采用了 SendOrPostCallback
。
SendOrPostCallback
是一个特殊委托,用于将消息分派到特定的同步上下文。加载窗体时,将捕获 UI 同步上下文的引用。在回调实现中,将为必要的 UI 更新创建一个 SendOrPostCallback
委托。然后,该委托将在 UI 同步上下文上调用,以避免安全错误。这提供了一种机制,供执行回调的工作线程与 UI 线程通信,以便更新窗体上的控件。
SendOrPostCallback callback =
delegate (object state)
{ this.WritePartyLogMessage(String.Format("{0} has joined the party.",
state.ToString())); };
_uiSyncContext.Post(callback, guestName);
结论
双工操作是一个非常强大的概念,在 WCF 中易于实现。利用这种能力,可以非常轻松地在服务和客户端之间实现类似事件的行为。希望本文为您提供了足够的信息,以便开始应用此概念。