使整个方法线程安全的最简单方法是什么?

12

多线程编程似乎需要学习的东西很多,让人有点不知所措。

就目前而言,我只是想防止在方法完成之前来自另一个线程的再次调用,并且我的问题是:

这种方式足以使方法变得线程安全吗?

class Foo
{
    bool doingWork;
    void DoWork()
    {
        if (doingWork)  // <- sophistocated thread-safety
            return;     // <-

        doingWork = true;

        try
        {
            [do work here]
        }
        finally
        {
            doingWork = false;
        }
    }
}
如果这不够用,那么最简单的实现方式是什么?编辑:更多关于场景的信息:1)只有一个 Foo 实例;2)Foo.DoWork() 将从 System.Timers.Timer 的 Elapsed 事件中的 ThreadPool 线程调用;3)通常情况下,Foo.DoWork() 完成的时间比下一次调用它的时间早得多,但我想编写代码应对极小的可能性,即在完成之前再次调用。 (我也不确定这个问题是否可以标记为语言无关,所以我没有这样做。如果适用,请随意这样做。)

每个线程是否实例化一个类型为Foo的对象,还是它在多个线程之间共享? - NotMe
关于调用dowork方法的代码,是否有多个线程?请提供更多细节。 - sll
要么支持可重入性,要么设计你的代码使其不会发生。当它发生时,仅仅退出可能不是正确的解决方案。但是你的编辑明确表明线程安全实际上是问题,而不是可重入性。 - David Heffernan
4个回答

14

你的代码不是线程安全的。你应该使用 lock 关键字。

在你当前的代码中:

  if (doingWork)
        return;

  // A thread having entered the function was suspended here by the scheduler.

  doingWork = true;
当下一个线程进入时,它也会进入该函数。 这就是为什么应该使用“锁”构造的原因。它基本上与您的代码执行相同,但没有线程在中间被中断的风险。
class Foo
{
    object lockObject = new object;
    void DoWork()
    {
        lock(lockObject)
        {
            [do work here]
        }
    }
}
请注意,此代码的语义与您原来的代码有些不同。此代码将导致第二个进入的线程等待并执行工作。您原来的代码只是使第二个线程中止。要接近您的原始代码,不能使用C#中的lock语句,必须直接使用底层的Monitor构造:
class Foo
{
    object lockObject = new object;
    void DoWork()
    {
        if(Monitor.TryEnter(lockObject))
        {
            try
            {
                [do work here]
            }
            finally
            {
                Monitor.Exit(lockObject);
            }
        }
    }
}

我其实不需要中止,所以锁定方法应该没问题。太好了 - 这很简单!谢谢。 - Igby Largeman
Valentin在这里提供了一种只使用lock()实现中止的方法,期待您的评论。 - Igby Largeman

3

“Re-entrancy”与多线程无关。

可重入方法是指一种可能会从同一个线程中再次被调用的方法。
例如,如果一个方法触发了一个事件,并且处理该事件的客户端代码在事件处理程序内部再次调用该方法,则该方法是可重入的。
防止该方法出现重入意味着确保如果您从它内部调用它自身,它要么不做任何事情,要么抛出异常。

只要一切都在同一个线程上,同一对象实例内的代码就受到重入的保护。

除非[在此执行操作]能够运行外部代码(例如通过引发事件或从其他地方调用委托或方法),否则它首先就不是可重入的。

您修改后的问题表明您不需要这整个部分。
尽管如此,您还是应该阅读一下。


您可能正在(编辑:确实)寻找排他性 - 确保如果由多个线程同时调用该方法,它不会同时运行两次。
您的代码并不具有独占性。如果两个线程同时运行该方法,并且它们同时运行if语句,则它们都可以绕过if,然后都设置doingWork标志,并且都运行整个方法。

要实现独占性,请使用lock关键字。


