【问题标题】:.NET Concurrency Question: Can I yield a semaphore to another thread.NET 并发问题:我可以向另一个线程产生信号量吗
【发布时间】:2010-03-06 02:59:35
【问题描述】:

我有多个共享信号量的线程。线程 A 持有信号量(使用锁),线程 B 和 C 正在等待同一个信号量(也使用锁)。线程共享全局变量等。

在 C# 中有可以用来关闭线程 B 的技术吗?我可以在 A 中设置一个标志,让线程 B 检查该标志并在它控制信号量后立即退出,但我不知道有任何技术可以让线程 A 将信号量交给线程 B(并得到它当线程 B 退出时返回),没有线程 C 夺取控制权的风险。

有人对如何解决这个设计问题有任何建议吗?如果我处理不正确,我可以根据需要重写程序。

[编辑] 一位评论者指出我使用了错误的术语。评论者是正确的 - 我使用的是临界区,但鉴于一切都在单个进程中运行,在此示例中临界区在功能上等同于更通用的术语“信号量”。

[编辑] 有人询问更多细节,所以在这里。

可以有多个线程执行代码 A。只有一个线程执行代码 B。

代码 A:

private static Thread workerThread = null;

lock (lockObject)
{
    ... do some work ...

    if (...condition...)
    {
        if (workerThread != null)
        {
            // Kill the worker thread and continue only after it is dead.
            quitWorkerThread = true;
            // Wait for the thread to die.
            while (workerThread.IsAlive)
            {
                Thread.Sleep(50);
            }
            workerThread = null;
            quitWorkerThread = false;
        } // if (workerThread != null)
    } // if (...condition...)

    ... do some more work ...

    if (...condition...)
    {
        if (workerThread == null)
        {
            // Start the worker thread.
            workerThread = new Thread(WorkerThread);
            workerThread.Start();
        } // if (workerThread == null)
    } // if (...condition...)

    ... do even more work ...

} // lock (lockObject)

代码 B:

private void WorkerThread()
{
    while (true)
    {
        if (quitWorkerThread)
        {
            return;
        }

        Thread.Sleep (2000);

        if (quitWorkerThread)
        {
            return;
        }

        lock(lockObject)
        {
            if (quitWorkerThread)
            {
                return;
            }
            ... do some work ...
        } // lock(lockObject)
    } // while (true)
} // WorkerThread

