如何进行多线程安全的原子对象创建和交换
在 WCF 中,设置通信通道是一项成本较高的操作,因此建议设置一个通信通道并在应用程序的整个生命周期中共享它。一个重要的警告是,如果通道出现故障,则必须中止并替换该通道,这些僵尸通道无法复活。
我想做的是编写一个自定义代理,负责保持创建的健康通道并提供对其操作的访问。当通道发生故障时,粘性部分就会出现,我需要以线程安全的方式将其更换为新的、良好的通道。
这是我到目前为止所拥有的代码,当谈到线程安全编程的黑魔法时,我仍然是一个学徒。这段代码是杀伤力过大、杀伤力不足还是完全错误?同样的事情能否以更快、更简单或更正确的方式完成?
public class WcfDeviceActivationService : IDeviceActivationService {
ChannelFactory<IDeviceActivationService> _factory = new ChannelFactory<IDeviceActivationService>();
IDeviceActivationService _channel = null;
ReaderWriterLockSlim _swaplock = new ReaderWriterLockSlim();
public WcfDeviceActivationService() {
_channel = _factory.CreateChannel();
((IClientChannel)_channel).Open();
}
public Guid ActivateDevice(string activationCode, Guid userId) {
return Call(c => c.ActivateDevice(activationCode, userId) );
}
private RT Call<RT>(Func<IDeviceActivationService, RT> chanfunc) {
try {
using(_swaplock.UseReadLock())
return chanfunc(_channel);
} catch(Exception) {
// Get a reference to the channel before attempting the exclusive lock
var chan = _channel;
// Take an exclusive lock to block callers while we check the channel
// (just to prevent excessive failures under heavy call load)
using (_swaplock.UseWriteLock()) {
// Let's see if we're still working with the original channel and if it's faulted
if (Object.ReferenceEquals(chan, _channel) && ((IClientChannel)chan).State == CommunicationState.Faulted) {
// It faulted, so lets create a new channel to replace the bad one
// If the channel creation throws, the next attempt to use the failed channel
// will take this path again and attempt to create a fresh channel.
// We want the creation exception to propagate so that the caller
// knows that their call failed because the channel couldn't be created.
var newchan = _factory.CreateChannel();
((IClientChannel)newchan).Open();
// Exchange the new channel for the old one
// (assigning reference types is atomic, but Exchange also does a memory barrier for us)
Interlocked.Exchange(ref _channel, newchan);
// Clean up the old channel
((IClientChannel)chan).Abort();
}
}
// Propagate exception to the caller
throw;
}
}
}
更新
感谢 Fredrik 和 mlaw 的帮助,通过简化代码的思考,我对此进行了重构,但仍然不确定它是否完全正确。
In WCF setting up a communication channel is a somewhat expensive operation, so it is recommended to set one up and share it throughout your application's lifetime. One big caveat is that if the channel ever faults then that channel has to be aborted and replaced, these zombie channels cannot be resurrected.
What I'm trying to do is write a custom proxy that takes care of keeping a healthy channel created and providing access to its operations. The sticky part comes when the channel faults and I need to swap it for a new, good channel in a threadsafe manner.
Here is the code I have so far, I'm still a bit of an apprentice when it comes to the black art of thread safe programming. Is this code overkill, underkill, just plain wrong? Can the same thing be accomplished in a quicker, simpler, or more correct manner?
public class WcfDeviceActivationService : IDeviceActivationService {
ChannelFactory<IDeviceActivationService> _factory = new ChannelFactory<IDeviceActivationService>();
IDeviceActivationService _channel = null;
ReaderWriterLockSlim _swaplock = new ReaderWriterLockSlim();
public WcfDeviceActivationService() {
_channel = _factory.CreateChannel();
((IClientChannel)_channel).Open();
}
public Guid ActivateDevice(string activationCode, Guid userId) {
return Call(c => c.ActivateDevice(activationCode, userId) );
}
private RT Call<RT>(Func<IDeviceActivationService, RT> chanfunc) {
try {
using(_swaplock.UseReadLock())
return chanfunc(_channel);
} catch(Exception) {
// Get a reference to the channel before attempting the exclusive lock
var chan = _channel;
// Take an exclusive lock to block callers while we check the channel
// (just to prevent excessive failures under heavy call load)
using (_swaplock.UseWriteLock()) {
// Let's see if we're still working with the original channel and if it's faulted
if (Object.ReferenceEquals(chan, _channel) && ((IClientChannel)chan).State == CommunicationState.Faulted) {
// It faulted, so lets create a new channel to replace the bad one
// If the channel creation throws, the next attempt to use the failed channel
// will take this path again and attempt to create a fresh channel.
// We want the creation exception to propagate so that the caller
// knows that their call failed because the channel couldn't be created.
var newchan = _factory.CreateChannel();
((IClientChannel)newchan).Open();
// Exchange the new channel for the old one
// (assigning reference types is atomic, but Exchange also does a memory barrier for us)
Interlocked.Exchange(ref _channel, newchan);
// Clean up the old channel
((IClientChannel)chan).Abort();
}
}
// Propagate exception to the caller
throw;
}
}
}
UPDATE
Thinking through simplifying the code thanks to Fredrik and mlaw, I've refactored to this, still not sure if it's totally correct or not.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
您可以执行 Interlocked.CompareExchange 并将死通道作为比较对象传入新通道作为值传入。
如果交换成功,那就太好了。
如果交换失败,则说明已经有人更新了失效的通道。
唯一的问题是,您有时可能会创建一个额外的通道,在发现 CompareExchange 失败后您必须将其丢弃。这在您的情况下可以接受吗?
好处是 CompareExchange 可能是非阻塞的(不是 100% 确定,因为我是 Java 人员,而不是 .Net。我基于 Java 的 AtomicReference.compareAndSet)。
You could do an Interlocked.CompareExchange with the dead channel passed in as comparand and the new channel passed in as value.
If the exchange succeeds, great.
If the exchange fails, then someone already updated the dead channel.
The only issue is that you may, occasionally, create an extra channel that you just have to throw away after you find that CompareExchange fails. Is this acceptable in your situation?
The benefit is that CompareExchange is probably non-blocking (not 100% sure as I'm a Java guy, not .Net. I'm basing this assumption on Java's AtomicReference.compareAndSet).