好的,确实在标签中提到了多线程,所以只能做出这种推断。 - Jesus Ramos
1
只能假设什么? 这个问题与多线程无关。编辑:现在,有关多线程的内容已经加入了。 - SLaks
不可否认,我只是在考虑另一个线程再次调用该方法。感谢您指出这一点 - 我已编辑问题以澄清这一点。但是我相信这个术语也适用于多线程场景。如果不行,当一个方法需要能够在其已经执行时被另一个线程调用时,你会怎么称呼它? - Igby Largeman
知道了。我的困惑来自于在 MSDN 中阅读到,如果我不防止我描述的情况,我的方法应该是“可重入”的。然而,现在我明白了这个区别。谢谢! - Igby Largeman
  • 问题已编辑以更正术语。希望现在更正确了。
- Igby Largeman

2

如果你希望编写简单的代码,且不太关心其性能,可以像这样轻松实现:

class Foo
{
    bool doingWork;
object m_lock=new object();
    void DoWork()
    {
        lock(m_lock) // <- not sophistocated multithread protection
{
        if (doingWork)  
            return;     
         doingWork = true;
}


        try
        {
            [do work here]
        }
        finally
        {
lock(m_lock) //<- not sophistocated multithread protection
{
            doingWork = false;
}
        }
    }

如果您想将锁定封装一下,可以创建一个线程安全的属性,如下所示:

public bool DoingWork
{
get{ lock(m_Lock){ return doingWork;}}
set{lock(m_lock){doingWork=value;}}
}

现在您可以使用它替代字段,但这将导致锁定时间增加,因为锁使用次数增加。
或者您可以采用全围栏方法(来自于伟大的线程书Joseph Albahari在线提供的线程)。
class Foo
{
  int _answer;
  bool _complete;

  void A()
  {
    _answer = 123;
    Thread.MemoryBarrier();    // Barrier 1
    _complete = true;
    Thread.MemoryBarrier();    // Barrier 2
  }

  void B()
  {
    Thread.MemoryBarrier();    // Barrier 3
    if (_complete)
    {
      Thread.MemoryBarrier();       // Barrier 4
      Console.WriteLine (_answer);
    }
  }
}

他说完整的栅栏比锁语句快2倍。在某些情况下,您可以通过删除不必要的MemoryBarrier()调用来提高性能,但使用lock更简单、更清晰、更少出错。
我认为这也可以通过在基于int的doingWork字段周围使用Interlocked类来完成。

你的lock()方法(锁定对doingWork的访问)和Anders的lock()方法(锁定执行工作的代码的访问)之间有什么显著的区别吗? - Igby Largeman
我可以看出他的方法是可重入的,并且进一步的调用被排队了。在我的代码中,我假设如果该方法正在一个线程上运行,您不希望允许其他线程排队等待。根据我的经验,我的方法通常更受欢迎,例如当您不希望用户通过双击按钮启动多个服务器请求时。顺便说一下,他说不能使用锁来完成的地方,我使用锁来完成。Monitor.TryEnter可能具有更好的性能,但对于日常需求来说稍微有些高级,我想我会将其留给关键性能代码。 - Valentin Kuzub
这个解决方案比我使用Monitor.TryEnter的第二段代码片段要简单。它的性能较差,因为它需要两个锁,但除非你的应用程序非常线程密集,否则这并不重要。 - Anders Abel
障碍和栅栏是真正专家用的,我们其余人使用锁并对正确性充满信心。 - David Heffernan
了解工具集在任务性能关键时可能很重要,但很难不同意。即使像上面的线程安全属性这样慢的解决方案对于日常使用也可以很好。 - Valentin Kuzub
幸运的是,在这种情况下性能不是问题,但非中止方法仍然最适合。但我有另一种情况,如果标志为真,我确实需要退出,因此我认为我会将这种方法(顶部示例)调整为该情况。 - Igby Largeman

0

那似乎对我的需求过于复杂了。我真的只想实现我的代码片段所暗示的:重新进入时跳出。 - Igby Largeman
检查屏障中的参与者数量,如果大于0则返回,否则添加一个参与者并进行工作,在最后移除参与者。屏障应该是线程安全的。 - Jesus Ramos
你能以原子方式检查和添加参与者吗?如果可以,该怎么做? - David Heffernan
屏障默认应该是原子性的。 - Jesus Ramos
获取底层锁定的方法操作是原子性的...因为C#字段可以被封装在属性中,从而允许原子访问,所以读取ParticipantCount也应该是原子性的... - Jesus Ramos
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接