理解 SynchronizationContext:第三部分






4.96/5 (47投票s)
在 WCF 中使用 SynchronizationContext。
引言
本文是关于 SynchronizationContext
三部曲的最后一部分。SynchronizationContext
是 .NET 2.0 引入的一个类,但关于如何使用它的文档或解释很少。我试图在第一部分中解释如何使用这个类,并在第二部分中解释如何创建自己的 SynchronizationContext
。在第二部分中,我展示了如何构建一个 SynchronizationContext
,它将把代码从任何 .NET 线程调度到 STA 线程。我这样做是为了能够执行需要在 STA 线程上运行的 COM 代码。下一步是创建一个 WCF 服务,它将在 STA 线程上执行其所有服务操作(使用我在第二部分中提供的 SynchronizationContext
)。在本文中,我将向您展示如何配置 WCF 以提供自定义的 STA SynchronizationContext
,以便 WCF 服务上的每个方法都将在同一个 STA 线程上执行。这将允许我提供一个简单的编程模型,该模型将完全兼容 COM,并且我不需要担心线程安全,因为我的所有代码都将在同一个线程中运行。
WCF - 强大之处
我甚至不会在这里尝试对 WCF 进行完整解释。WCF 功能非常广泛,它不仅仅是“远程处理”或 Web 服务。我开始相信 WCF 应该被视为运行时而不是通信框架。WCF 为我们提供了以下“开箱即用”的编程模型:
- 内置错误处理支持
- 内置并发支持
- 内置安全性支持
- 内置事务支持
- 内置数据加密支持
- 内置持久服务支持
- 内置方法拦截和检查支持 (AOP)
- 内置单向通信和回调支持
- 等等……
听听这个 ARCast,其中 Juval Lowy 认为每个类都应该是一个 WCF 类:每个类都是一个 WCF 服务,与 Juval Lowy。起初,我对自己说,这太多了,每个类都成为 WCF 类简直是疯了。但是,我越是深入了解 WCF 及其所提供的一切,它就越对我更有意义。WCF 是一个非常可扩展的框架,允许开发人员进行大量自定义配置。例如,为服务提供 SynchronizationContext
是您无法在原生 .NET 类中完成的事情,但只能在 WCF 中完成(这只是众多功能之一)。虽然,我必须说我没有做到让每个类都成为 WCF 类。我决定让每个组件都成为 WCF 服务。即使我不打算远程运行我的组件,我仍然相信 WCF 运行时的好处值得将我的组件编写为 WCF 服务。底线是,WCF 不仅仅是 Web 服务或远程处理服务,它提供了一个适用于每种类型开发的坚实编程模型。如果您不了解 WCF,我强烈建议您学习它,它是微软发布的更好的框架之一。对于本文,我假设您对 WCF 服务有基本了解。
一个简单的 WCF 服务
让我立即声明,我为本文编写的 WCF 服务仅用于测试。我不建议您以我编写的方式编写服务。更具体地说,在我的代码中,服务实现和服务契约位于同一个程序集中——这是不推荐的。但是,因为我正在原型/测试项目中测试我的SynchronizationContext
,我希望将程序集和代码的数量保持在最低限度。通常,当我编写 WCF 服务时,我有三个项目:
- 包含服务契约(接口)以及可能的任何数据契约(DTOs)的项目
- 包含服务实现的项目
- 托管服务的项目(可选)
所以请不要指责,我在这里声明这些代码仅用于测试而非生产。现在我们已经解决了这个问题,让我展示服务契约和实现。
[ServiceContract]
public interface IStaService
{
[OperationContract]
string DoWorkOnSTAThread(string state);
}
关于这个服务真的没什么好说的,它非常简单。让我向您展示这个服务的实现:
public class StaService : IStaService
{
public string DoWorkOnSTAThread(string state)
{
ApartmentState aptState = Thread.CurrentThread.GetApartmentState();
if (aptState == ApartmentState.STA)
Trace.WriteLine("Using STA thread");
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("WCF current thread: " + id);
return "processed by " + aptState.ToString() + " Thread id: " + id.ToString();
}
}
注释
DoWorkOnSTAThread
是我计划在 STA 线程上执行的方法,所以我编写了一些跟踪代码,以确保我正在 STA 线程上运行此方法。我检查ApartmentState
并确保它是ApartmentState.STA
。- 我还记录了线程 ID;考虑到我希望我的所有代码都在同一个 STA 线程上运行,所以 ID 应该始终相同。
- 但是,我没有编写任何代码来指示使用 STA 同步上下文,因此目前尚未使用 STA 线程调度。
测试我们的服务
我们将运行我们的服务几次,所以让我们学习如何测试它。我的服务使用 webdev 托管,并作为 Web 服务运行。为了测试它,我使用了 WCF 测试客户端应用程序。它通常位于“C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\WcfTestClient.exe”。通过多次调用 DoWorkOnSTAThread
,我得到了以下输出:
WCF current thread: 12
WCF current thread: 12
WCF current thread: 11
WCF current thread: 11
WCF current thread: 11
WCF current thread: 13
WCF current thread: 10
WCF current thread: 11
WCF current thread: 10
WCF current thread: 10
WCF current thread: 13
要了解服务行为,您可以在自定义 ServiceHost 中查看 ServiceDescription
(我将在文章后面展示自定义服务主机代码)。
请注意,我正在多个线程上运行相同的方法。这是因为,默认情况下,WCF 使用 PerSession
InstanceContextMode
和 ConcurrencyMode
为 Single
创建了我的服务(参见上图)。这意味着该方法将一次执行一个,但每次都由线程池分配的线程执行。首先,您应该避免为服务使用 PerSession
。PerSession
根本无法扩展,我将其视为编写可伸缩服务的祸根,因此,我将更改服务行为。
[ServiceBehavior(UseSynchronizationContext=true,
ConcurrencyMode=ConcurrencyMode.Multiple,
InstanceContextMode=InstanceContextMode.PerCall)]
public class StaService : IStaService
{
public string DoWorkOnSTAThread(string state)
{
ApartmentState aptState = Thread.CurrentThread.GetApartmentState();
if (aptState == ApartmentState.STA)
Trace.WriteLine("Using STA thread");
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("WCF current thread: " + id);
//throw new Exception("boom");
return "processed by " + aptState.ToString() + " Thread id: " + id.ToString();
}
}
使用 WCF 服务行为属性 [ServiceBehavior(UseSynchronizationContext=true, ConcurrencyMode=ConcurrencyMode.Multiple, InstanceContextMode=InstanceContextMode.PerCall)]
,我要求 WCF 使用 InstanceContextMode.PerCall
创建此服务。这意味着,每个方法调用都将创建一个服务实例,并且方法完成后,实例将被销毁。使用 ConcurrencyMode.Multiple
允许客户端并发执行此服务中的方法,从而允许多个线程同时执行相同的方法。UseSynchronizationContext=true
意味着我要求服务使用附加到主机线程的 SynchronizationContext
。使用这种新的服务行为,让我们通过执行该方法三次来查看我们的输出:
WCF current thread: 12
WCF current thread: 9
WCF current thread: 6
结果大同小异……我们的服务可以更好地扩展,但仍然无法控制代码在哪个线程上执行。
为您的服务提供 SynchronizationContext
您会注意到,无论服务是使用单并发模式还是多并发模式,WCF 都会使用线程池中不同的线程来控制调用。为了提供您自己的 SynchronizationContext
,您有两种选择:
- 在打开主机之前,在托管线程上设置
SynchronizationContext
- 创建您自己的
ServiceBehavior
并覆盖服务终结点上的SynchronizationContext
我将探讨第二种选择,因为我们并不总是创建自己的托管程序,并且在许多情况下,托管是在 WAS 或 IIS 中完成的。让我向您展示我为使用 STA 线程同步而创建的 ServiceBehvior
属性。
public class StaServiceBehaviorAttribute : Attribute, IContractBehavior, IServiceBehavior
{
StaSynchronizationContext mStaContext;
public StaServiceBehaviorAttribute()
{
mStaContext = new StaSynchronizationContext();
}
#region IContractBehavior Members
void IContractBehavior.AddBindingParameters(ContractDescription contractDescription,
ServiceEndpoint endpoint,
System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
void IContractBehavior.ApplyClientBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint,
System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
}
void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint,
System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime)
{
dispatchRuntime.SynchronizationContext = mStaContext;
}
void IContractBehavior.Validate(ContractDescription contractDescription,
ServiceEndpoint endpoint)
{
}
#endregion
#region IServiceBehavior Members
void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase,
System.Collections.ObjectModel.Collection endpoints,
System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription,
System.ServiceModel.
ServiceHostBase serviceHostBase)
{
}
void IServiceBehavior.Validate(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase)
{
serviceHostBase.Closed += delegate
{
mStaContext.Dispose();
};
}
#endregion
}
通过创建我自己的自定义 ServiceBehvior
属性,我可以设置一些我原本无法使用的设置。
请注意,我正在构造函数中创建 StaSynchronizationContext
public StaServiceBehaviorAttribute()
{
mStaContext = new StaSynchronizationContext();
}
另一个最重要的部分是实现 ApplydispatchBehvior
方法。此方法将在服务中的每个终结点上执行一次。它允许我用我自己的自定义 SynchronizationContext
覆盖默认的 SynchronizationContext
。
void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint,
System.ServiceModel.Dispatcher.DispatchRuntime dispatchRuntime)
{
dispatchRuntime.SynchronizationContext = mStaContext;
}
为了确保我的 STA 线程正确结束,我还修改了 IServiceBehavior.Validate
并为关闭主机提供了事件处理程序。
void IServiceBehavior.Validate(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase)
{
serviceHostBase.Closed += delegate
{
mStaContext.Dispose();
};
}
使用 STA 同步上下文
现在我们已经创建了自己的服务行为来应用自定义同步上下文,让我们再看看我们的服务。
[StaServiceBehaviorAttribute]
[ServiceBehavior(UseSynchronizationContext=true, ConcurrencyMode=ConcurrencyMode.Multiple,
InstanceContextMode=InstanceContextMode.PerCall)]
public class StaService : IStaService
{
public string DoWorkOnSTAThread(string state)
{
ApartmentState aptState = Thread.CurrentThread.GetApartmentState();
if (aptState == ApartmentState.STA)
Trace.WriteLine("Using STA thread");
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("WCF current thread: " + id);
//throw new Exception("boom");
return "processed by " + aptState.ToString() + " Thread id: " + id.ToString();
}
}
- 请注意,我保留了 WCF 服务行为,以使用
PerCall
和ConcurrencyMode.Multiple
。考虑到我们计划将代码调度到 STA 线程,使用ConcurrencyMode.Single
将与ConcurrencyMode.Multiple
具有相同的行为。 - 请注意,我已将
StaServiceBehaiorAttribue
放置在服务上。这将创建 STA 线程,并对我的服务公开的所有终结点应用 STA 同步上下文(在示例中,我只有一个终结点)。
好的,让我们尝试一下。使用 WCF 客户端工具,我向我的服务发送了多个请求,结果如下:
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
Using STA thread
WCF current thread: 9
- 请注意,所有对 WCF 方法的调用都在线程 9 上执行。
- 请注意,线程 9 是一个 STA 线程,正如我们所预期的。
现在,我能够控制 WCF 服务在哪个线程上执行。
关于托管的一句话
请注意,使用此方法,无论您选择哪种托管方式,您都可以控制服务的同步上下文。如果您使用控制台应用程序进行托管,则可以将同步上下文设置为 STA(使用 SynchronizationContext.SetSynchronizationContext
)。在打开主机之前,它将具有相同的效果。我尝试使用自定义 ServiceHostFactory
并在那里设置同步上下文,但它不起作用。让我向您展示自定义 ServiceHostFactory
和自定义 ServiceHost
。
public class StaCustomHostFactory : ServiceHostFactory
{
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
return new StaCustomHost(serviceType, baseAddresses);
}
}
public class StaCustomHost : ServiceHost
{
public StaCustomHost(Type serviceType, params Uri[] baseAddresses) :
base(serviceType, baseAddresses)
{
}
protected override void InitializeRuntime()
{
int id = Thread.CurrentThread.ManagedThreadId;
base.InitializeRuntime();
}
protected override void ApplyConfiguration()
{
int id = Thread.CurrentThread.ManagedThreadId;
// get the configuration from the app.config first
base.ApplyConfiguration();
// create the STA Thread Sync object and attach it to this hosting
// thread.
StaSynchronizationContext staContext = new StaSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(staContext);
}
}
<%@ ServiceHost
Language="C#" Debug="true"
Factory="StaServiceHost.StaCustomHostFactory"
Service="TestStaService.StaService"
CodeBehind="StaService.svc.cs" %>
请注意,我在 ApplyConfiguration
方法中设置了 SynchronizationContext
。WCF 将获取主机线程上设置的任何 SynchronizationContext
,并将其用于每次调用。但是,它根本不起作用。我不知道具体原因,但我能想到的唯一解释是 ApplyConfiguration
中的代码与 ServiceHost.Open
不在同一个线程中运行。考虑到主机是由 webdev 或 IIS 打开的,我真的不知道如何在这些线程上设置同步上下文。因此,我建议您坚持使用我之前展示的自定义服务行为来覆盖服务终结点的同步上下文。
关于关闭主机的简要说明。如第二部分所述,一旦创建了 STA SynchronizationContext
对象,就会创建 STA 线程。在这种情况下,它是在服务行为中创建的。重要的是,此线程在主机关闭时结束。这就是我编写此事件处理程序的原因:
void IServiceBehavior.Validate(ServiceDescription serviceDescription,
System.ServiceModel.ServiceHostBase serviceHostBase)
{
serviceHostBase.Closed += delegate
{
mStaContext.Dispose();
};
}
我通过停止 webdev 进程并在事件处理程序上设置断点来测试此代码。我验证了当 webdev 关闭时 STA 线程正在退出。感谢 WCF 的小奇迹。
WCF 使用 Send 还是 Post?
您可能想知道是使用了我们同步上下文的 Send
还是 Post
。我真的不知道,所以我在这两个 Send
和 Post
方法上设置了一个断点,发现 WCF 总是使用 Post
方法。我将服务并发模式更改为 ConcurrencyMode.Single
,但仍然使用了 Post
。因此,当使用 Single
或 Multiple
并发时,在这两种情况下,都使用 Post
方法将代码调度到 STA 线程。那么异常呢?请注意,我已经修改了我的服务以抛出异常:
[StaServiceBehaviorAttribute]
[ServiceBehavior(UseSynchronizationContext=true,
ConcurrencyMode=ConcurrencyMode.Multiple,
InstanceContextMode=InstanceContextMode.PerCall)]
public class StaService : IStaService
{
public string DoWorkOnSTAThread(string state)
{
ApartmentState aptState = Thread.CurrentThread.GetApartmentState();
if (aptState == ApartmentState.STA)
Trace.WriteLine("Using STA thread");
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("WCF current thread: " + id);
throw new Exception("boom");
//return "processed by " + aptState.ToString() +
// " Thread id: " + id.ToString();
}
}
最初,我相信这可能会导致 STA 线程终止。这是一个在 STA 线程上运行的未处理异常,但 WCF 会为您处理异常。因此,即使您在 STA 线程中抛出异常,STA 线程也不会终止。WCF 会捕获任何未处理的异常,并将其转换为客户端线程上的 FaultException
。这使得我们的 STA 线程不会终止,因此无论是否抛出异常,它都会继续运行。感谢上帝的另一个 WCF 奇迹。
结论
在本文中,我深入探讨了 WCF 的内部工作原理,并掌握了服务方法“在哪里”执行的控制权。我能够“告诉”WCF 将所有方法调用调度到 STA 线程,从而使我们不必担心调用旨在在 STA 线程上工作的 COM 对象。这还将允许您在服务方法中弹出 UI,但我真的不推荐这样做。通过自行调度代码,您可以添加额外的逻辑来记录和验证每次调用。您甚至可以在此级别添加安全性,并在不符合特定条件时拒绝调度调用。但是,不要将其用作通配符;服务端方法拦截也可以做同样的事情。尽管如此,既然您已经了解了此功能,您可能会在项目中找到它的好用途。
感谢阅读,祝您 .NET 开发愉快。