【问题标题】:ReaderWriterLock not working in ServiceBehavior constructorReaderWriterLock 在 ServiceBehavior 构造函数中不起作用
【发布时间】:2015-05-15 15:20:45
【问题描述】:

我有一个 WCF 服务,其中 InstanceContextModeSingleConcurrencyModeMultiple。目的是在实例化时创建值的缓存,而不会阻止其他不依赖于缓存创建的服务调用。

这样,只有尝试在 _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

更改为在代码中使用手动重置事件进行演示,并注释掉问题代码。

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


【解决方案1】:

CaseWork() 方法中,每次调用该方法时,您都会创建新的ReaderWriterLock。所以被获取的锁只是简单地发送给垃圾收集器,新的锁就会出现。因此实际上没有正确获取任何锁。

为什么不为此使用static 锁,并在static 构造函数中创建它?

如果我错了,请纠正我,但如果您要更新缓存,我不能。如果这是真的,我建议您简单地使用Lazy&lt;T&gt; 类。它是线程安全的,并在设置值之前保存所有读取器。它在内部使用TPL,并且简单地使用:

private Lazy<List<string>> _classificationsCache = new Lazy<List<string>>
(() => 
{
    var cases = SomeServices.GetAllFOIRequests();
    return cases.SelectMany(c => c.Classifications.Classification.Select(cl => cl.Group)).Distinct().ToList();
});

你可以得到这样的值:

response.Classifications = _classificationsCache.Value;

Update from MSDN:

如果当前线程已经有写锁,没有读锁 收购。相反,写入者锁的锁计数会增加。 这可以防止线程阻塞自己的写入器锁。这 结果与调用AcquireWriterLock 完全相同,并且 释放时需要额外调用ReleaseWriterLock 写锁。

我认为这件事正在发生:

您的读取器锁获取正在同一线程Task.StartNew 方法使用TaskScheduler.Current 属性)内触发写入器锁正在工作,因此它不会阻塞,因为它具有相同的像Task 那样的特权,并得到了空列表。因此,在您的情况下,您必须选择另一个同步原语。

【讨论】:

  • 我必须道歉,我错过了重命名构造函数的原始名称,我现在已经更正了。这会误导您认为我每次都在创建新的锁实例。我只为每个设置为单个的类实例创建一个实例,因此 WCF 应该只创建一个 MyService 实例,该实例应该为每个请求提供服务。
  • 确实,我可以使用线程安全的集合,也许这就是我在这种特殊情况下最终会做的事情。但是,在这种情况下,ReaderWriterLock 似乎行为不端是多么奇怪。我还看不出我做了什么(或没有做什么)会导致这种情况。
  • 我认为您正在启动的Task 运行不正确
  • classificationsCache 确实会在任务完成后填充,不会引发错误。同时,后台的请求不会被对分类缓存锁的调用阻塞。AcquireReaderLock 而是获取空分类缓存,直到任务运行,然后每个后续请求都获取填充分类缓存。尽管将获取写锁放置在调用上下文(已注释掉)或任务本身中。
  • 对不起VMAtm,它不是同一个线程。您的延迟加载是一种优雅的解决方法,可能是实现目标的更好方法(或在 ii7+ 中使用 WCF 初始化程序),但这仍然会留下这个问题,这表明 .Net 中的行为异常,除非我们能够深入了解原因,这可能是一个 .NET 问题。我选择不将您的建议标记为答案,因为尽管在不同线程上获得了写入块,但它并没有深入了解为什么 R/W 锁无法正常工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-16
  • 1970-01-01
相关资源
最近更新 更多