是的,有一种通用方法可以将lock
部分转换为使用 a ,使用与aSemaphore
相同的try...finally
块,最大计数为 1,初始化为计数 1。lock
Semaphore
编辑(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++;
}
}
}