11

似乎有很多关于多线程编程的知识要学习,而且有点吓人。

对于我目前的需求,我只想防止在另一个线程完成之前再次调用一个方法,我的问题是:

这是使方法线程安全的适当(安全)方法吗?

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

        doingWork = true;

        try
        {
            [do work here]
        }
        finally
        {
            doingWork = false;
        }
    }
}

如果这还不够,那么实现这一目标的最简单方法是什么?


编辑:有关该场景的更多信息:

  • Foo 只有一个实例

  • Foo.DoWork() 将在 System.Timers.Timer 的 Elapsed 事件上从 ThreadPool 线程调用。

  • 通常 Foo.DoWork() 将在下一次调用之前完成 eons,但我想编写代码以防止它运行很长时间,并在完成之前再次被调用。


(我也不够聪明,无法确定这个问题是否可以标记为与语言无关,所以我没有。开明的读者,如果适用,请随时这样做。)

4

4 回答 4

13

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

在您当前的代码中:

  if (doingWork)
        return;

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

  doingWork = true;

当下一个线程通过时,它也会进入该函数。

这就是lock应该使用构造的原因。它基本上与您的代码相同,但没有线程在中间被中断的风险:

class Foo
{
    object lockObject = new object;
    void DoWork()
    {
        lock(lockObject)
        {
            [do work here]
        }
    }
}

请注意,此代码的语义与您的原始代码有些不同。此代码将导致进入的第二个线程等待然后执行工作。您的原始代码使第二个线程中止。为了更接近您的原始代码,lock不能使用 C# 语句。Monitor必须直接使用底层结构:

class Foo
{
    object lockObject = new object;
    void DoWork()
    {
        if(Monitor.TryEnter(lockObject))
        {
            try
            {
                [do work here]
            }
            finally
            {
                Monitor.Exit(lockObject);
            }
        }
    }
}
于 2011-07-29T22:13:36.123 回答
3

重入与多线程无关。

可重入方法是一种最终可以在同一线程上从自身内部调用的方法。
例如,如果一个方法引发了一个事件,并且处理该事件的客户端代码在事件处理程序内再次调用该方法,则该方法是可重入的。
保护该方法免于重入意味着确保如果您从内部调用它,它不会执行任何操作或抛出异常。

只要所有内容都在同一个线程上,您的代码就不会在同一个对象实例中重新进入。

除非[do work here]能够运行外部代码(例如,通过引发事件,或通过从其他事物调用委托或方法),否则它一开始就不是可重入的。

您编辑的问题表明这整个部分与您无关。
无论如何,您可能应该阅读它。


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

为此,请使用lock关键字。

于 2011-07-29T22:04:08.360 回答
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;}}
}

现在您可以使用它来代替 field ,但是它会导致花费更多的时间来锁定,导致锁定使用次数增加。

或者您可以使用完整的围栏方法(来自伟大的线程书籍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);
    }
  }
}

他指出,full fence 比 lock 语句快 2 倍。在某些情况下,您可以通过删除对 MemoryBarrier() 的不必要调用来提高性能,但使用lock起来更简单、更清晰且不易出错。

我相信这也可以使用Interlocked基于 int 的doingWork 字段的类来完成。

于 2011-07-29T22:33:42.620 回答
0

http://msdn.microsoft.com/en-us/library/system.threading.barrier.aspx

可能想考虑使用屏障,它会为您完成所有工作。这是控制可重入代码的标准方法。还允许您控制一次执行该工作的线程数量(如果您允许超过 1 个)。

于 2011-07-29T22:02:26.873 回答