我怀疑我会使用 Aaron 解决方案的变体。我主要希望有一些更优雅的解决方案可用,但我怀疑就像这个项目的其他一切一样,这都是蛮力和极端情况:-(。

【问题讨论】:

  • “线程 A 持有信号量(使用锁)” - lock 语句是关键部分。您使用的是信号量还是临界区(锁)?
  • 如果你使用信号量,你可能会使用 waitOne()。
  • 仅供参考,关键部分与信号量完全不同,即使在单个进程中也是如此。临界区恰好允许 一个 线程持有资源;一个信号量可能允许多个。锁定语义也不同;一个线程可以多次重新进入它自己的临界区,但是信号量并不关心线程标识,所以重新获取它太多次会死锁。对不起,迂腐,但区别非常重要。
  • 感谢您的澄清,Aaronnaught。无需为迂腐道歉,这是计算机科学,魔鬼藏在细节中,一切都是魔鬼。
  • 我认为术语差异存在于一般 comp sci 用法和 .NET 特定用法之间。在 .NET 中,lock 关键字等同于 Monitor.Enter(lockObject);try{}finally {Monitor.Exit(lockObject);},因此与其说持有信号量,不如说是(获取然后)持有一个监视器或锁。在 .NET 中,“信号量”是指 Win32 事件句柄的特定托管包装器,它可以由多个单独的线程持有,这些线程等于在构造时指定的 int 值。然而,一般而言,CS“信号量”是“二进制”(如 .NET 监视器)或“计数”(如 .NET 信号量)。

标签: c# concurrency semaphore


【解决方案1】:

我相当肯定没有办法将控制权交给 特定 线程,这似乎是您想要做的。您只能让步 - 由 Windows 调度程序决定接下来运行哪个线程。

情况是您有三个线程,A、B 和 C。A 有锁,B 和 C 正在等待它,并且您想要一种方法来保证 B 得到下一个执行。

显而易见的解决方案是使用多个锁和/或同步原语。您可以将lock 的语义与ManualResetEvent 结合起来。让线程 C 等待事件临界区,而线程 B 只需要等待临界区。在正常情况下,您在释放锁之前发出事件信号,这让操作系统决定执行哪个线程。在特殊情况下,您根本不发出事件信号,让线程 B 执行,而 C 仍被阻塞。

一旦 B 完成,然后您发出信号让 C 完成。


一个(未经测试的)示例是:

// Main thread is Thread A
object myLock = new Object();
AutoResetEvent myEvent = new AutoResetEvent(false);
ManualResetEvent completedEvent = new ManualResetEvent(false);

ThreadPool.QueueUserWorkItem(s =>
{
    for (int i = 0; i < 10000; i++)
    {
        lock (myLock)
        {
            // Do some work
        }
    }
    completedEvent.Set();
});  // Thread B

ThreadPool.QueueUserWorkItem(s =>
{
    for (int i = 0; i < 10000; i++)
    {
        myEvent.WaitOne();
        lock (myLock)
        {
            // Do some work
        }
    }
});  // Thread C

// Main loop for thread A
while (true)
{
    lock (myLock)
    {
        // Do some work
        if (SomeSpecialCondition)
            break;
        else
            myEvent.Set();
    }
}

completedEvent.WaitOne(); // Wait for B to finish processing
if (SomeSpecialCondition) // If we terminated without signaling C...
    myEvent.Set();        // Now allow thread C to clean up

这实质上是让线程 A 负责线程 C 何时开始执行。线程 A 和 B 将正常竞争,但由线程 A 为线程 C 发出事件信号。

【讨论】:

  • 附注——其他人建议使用boolint 变量作为状态变量,这是一种有效的方法;我使用额外的同步原语的原因之一是因为它可以跨越实例边界,而检查bool 或使用Interlocked 只有在所有线程方法都在同一个类中时才有用。此外,不要被workCompleted 事件吓倒,这不是绝对必要的,如果您需要确保 C 在 after B 在最后阶段。
  • 感谢一位多伦多同胞的(令人难以置信的)及时和翔实的回答。非常感谢。难怪你有 >13K 点!
【解决方案2】:

看看使用 Monitor 和 Monitor.Pulse,这不会让您在屈服于特定线程时完全得到您想要的,但它可以让您在线程和关键部分之间转移控制。

我不清楚您要解决什么问题。您也可以使用ReaderWriterLockSlim 来解决您的问题。

最后,在我看来,您的场景是使用 .NET events 的合适场所。

【讨论】:

  • 我真的不明白 ReaderWriterLockSlim 或事件代表如何在这里申请,需要具体说明吗?
  • 这完全取决于要解决的问题。我的想法是,如果问题是同步访问某些共享数据的问题,那么 ReaderWriterLock 可能会解决问题,而不是使用和释放锁、监视器等。如果实际问题是希望在发生某些事情时提醒各种对象,然后事件听起来像是要走的路。由于最初的问题似乎缺乏对正在解决的问题的描述,因此我试图涵盖所有基础。
  • 非常感谢您提供指向 Monitor.Pulse 的指针。这是一个我不熟悉的 .NET 特定并发结构。
【解决方案3】:

(免责声明:如果您唯一的用例是那个特定的用例,@Aaronaught 的简单使用 ManualResetEvents 的解决方案可能是最简单的。)

为附加免责声明编辑: 如果您想扩展此概念,请非常小心死锁。

如果在某些情况下,您可能希望 C 工作而不管 B 是否已完成工作,但如果 C 正在等待,则始终希望 B 先走,这里有一个简单的解决方案:

    object lockObject = new object();
    int WaitCounter = 0;

    void B()
    {
        System.Threading.Interlocked.Increment(ref WaitCounter);

        try
        {
            lock (lockObject)
            {
            }
        }
        finally
        {
            System.Threading.Interlocked.Decrement(ref WaitCounter);
        }
    }

    void C()
    {
        while (true)
        {
            // always attempt to yield to other threads first
            System.Threading.Thread.Sleep(0);
            lock (lockObject)
            {
                if (WaitCounter > 0)
                    continue;

                // ...

                return;
            }
        }
    }

一堆额外的代码,但从来没有人声称并发是容易的。 :)

【讨论】:

  • 当您在C 中获取WaitCounter 值时,您不应该使用Interlocked.Read 吗?
  • @Aaronaught:我认为 Int32 不需要,但我不是 100% 确定……实际上,这将是一个很好的新问题。 :p
  • @Aaronaught:……实际上已经有人问过了。 stackoverflow.com/questions/1186515/interlocked-and-volatile 看起来没有 Interlocked.Read 也没关系。
  • 感谢您的解决方案和代码。我不熟悉 System.Threading.Interlocked.Increment/Decrement(我很早就开始使用 .NET 编程),所以它是添加到我的工具箱中的另一个构造。非常感谢!
【解决方案4】:

Aaronaught 的解决方案看起来不错,但我认为更简单的解决方案是使用更共享的状态和单个锁。

基本上,您在线程之间传递控制权,线程决定它们是否该工作了。如果不是,他们简单的 PulseAll(将所有现有的等待线程移动到就绪队列中)并等待(被脉冲然后)再次获得锁。在某些时候,决定何时可以使用 ThreadC。

public class ThreadsGettinCrazy {
  static readonly _sync = new object();
  static bool _threadCReady = false;

  public void ThreadA() {
    while (true) {
      lock(_sync) {
        while(/* my condition not met */) {
          Monitor.PulseAll(_sync);
          Monitor.Wait(_sync);
        }
        // do work, possibly set _threadCReady = true
        Monitor.PulseAll(_sync);
      }
      if (/* i'm done */) break;
    }
  }

  public void ThreadB() {
    while (true) {
      lock(_sync) {
        while(/* my condition not met */) {
          Monitor.PulseAll(_sync);
          Monitor.Wait(_sync);
        }
        // do work, possibly set _threadCReady = true
        Monitor.PulseAll(_sync);
      }
      if (/* i'm done */) break;
    }
  }

  public void ThreadC() {
    lock(_sync) {
      while (!_threadCReady) {
        Monitor.PulseAll(_sync);
        Monitor.Wait(_sync);
      }
      // do work
    }
  }
}

【讨论】:

  • 非常感谢您提供的代码演示了 Monitor.Pulse 的使用。我对您的努力感到惊讶和感激,并且很高兴研究所有三种方法。无论我使用哪一种,它们都肯定具有教育意义。
猜你喜欢
  • 2015-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多