【问题标题】:Howto avoid concurrently calling a method without blocking?如何避免在不阻塞的情况下同时调用方法?
【发布时间】:2016-02-24 00:52:32
【问题描述】:

我想运行一个可能会运行几秒钟的清理任务。多个线程可以调用此任务,但我只想运行此任务一次。所有其他调用都应该跳过。

以下是我当前的实现,但我无法想象 .net 框架中没有更好的解决方案,从而减少代码行数。

    object taskLock;
    bool isRunning;

    void Task()
    {
        if (isRunning) return;

        try
        {
            lock (taskLock)
            {
                if (isRunning) return;
                isRunning = true;
            }
            // Perform the magic
        }
        finally
        {
            isRunning = false;
        }
    }

【问题讨论】:

  • 您可以使用类似的方法,但使用并发字典代替 taskLock 对象。 tbh - 如果它目前有效,请继续并分解积压中的其他项目
  • "我无法想象没有更好的解决方案" 这是一个非正统的问题,因为根据定义它充满了并发歧义。根据时间的不同,任务可能只运行一次 - 或针对每个调用线程。至少,它是低效的。
  • 是的,它可能非常低效,但我没有看到更好的替代信号来发出特定任务的信号。另一种方法可能是使用重置事件,其中任务只是等待直到另一个线程设置事件,但我必须管理一个线程/任务。

标签: c# multithreading synchronization locking nonblocking


【解决方案1】:

是的,有更好的解决方案。可以使用Interlocked.CompareExchange,代码变得更简单无锁:

class Worker
{
    private volatile int isRunning = 0;

    public void DoWork()
    {
        if (isRunning == 0 && Interlocked.CompareExchange(ref isRunning, 1, 0) == 0)
        {
            try
            {
                DoTheMagic();
            }
            finally
            {
                isRunning = 0;
            }
        }
    }

    private void DoTheMagic()
    {
        // do something interesting
    }
}

在这种情况下,Interlocked.CompareExchange 执行以下原子操作(伪代码):

wasRunning = isRunning;
if isRunning = 0 then 
     isRunning = 1
end if
return wasRunning

来自 MSDN 文档:

public static int CompareExchange(
    ref int location1,
    int value,
    int comparand
)

如果comparand和location1中的值相等,那么值是 存储在位置 1。否则,不执行任何操作。比较 和交换操作作为原子操作执行。这 CompareExchange 的返回值是 location1 中的原始值, 交换是否发生

【讨论】:

  • 我喜欢这个解决方案,但 CompareExchange 不是比 if (isRunning) return; 慢吗?当任务更频繁地运行而不是不运行时,也许执行if (isRunning == 1 || Interlocked.CompareExchange(ref isRunning, 1, 0) == 1) return; 可能会更快
  • @RamonSmits,是的,它会更快,但它应该是 if (isRunning == 0 && Interlocked.CompareExchange(ref isRunning, 1, 0) == 0),您需要将 isRunning 声明为 volatile。另一方面,Interlocked.CompareExchange 非常快。我不相信你会获得可观的收益。
  • @RamonSmits。我编辑了我的答案以包括优化。这里有一个微基准比较优化gist.github.com/jesuslpm/77b60126c8d787c7a76b
  • 为什么需要'volatile'关键字?
  • @RamonSmits 因为它可以被另一个线程修改。它可以防止编译器执行一些可能导致错误结果的优化。与内存栅栏有关en.wikipedia.org/wiki/Memory_barrier
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-09-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-27
  • 2011-09-01
  • 1970-01-01
相关资源
最近更新 更多