【发布时间】:2015-05-15 15:20:45
【问题描述】:
我有一个 WCF 服务,其中 InstanceContextMode 是 Single 和 ConcurrencyMode 是 Multiple。目的是在实例化时创建值的缓存,而不会阻止其他不依赖于缓存创建的服务调用。
这样,只有尝试在 _classificationsCacheLock 上获得读取锁定的方法才需要等到填充 classificationsCache 的值 (classificationsCacheLock.IsWriterLockHeld = false)。
但是问题是,尽管在任务线程中获得了写入锁,调用 WCF 继续服务以响应对服务方法 GetFOIRequestClassificationsList() 的调用导致 _classificationsCacheLock.IsWriterLockHeld 成为 false,而它应该是真的。
这是WCF 实例化的奇怪行为,还是我从根本上错过了一个技巧。
我尝试在构造函数的线程上下文(安全选项)和生成的任务线程的上下文中获取写锁(这可能会在WCF 之间引入竞争,然后调用对GetFOIRequestClassificationsList() 函数的调用比调用classificationsCacheLock.AcquireWriterLock(Timeout.Infinite); 更快),但两者都导致classificationsCacheLock.IsWriterLockHeld 成为false,尽管通过使用thread.sleep 防止了任何竞争条件,在每个线程的代码块中适当地交错分开。
[ServiceBehavior(Namespace = Namespaces.MyNamespace,
ConcurrencyMode = ConcurrencyMode.Multiple,
InstanceContextMode = InstanceContextMode.Single)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MyService : IMyService
{
private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
private List<string> _classificationsCache;
private ReaderWriterLock _classificationsCacheLock;
public MyService()
{
try
{
_classificationsCacheLock = new ReaderWriterLock();
LoadCache();
}
catch (Exception ex)
{
_logger.Error(ex);
}
}
private void LoadCache()
{
// _classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
Task.Factory.StartNew(() =>
{
try
{
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite); // can only set writer on or off on same thread, not between threads
if (_classificationsCache == null)
{
var cases = SomeServices.GetAllFOIRequests();
_classificationsCache = cases.SelectMany(c => c.Classifications.Classification.Select(cl => cl.Group)).Distinct().ToList();
}
}
catch (Exception ex)
{
_logger.Error(ex);
}
finally
{
if (_classificationsCacheLock.IsWriterLockHeld)
_classificationsCacheLock.ReleaseWriterLock();
}
});//.ContinueWith((prevTask) =>
//{
// if (_classificationsCacheLock.IsWriterLockHeld)
// _classificationsCacheLock.ReleaseWriterLock();
// });
}
public GetFOIRequestClassificationsList_Response GetFOIRequestClassificationsList()
{
try
{
GetFOIRequestClassificationsList_Response response = new GetFOIRequestClassificationsList_Response();
_classificationsCacheLock.AcquireReaderLock(Timeout.Infinite);
response.Classifications = _classificationsCache;
_classificationsCacheLock.ReleaseReaderLock();
return response;
}
catch (Exception ex)
{
_logger.Error(ex);
if (ex is FaultException)
{
throw;
}
else
throw new FaultException(ex.Message);
}
}
}
编辑 1
由于许多建议都围绕线程池中的不确定性以及任务如何处理线程亲和性,我更改了方法以显式生成新线程
var newThread = new Thread(new ThreadStart(() =>
{
try
{
Thread.Sleep(2000);
Debug.WriteLine(string.Format("LoadCache - _classificationsCacheLock.GetHashCode - {0}", _classificationsCacheLock.GetHashCode()));
Debug.WriteLine(string.Format("LoadCache - Thread.CurrentThread.ManagedThreadId- {0} ", Thread.CurrentThread.ManagedThreadId));
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite); // can only set writer on or off on same thread, not between threads
if (_classificationsCache == null)
{
var cases = SomeServices.GetAllFOIRequests();
_classificationsCache = cases.SelectMany(c => c.Classifications.Classification.Select(cl => cl.Group)).Distinct().ToList();
}
}
catch (Exception ex)
{
_logger.Error(ex);
}
finally
{
if (_classificationsCacheLock.IsWriterLockHeld)
_classificationsCacheLock.ReleaseWriterLock();
}
}));
newThread.IsBackground = true;
newThread.Name = "MyNewThread"
newThread.Start();
结果还是一样。分类CacheLock.AcquireReaderLock 不会像它看起来应该的那样等待/阻塞。
我还添加了一些诊断程序来检查是否;
- 线程实际上是同一个线程,你不能指望 R/W 在同一个线程上阻塞
-
_classificationsCacheLock 的实例完全一样 次
公共 GetFOIRequestClassificationsList_Response GetFOIRequestClassificationsList() { 尝试 {
GetFOIRequestClassificationsList_Response response = new GetFOIRequestClassificationsList_Response(); Debug.WriteLine(string.Format("GetFOIRequestClassificationsList - _classificationsCacheLock.GetHashCode - {0}", _classificationsCacheLock.GetHashCode())); Debug.WriteLine(string.Format("GetFOIRequestClassificationsList - Thread.CurrentThread.ManagedThreadId - {0} ", Thread.CurrentThread.ManagedThreadId)); Thread.Sleep(1000); _classificationsCacheLock.AcquireReaderLock(Timeout.Infinite); //_classificationsCacheMRE.WaitOne(); response.Classifications = _classificationsCache; _classificationsCacheLock.ReleaseReaderLock(); return response; } catch (Exception ex) { _logger.Error(ex); if (ex is FaultException) { throw; } else throw new FaultException(ex.Message); } }
结果是..
GetFOIRequestClassificationsList - _classificationsCacheLock.GetHashCode - 16265870
GetFOIRequestClassificationsList - Thread.CurrentThread.ManagedThreadId - 9
LoadCache - _classificationsCacheLock.GetHashCode - 16265870
LoadCache - Thread.CurrentThread.ManagedThreadId- 10
.. 以这个顺序,所以现在我们有一个预期的竞争条件,因为在新创建的线程中获得了写锁。实际的WCF 服务调用是在构造函数的派生线程被安排实际运行之前进行的。所以我搬家了
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
到构造函数,因为这保证在访问任何类字段之前执行。
尽管有证据表明构造函数是在与执行服务方法的 WCF 线程不同的 WCF 线程上初始化的,但 AcquireWriterLock 仍然没有阻塞。
private void LoadCache()
{
_classificationsCacheLock.AcquireWriterLock(Timeout.Infinite);
Debug.WriteLine(string.Format("LoadCache constructor thread - _classificationsCacheLock.GetHashCode - {0}", _classificationsCacheLock.GetHashCode()));
Debug.WriteLine(string.Format("LoadCache constructor thread - Thread.CurrentThread.ManagedThreadId- {0} ", Thread.CurrentThread.ManagedThreadId));
var newThread = new Thread(new ThreadStart(() =>
{
try
{
Thread.Sleep(5000);
Debug.WriteLine(string.Format("LoadCache new thread - _classificationsCacheLock.GetHashCode - {0}", _classificationsCacheLock.GetHashCode()));
Debug.WriteLine(string.Format("LoadCache new thread - Thread.CurrentThread.ManagedThreadId- {0} ", Thread.CurrentThread.ManagedThreadId));
// _classificationsCacheLock.AcquireWriterLock(Timeout.Infinite); // can only set writer on or off on same thread, not between threads
if (_classificationsCache == null)
{
var cases = SomeServices.GetAllFOIRequests();
_classificationsCache = cases.SelectMany(c => c.Classifications.Classification.Select(cl => cl.Group)).Distinct().ToList();
}
}
catch (Exception ex)
{
_logger.Error(ex);
}
finally
{
if (_classificationsCacheLock.IsWriterLockHeld)
_classificationsCacheLock.ReleaseWriterLock();
}
}));
newThread.IsBackground = true;
newThread.Name = "CheckQueues" + DateTime.Now.Ticks.ToString();
newThread.Start();
}
再次 AcquireWriterLock 不会阻塞并允许分配空引用分类缓存。
结果是..
LoadCache constructor thread - _classificationsCacheLock.GetHashCode - 22863715
LoadCache constructor thread - Thread.CurrentThread.ManagedThreadId- 9
GetFOIRequestClassificationsList - _classificationsCacheLock.GetHashCode - 22863715
GetFOIRequestClassificationsList - Thread.CurrentThread.ManagedThreadId - 8
LoadCache new thread - _classificationsCacheLock.GetHashCode - 22863715
LoadCache new thread - Thread.CurrentThread.ManagedThreadId- 10
编辑 2
在没有源代码管理的情况下创建了解决方案的副本。
如果您想亲自解决问题,请上传here。
更改为在代码中使用手动重置事件进行演示,并注释掉问题代码。
- 放置断点
- 在调试中运行 Example.Web
- 在浏览器中导航到 'http://localhost:11164/GetFOIRequestClassificationsList.htm' 和 点击按钮。
MRE 有效,ReaderWriterLock 未按预期工作。
.net 4.0 - C#
【问题讨论】:
-
打印
_classificationsCacheLock.GetHashCode()。此外,通常最好不要依赖 WCF 实例化。我不明白为什么它甚至是框架的一部分。将您的共享状态放在其他类中并手动管理它(这很容易)。 -
您这么说既有趣又令人担忧,您是否有任何关于此类问题的讨论链接。是的,我稍后会得到实例的哈希值,看看我是否真的在研究某种对象上下文管理问题。
-
WCF 实例化像宣传的那样工作,但为什么要费心去理解和测试它呢?这就是我的观点。自己做一个单例是微不足道的。为什么要涉及 WCF?
-
我倾向于根据我的使用情况来决定并发和实例化策略,然后根据 WCF 中的需要对所选策略进行编码。坚持默认的 WCF 实现并不总是吞吐量、内存消耗和进程效率的最佳方法。使用 Monitor (lock {}) 可以解决这个问题,但它并不理想,此外,这是 ReaderWriterLock 的问题或我对如何使用它的误解。
-
如果我要进行大量短调用来访问可能共享的数据,最好告诉 WCF 不要为每个请求创建新实例。通过将服务移动到自定义代理单例中,我仍然需要 WCF 为每个线程创建一个实例。想法是,对于单行属性,我在 WCF 单模式下获得了一个有效的 Singleton,WCF 也没有在我的实例化策略中执行冗余操作,接受请求,扇出实例只是为了让我再次扇回委托给自定义单身人士。不过,这里有些东西不起作用。
标签: c# multithreading wcf readerwriterlock wcf-instancing