65.9K
CodeProject 正在变化。 阅读更多。
Home

WCF:双工操作和 UI 线程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (160投票s)

2007 年 2 月 20 日

CPOL

7分钟阅读

viewsIcon

551251

downloadIcon

9956

本文探讨了 WCF 中双工操作的实现方式,以及在处理 UI 线程时可能出现的一些问题。

引言

简单来说,双工操作提供了一种允许服务回调客户端的机制。当为服务定义契约时,可以指定相应的回调契约。标准服务契约定义了客户端可以调用的服务操作。回调契约定义了服务可以调用的客户端操作。客户端有责任实现回调契约并托管回调对象。每次客户端调用与回调契约关联的服务操作时,客户端都会提供服务定位和调用客户端回调操作所需的必要信息。

双工操作最常见的用途似乎是用于事件。这通常通过使用发布-订阅模式来实现。一个或多个客户端将订阅该服务。当发生感兴趣的事件时,服务会将信息发布给已订阅的客户端。

演示采用的就是这种方法。它基于这样一个概念:通过跟踪客人及其啤酒消费来协助派对主人。虽然故意幽默,但它有效地说明了双工操作涉及的关键概念。每个客人通过加入派对来订阅服务。当发生有趣的事情时,例如其他客人加入/离开派对以及啤酒的消耗,服务会将信息发布给订阅的客户端。

Screenshot - beer_inventory.jpg

让我们更仔细地看看这个魔法是如何实现的。

服务契约和回调契约

通常,一个接口会用 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 ,这比死锁要好。

那么,如何在服务操作的中间调用回调?这基本上归结为三个选择:

  1. ServiceBehavior 特性中,将 ConcurrencyMode 属性设置为 Reentrant。这仍然使用单线程模型,但 WCF 在您调用客户端回调时会释放锁。这允许服务实例处理来自回调的回复。但是,您必须确保在调用回调之前和之后任何本地数据的状态都是有效的。
  2. ServiceBehavior 特性中,将 ConcurrencyMode 属性设置为 Multiple。这启用了多线程模型,需要服务实现来处理必要的锁定。
  3. 在回调方法的 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 中易于实现。利用这种能力,可以非常轻松地在服务和客户端之间实现类似事件的行为。希望本文为您提供了足够的信息,以便开始应用此概念。

© . All rights reserved.