防止重入并确保为某些操作获取锁的正确方法是啥?

Posted

技术标签:

【中文标题】防止重入并确保为某些操作获取锁的正确方法是啥?【英文标题】:What is the correct way to prevent reentrancy and ensure a lock is acquired for certain operations?防止重入并确保为某些操作获取锁的正确方法是什么? 【发布时间】:2013-10-01 17:37:22 【问题描述】:

我正在设计一个基类,当它被继承时,它将针对多线程环境中的上下文提供业务功能。每个实例都可能有长时间运行的初始化操作,所以我想让对象可重用。为此,我需要能够:

    为这些对象之一分配上下文以允许其工作 防止已为对象分配新上下文 防止在对象没有上下文时访问某些成员

此外,每个上下文对象可以被许多工作对象共享。

是否有适合我想要做的正确同步原语?这是我想出的最适合我需要的模式:

private Context currentContext;

internal void BeginProcess(Context currentContext)

    // attempt to acquire a lock; throw if the lock is already acquired,
    // otherwise store the current context in the instance field


internal void EndProcess()

    // release the lock and set the instance field to null


private void ThrowIfNotProcessing()

    // throw if this method is called while there is no lock acquired

使用上述方法,我可以保护不应访问的基类属性和方法,除非对象当前处于处理状态。

protected Context CurrentContext

    get
    
        this.ThrowIfNotProcessing();
        return this.context;
    


protected void SomeAction()

    this.ThrowIfNotProcessing();

    // do something important

虽然我最初是使用Monitor.Enter 和相关函数,但这并不能阻止同线程重入(在原始线程上多次调用BeginProcess)。

【问题讨论】:

既然您已经throw if the lock is already acquired 并且不想等待当前处理完成:为什么不只是throw if current context is not null 【参考方案1】:

.NET 中有一个不可重入的同步对象,您正在寻找一个信号量。

在你承诺之前,一定要把你的鸭子排成一排,问问自己,BeginProcess() 怎么可能在同一个线程上再次被调用。这是非常非常不寻常的,您的代码必须可重入才能发生。这通常只会发生在具有调度程序循环的线程上,GUI 应用程序的 UI 线程就是一个常见的例子。如果这确实可行并且您实际上使用了信号量,那么您也将处理后果,您的代码将死锁。因为它递归到 BeginProcess 并在信号量上停止。因此永远不会完成,也永远无法调用 EndProcess()。 Monitor 和 Mutex 可重入是有充分理由的 :)

【讨论】:

你提出了好点;其主要功能是确保调用BeginProcess 的基础架构代码正确执行。 “一个好的程序员在穿过一条单行道之前会左右两边看。”我不会让信号量阻塞(我会以零超时调用WaitOne)并检查返回值以决定是否抛出。这将防止出现死锁情况。 这个答案有经验!【参考方案2】:

您可以使用 .NET Framework 2.0 附带的 Semaphore 类。

信号量的一个很好的用途是同步有限数量的资源。在您的情况下,您似乎拥有想要在消费者之间共享的资源,例如 Context

您可以创建一个信号量来管理以下资源:

var resourceManager = new Semaphore(0, 10);

然后在BeginProcess 方法中等待资源可用:

resourceManager.WaitOne();

最后使用 EndProcess 方法释放资源:

resourceManager.Release();

这是一个good blog,关于在像您这样的情况下使用信号量:

https://web.archive.org/web/20121207180440/http://www.dijksterhuis.org/using-semaphores-in-c/

【讨论】:

您提供的链接已损坏!【参考方案3】:

Interlocked 类可用于退出方法的线程安全解决方案,而不是在进行重入调用时进行阻塞。像 Vlad Gonchar 解决方案,但线程安全。


    private int refreshCount = 0;
    private void Refresh()
    
      if (Interlocked.Increment(ref refreshCount) != 1) return;
        
      try
      
        // do something here
      
      finally
      
        Interlocked.Decrement(ref refreshCount);
      
    

【讨论】:

【参考方案4】:

有一种非常简单的方法可以防止重入(在一个线程上):

private bool bRefresh = false;
private void Refresh()

  if (bRefresh) return;
  bRefresh = true;
  try
  
    // do something here
  
  finally
  
    bRefresh = false;
  

【讨论】:

这绝对不会阻止重入。 它不会阻止它,因为if (bRefresh) return;bRefresh = true; 不是原子操作,所以当bRefresh 为false 时,多个线程可以评估条件,跳过return 然后继续将其设置为 true 并完成剩下的工作。 @Mike 当然它确实 防止重新进入——它只是不是线程安全的。这两个概念完全不相关——访问同一资源的两个线程与重入无关。如果您需要防止来自多个线程的重入和同时访问,则需要结合这两种方法或使用同时考虑这两种方法的原语。

以上是关于防止重入并确保为某些操作获取锁的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

ReentrantLock可重入锁的原理及使用场景

常用多线程方法

常见分布式锁的基本实现

用 Go + Redis 实现分布式锁

显式锁

在 useEffect 挂钩中取消所有异步/等待任务以防止反应中的内存泄漏的正确方法是啥?