有没有将关键部分转换为一个或多个信号量的一般方法?

时间:2013-04-30 19:14:26

标签: c# optimization concurrency locking semaphore

是否有将关键部分转换为一个或多个信号量的一般方法?也就是说,是否可以通过某种直接的代码转换来转换它们?

例如,如果我有两个线程正在执行受保护和不受保护的工作,如下所示。我可以将它们转换为可以发信号,清除和等待的信号量吗?

void AThread()
{
  lock (this)
  {
    Do Protected Work
  }

  Do Unprotected work.
}

在考虑C#的lock()语句之后,我想到了这个问题,以及我是否可以用EventWaitHandle实现等效的功能。

3 个答案:

答案 0 :(得分:2)

是的,有一种通用方法可以将lock部分转换为使用Semaphore,使用与try...finally相同的lock块,并使用{{ 1}},最大计数为1,初始化为计数1.

编辑(5月11日) recent research向我展示了我对try ...最终等效的参考已经过时了。因此,下面的代码示例需要相应调整。 (结束编辑)

Semaphore

然而,你永远不会这样做。 private readonly Semaphore semLock = new Semaphore(1, 1); void AThread() { semLock.WaitOne(); try { // Protected code } finally { semLock.Release(); } // Unprotected code }

  • 用于一次限制对单个线程的资源访问,
  • 表达了不能通过多个线程同时访问该部分中的资源的意图

相反lock

  • 旨在通过限制并发访问来控制对资源池的同时访问。
  • 传达了可以通过最大线程数访问的资源池或者可以释放多个线程的控制线程的意图,以便在它准备就绪时执行某些工作。
  • 最大计数为1的
  • 执行速度比锁定慢。
  • 可以由任何线程发布,而不仅仅是进入该部分的线程(在编辑中添加

修改:您还在问题末尾提到Semaphore。值得注意的是EventWaitHandleSemaphore,而不是WaitHandle,也来自MSDN documentation for EventWaitHandle.Set

  

无法保证每次调用Set方法都会从EventWaitHandle释放一个线程,其复位模式为EventResetMode.AutoReset。如果两个调用太靠近,那么第二次调用在线程释放之前发生,则只释放一个线程。就好像第二次通话没有发生一样。

详情

你问:

  

是否有将关键部分转换为一个或多个信号量的一般方法?也就是说,是否可以通过某种直接的代码转换来转换它们?

鉴于:

EventWaitHandle

等同于(参见下面的参考资料和相关说明)

**编辑:(5月11日)根据上述评论,此代码示例需要在使用前根据this link进行调整

    lock (this) {
        //  Do protected work
    }
    //Do unprotected work

您可以通过以下方式使用 Monitor.Enter(this); try { // Protected code } finally { Monitor.Exit(this); } // Unprotected code 实现相同目的:

Semaphore

您还问:

  

例如,如果我有两个线程正在执行受保护和不受保护的工作,如下所示。我可以将它们转换为可以发信号,清除和等待的信号量吗?

这是我难以理解的问题,所以我道歉。在您的示例中,您将方法命名为AThread。对我来说,它并不是真正的AThread,它是AMethodToBeRunByManyThreads !!

    private readonly Semaphore semLock = new Semaphore(1, 1);
    void AThread()
    {
        semLock.WaitOne();
        try {
            // Protected code
        }
        finally {
            semLock.Release();
        }
        // Unprotected code
    }

因此 private readonly Semaphore semLock = new Semaphore(1, 1); void MainMethod() { Thread t1 = new Thread(AMethodToBeRunByManyThreads); Thread t2 = new Thread(AMethodToBeRunByManyThreads); t1.Start(); t2.Start(); // Now wait for them to finish - but how? } void AMethodToBeRunByManyThreads() { ... } 会保护您受保护的代码",但semLock = new Semaphore(1, 1);更适合该用途。区别在于信号量允许第三个线程参与:

lock

但是,在.NET 4.5中,您将使用Tasks来执行此操作并控制主线程同步。


以下是一些想法:

lock(x)和Monitor.Enter - 等效

上述关于等价的陈述并不十分准确。事实上:

  

" [lock]完全等同于[Monitor.Enter try ... finally] ,除了x只评估一次 [通过锁定]"   (参考:C# Language Specification

这是次要的,对我们来说可能并不重要。

您可能必须小心内存障碍,并增加类似计数器的字段,因此如果您使用的是信号量,则可能仍需要锁定,或者如果您有信心使用它,则可以联锁。

小心锁(此)和死锁

我的原始资料来源是Jeffrey Richter的文章" Safe Thread Synchronization"。那和一般的最佳实践:

  • 不要锁定 private readonly Semaphore semLock = new Semaphore(0, 2); private readonly object _lockObject = new object(); private int counter = 0; void MainMethod() { Thread t1 = new Thread(AMethodToBeRunByManyThreads); Thread t2 = new Thread(AMethodToBeRunByManyThreads); t1.Start(); t2.Start(); // Now wait for them to finish semLock.WaitOne(); semLock.WaitOne(); lock (_lockObject) { // uses lock to enforce a memory barrier to ensure we read the right value of counter Console.WriteLine("done: {0}", counter); } } void AMethodToBeRunByManyThreads() { lock (_lockObject) { counter++; Console.WriteLine("one"); Thread.Sleep(1000); } semLock.Release(); } ,而是在班级实例化中创建一个this字段(不要使用值类型,因为它会被装箱)
  • object字段设为只读(个人偏好 - 但它不仅传达意图,还可以防止其他代码贡献者更改锁定对象等。)

影响很多,但是为了让团队更容易工作,遵循封装的最佳实践并避免难以检测的令人讨厌的边缘情况错误,最好遵循上述规则。

您的原始代码将成为:

object

(注意:通常,Visual Studio会使用SyncRoot作为锁定对象名称来帮助您处理其片段)

信号量和锁定用于不同用途

private readonly object m_lockObject = new object(); void AThread() { lock (m_lockObject) { // Do protected work } //Do unprotected work } 授予线程在"就绪队列中的位置"以FIFO为基础(ref. Threading in C# - Joseph Albahari, part 2: Basic Synchronization, Section: Locking)。当有人看到lock时,他们知道通常在该部分内部是一个共享资源,例如类字段,一次只能被一个线程更改。

信号量是一段代码的非FIFO控件。它非常适合发布者 - 订阅者(线程间通信)场景。围绕不同线程的自由能够将信号量释放到获取信号量的信号量非常强大。在语义上它并不一定说"只有一个线程访问本节内的资源",与lock不同。

示例:要增加某个类的计数器,您可以使用lock,但不能使用lock

Semaphore

但是,只有在另一个线程表示可以这样做时才增加,你可以使用 lock (_lockObject) { counter++; } ,而不是Semaphore,其中线程A在具有信号量部分后执行增量:

lock

当线程B准备好允许增量时,它会释放信号量:

    semLock.WaitOne();
    counter++;
    return;

(请注意,这是强制的,在该示例中,诸如ManualResetEvent之类的WaitHandle可能更合适。)

<强>性能

从性能的角度来看,在一个小型多线程虚拟机上运行下面的简单程序,虽然时间尺度仍然非常快,并且足以满足所有高吞吐量软件的需求,但锁定会胜过信号量。请注意,在使用两个并行线程访问锁定运行测试时,此排名大致相同。

小型VM上的滴答100次迭代的时间(越小越好):

  • 291.334(Semaphore)
  • 44.075(SemaphoreSlim)
  • 4.510(Monitor.Enter)
  • 6.991(锁定)

每毫秒刻度:10000

    // when I'm ready in thread B
    semLock.Release();

答案 1 :(得分:1)

很难确定你在这里要求的内容。

如果你只是想要一些可以等待的东西,你可以使用Monitor,这是lock使用的内容。也就是说,上面的lock序列扩展为:

void AThread()
{
    Monitor.Enter(this);
    try
    {
        // Do protected work
    }
    finally
    {
        Monitor.Exit(this);
    }
    // Do unprotected work
}

顺便说一句,lock (this)通常是not a good idea。您最好创建一个锁定对象:

private object _lockObject = new object();

现在,如果你想有条件地获得锁,你可以使用`Monitor.TryEnter:

if (Monitor.TryEnter(_lockObject))
{
    try
    {
        // Do protected work
    }
    finally
    {
        Monitor.Exit(_lockObject);
    }
 }

如果您想等待超时,请使用TryEnter重载:

if (Monitor.TryEnter(_lockObject, 5000))  // waits for up to 5 seconds

如果获得锁定,则返回值为true

互斥锁与EventWaitHandleSemaphore根本不同,因为只有获取互斥锁的线程才能释放它。 任何线程都可以设置或清除WaitHandle任何线程都可以释放Semaphore

我希望能回答你的问题。如果没有,请修改您的问题,以便向我们提供有关您所要求的更多详细信息。

答案 2 :(得分:0)

您应该考虑查看Wintellect Power Threading librarieshttps://github.com/Wintellect/PowerThreading

这些库所做的事情之一就是创建通用抽象,允许交换线程原语。

这意味着在1或2台处理器机器上,您可以看到很少的争用,您可以使用标准锁。一个4或8处理器机器,其中争用很常见,也许读取器/写入器锁更正确。如果使用ResourceLock等原语,则可以换出:

  • 旋转锁
  • 监视器
  • Mutex
  • 读者作家
  • OPTEX
  • 信号量
  • ......和其他人

我编写的代码基于处理器的数量动态地根据可能存在的争用量选择特定的锁。使用该库中的结构,这是可行的。