0

有没有一种通用的方法可以将临界区转换为一个或多个信号量?也就是说,是否可以对代码进行某种直接的转换来转换它们?

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

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

  Do Unprotected work.
}

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

4

3 回答 3

2

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

编辑(5 月 11 日) 最近的研究表明,我对 try ... finally 等价的参考已经过时了。因此,需要对下面的代码示例进行相应调整。(结束编辑)

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

但是,您永远不会这样做。lock

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

相反Semaphore

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

编辑:您还在EventWaitHandle问题末尾提到。值得注意的是,它Semaphore是一个WaitHandle,但不是一个EventWaitHandle,而且来自EventWaitHandle.Set 的 MSDN 文档

不能保证每次调用 Set 方法都会从重置模式为 EventResetMode.AutoReset 的 EventWaitHandle 中释放线程。如果两个调用靠得太近,以至于第二个调用发生在一个线程被释放之前,那么只有一个线程被释放。就好像第二次通话没有发生一样。

细节

您问:

有没有一种通用的方法可以将临界区转换为一个或多个信号量?也就是说,是否可以对代码进行某种直接的转换来转换它们?

鉴于:

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

等同于(参见下面的参考和注释)到

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

    Monitor.Enter(this);
    try {
        // Protected code
    }
    finally {
        Monitor.Exit(this);
    }
    // Unprotected code

您可以Semaphore通过执行以下操作来实现相同的目的:

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

你还问:

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

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

    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更适合该用途。不同之处在于信号量将允许第三个线程参与:

    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();
    }

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


这里有一些想法:

lock(x) 和 Monitor.Enter - 等价

上述关于等价的说法并不十分准确。实际上:

“[lock] 完全等同于 [to Monitor.Enter try ... finally],除了 x 只评估一次[by lock]”(参考:C# Language Specification

这是次要的,可能对我们无关紧要。

您可能必须小心内存屏障和递增计数器类字段,因此如果您使用 Semaphore,您可能仍需要锁定,如果您有信心使用它,则可能需要 Interlocked。

当心锁(这个)和死锁

我最初的来源是 Jeffrey Richter 的文章“安全线程同步”。那和一般的最佳实践:

  • 不要 lock this,而是object在类实例化时在类中创建一个字段(不要使用值类型,因为它无论如何都会被装箱)
  • 使该object字段只读(个人偏好 - 但它不仅传达意图,还防止您的锁定对象被其他代码贡献者等更改)

含义很多,但为了使团队工作更轻松,遵循封装的最佳实践并避免测试难以检测到的令人讨厌的边缘情况错误,最好遵循上述规则。

因此,您的原始代码将变为:

    private readonly object m_lockObject = new object();
    void AThread()
    {
        lock (m_lockObject) {
            //  Do protected work
        }
        //Do unprotected work
    }

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

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

lock在 FIFO 基础上授予线程在“就绪队列”中的一个位置(参考 C# 中的线程 - Joseph Albahari,第 2 部分:基本同步,部分:锁定)。当任何人看到lock时,他们知道通常在该部分内部是共享资源,例如类字段,一次只能由单个线程更改。

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

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

    lock (_lockObject) {
        counter++;
    }

但是要仅在另一个线程表示可以这样做时才增加它,您可以使用 a Semaphore,而不是 a lock,其中线程 A 在具有 Semaphore 部分后进行增量:。

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

线程 B 在准备好允许增量时释放信号量:

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

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

表现

从性能的角度来看,在小型多线程 VM 上运行下面的简单程序,锁在很长一段时间内胜过 Semaphore,尽管时间尺度仍然非常快,并且对于除高吞吐量软件之外的所有软件来说已经足够了。请注意,在两个并行线程访问锁的情况下运行测试时,此排名大致相同。

在小型 VM 上进行 100 次迭代的时间(越小越好):

  • 291.334(信号量)
  • 44.075 (SemaphoreSlim)
  • 4.510(监视器。输入)
  • 6.991(锁定)

每毫秒滴答声:10000

class Program
{
    static void Main(string[] args)
    {
        Program p = new Program();
        Console.WriteLine("100 iterations in ticks");
        p.TimeMethod("Semaphore", p.AThreadSemaphore);
        p.TimeMethod("SemaphoreSlim", p.AThreadSemaphoreSlim);
        p.TimeMethod("Monitor.Enter", p.AThreadMonitorEnter);
        p.TimeMethod("Lock", p.AThreadLock);
        Console.WriteLine("Ticks per millisecond: {0}", TimeSpan.TicksPerMillisecond);
    }

    private readonly Semaphore semLock = new Semaphore(1, 1);
    private readonly SemaphoreSlim semSlimLock = new SemaphoreSlim(1, 1);
    private readonly object _lockObject = new object();
    const int Iterations = (int)1E6;
    int sharedResource = 0;

    void TimeMethod(string description, Action a)
    {
        sharedResource = 0;
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < Iterations; i++)
        {
            a();
        }
        sw.Stop();
        Console.WriteLine("{0:0.000} ({1})", (double)sw.ElapsedTicks * 100d / (double)Iterations, description);
    }

    void TimeMethod2Threads(string description, Action a)
    {
        sharedResource = 0;
        Stopwatch sw = new Stopwatch();
        using (Task t1 = new Task(() => IterateAction(a, Iterations / 2)))
        using (Task t2 = new Task(() => IterateAction(a, Iterations / 2)))
        {
            sw.Start();
            t1.Start();
            t2.Start();
            Task.WaitAll(t1, t2);
            sw.Stop();
        }
        Console.WriteLine("{0:0.000} ({1})", (double)sw.ElapsedTicks * (double)100 / (double)Iterations, description);
    }

    private static void IterateAction(Action a, int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            a();
        }
    }

    void AThreadSemaphore()
    {
        semLock.WaitOne();
        try {
            sharedResource++;
        }
        finally {
            semLock.Release();
        }
    }
    void AThreadSemaphoreSlim()
    {
        semSlimLock.Wait();
        try
        {
            sharedResource++;
        }
        finally
        {
            semSlimLock.Release();
        }
    }
    void AThreadMonitorEnter()
    {
        Monitor.Enter(_lockObject);
        try
        {
            sharedResource++;
        }
        finally
        {
            Monitor.Exit(_lockObject);
        }
    }
    void AThreadLock()
    {
        lock (_lockObject)
        {
            sharedResource++;
        }
    }
}
于 2013-05-06T20:08:23.090 回答
1

很难确定你在这里要求什么。

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

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

顺便说一句,lock (this)一般不是一个好主意。你最好创建一个锁对象:

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是否获得了锁。

EventWaitHandle互斥锁与or的根本不同Semaphore在于,只有获得互斥锁的线程才能释放它。任何线程都可以设置或清除a WaitHandle任何线程都可以释放a Semaphore

我希望这能回答你的问题。如果没有,请编辑您的问题,向我们提供有关您所要求内容的更多详细信息。

于 2013-05-01T13:18:43.163 回答
0

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

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

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

  • 自旋锁
  • 监视器
  • 互斥体
  • 读者作家
  • 光学
  • 信号
  • ... 和别的

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

于 2013-05-06T22:13:01.530 回答