无锁、可等待、独占的访问方式
Lock-free, awaitable, exclusive access methods
我有一个线程安全的 class,它使用需要独占访问的特定资源。根据我的评估,让各种方法的调用者阻塞 Monitor.Enter
或等待 SemaphoreSlim
以访问此资源是没有意义的。
例如我有一些"expensive"异步初始化。由于多次初始化没有意义,无论是来自多个线程还是单个线程,多次调用应该 return 立即(甚至抛出异常)。相反,应该创建、初始化和 然后 将实例分发给多个线程。
更新 1:
MyClass
在任一方向使用两个 NamedPipes
。 InitBeforeDistribute
方法并不是真正的初始化,而是正确地建立双向连接。在建立连接之前让 N
线程可以使用管道是没有意义的。设置后,多个线程可以 post 工作,但实际上只有一个线程可以 read/write 到流。我很抱歉因为示例命名不当而混淆了这一点。
更新 2:
如果 InitBeforeDistribute
使用适当的 await 逻辑(而不是互锁操作抛出异常)实现了 SemaphoreSlim(1, 1)
,那么 Add/Do Square 方法是否可行?它不会抛出冗余异常(例如 InitBeforeDistribute
),同时是无锁的?
以下是一个好的坏的例子:
class MyClass
{
private int m_isIniting = 0; // exclusive access "lock"
private volatile bool vm_isInited = false; // vol. because other methods will read it
public async Task InitBeforeDistribute()
{
if (Interlocked.Exchange(ref this.m_isIniting, -1) != 0)
throw new InvalidOperationException(
"Cannot init concurrently! Did you distribute before init was finished?");
try
{
if (this.vm_isInited)
return;
await Task.Delay(5000) // init asynchronously
.ConfigureAwait(false);
this.vm_isInited = true;
}
finally
{
Interlocked.Exchange(ref this.m_isConnecting, 0);
}
}
}
几点:
- 如果有blocking/awaiting访问锁的情况
perfect sense,那么这个例子没有(make sense,就是)。
- 因为我需要在方法中等待,所以我必须使用类似
SemaphoreSlim 如果我在哪里使用 "proper" 锁。放弃
上面例子的信号量让我不用担心
完成后处理 class 。 (我一直不喜欢
处理多个线程使用的项目的想法。这是次要
积极的,肯定的。)
- 如果经常调用该方法,可能会有一些性能
好处,当然应该衡量。
上面的例子在参考文献中没有意义。到 (3.) 所以这是另一个例子:
class MyClass
{
private volatile bool vm_isInited = false; // see above example
private int m_isWorking = 0; // exclusive access "lock"
private readonly ConcurrentQueue<Tuple<int, TaskCompletionSource<int>> m_squareWork =
new ConcurrentQueue<Tuple<int, TaskCompletionSource<int>>();
public Task<int> AddSquare(int number)
{
if (!this.vm_isInited) // see above example
throw new InvalidOperationException(
"You forgot to init! Did you already distribute?");
var work = new Tuple<int, TaskCompletionSource<int>(number, new TaskCompletionSource<int>()
this.m_squareWork.Enqueue(work);
Task do = DoSquare();
return work.Item2.Task;
}
private async Task DoSquare()
{
if (Interlocked.Exchange(ref this.m_isWorking, -1) != 0)
return; // let someone else do the work for you
do
{
try
{
Tuple<int, TaskCompletionSource<int> work;
while (this.m_squareWork.TryDequeue(out work))
{
await Task.Delay(5000) // Limiting resource that can only be
.ConfigureAwait(false); // used by one thread at a time.
work.Item2.TrySetResult(work.Item1 * work.Item1);
}
}
finally
{
Interlocked.Exchange(ref this.m_isWorking, 0);
}
} while (this.m_squareWork.Count != 0 &&
Interlocked.Exchange(ref this.m_isWorking, -1) == 0)
}
}
我应该注意这个 "lock-free" 示例中的某些特定负面方面吗?
大多数与 SO 上的 "lock-free" 代码相关的问题通常都反对它,声明它是针对 "experts" 的。很少(我可能在这一点上是错的)我看到了可以深入研究的 books/blogs/etc 的建议,如果有人愿意的话。如果有任何我应该研究的此类资源,请分享。任何建议将不胜感激!
更新:相关的好文章
.: Creating High-Performance Locks and Lock-free Code (for .NET) :.
关于 lock-free
算法的要点不是它们适用于 experts
。
重点是Do you really need lock-free algorythm here?
我看不懂你的逻辑:
Since it does not make sense to initialize more than once, whether it be from multiple threads or a single one, multiple calls should return immediately (or even throw an exception).
为什么您的用户不能简单地等待初始化结果,然后再使用您的资源?如果可以,只需使用 Lazy<T>
class 甚至 Asynchronous Lazy Initialization
.
您真的应该阅读有关 consensus number and CAS-operations 的内容以及为什么它在实现您自己的同步原语时很重要。
在您的代码中,您使用的是 Interlocked.Exchange
方法,这实际上不是 CAS
,因为它 总是 交换值,并且它共识数等于 2
。这意味着使用这种构造的原语仅适用于 2
个线程(不是您的情况,但仍然 2
)。
我试图确定您的代码是否可以在 3
线程上正常工作,或者可能存在导致您的应用程序损坏的某些情况,但在 30
分钟后我停止了。在尝试理解您的代码一段时间后,您的任何团队成员都会像我一样停下来。这是在浪费时间,不仅是您的时间,也是您团队的时间。除非真的需要,否则不要重新发明轮子。
我在相关领域最喜欢的书是 Writing High-Performance .NET Code by Ben Watson, and my favorite blog is Stephen Cleary 的。如果你能具体说说你对什么样的书感兴趣,我可以再补充一些参考。
程序中没有锁不会使您的应用程序 lock-free
。在 .NET 应用程序中,您真的不应该将 Exceptions
用于内部程序流。考虑到初始化线程没有被 OS 安排一段时间(出于各种原因,无论它们到底是什么)。
在这种情况下,您应用中的所有其他线程都将在尝试访问您的共享资源时逐步死掉。我不能说这是 lock-free
代码。是的,里面没有锁,但是它不能保证程序的正确性,因此根据定义它不是无锁的。
Maurice Herlihy 和 Nir Shavit 的多处理器编程艺术,是无锁和无等待编程的重要资源。无锁是一种进度保证,而不是一种编程模式,因此要论证算法是无锁的,必须验证或证明进度保证。简单来说,无锁意味着一个线程的阻塞或停止不会阻塞其他线程的进程,或者如果一个线程被无限频繁地阻塞,那么另一个线程会无限频繁地进行。
我有一个线程安全的 class,它使用需要独占访问的特定资源。根据我的评估,让各种方法的调用者阻塞 Monitor.Enter
或等待 SemaphoreSlim
以访问此资源是没有意义的。
例如我有一些"expensive"异步初始化。由于多次初始化没有意义,无论是来自多个线程还是单个线程,多次调用应该 return 立即(甚至抛出异常)。相反,应该创建、初始化和 然后 将实例分发给多个线程。
更新 1:
MyClass
在任一方向使用两个 NamedPipes
。 InitBeforeDistribute
方法并不是真正的初始化,而是正确地建立双向连接。在建立连接之前让 N
线程可以使用管道是没有意义的。设置后,多个线程可以 post 工作,但实际上只有一个线程可以 read/write 到流。我很抱歉因为示例命名不当而混淆了这一点。
更新 2:
如果 InitBeforeDistribute
使用适当的 await 逻辑(而不是互锁操作抛出异常)实现了 SemaphoreSlim(1, 1)
,那么 Add/Do Square 方法是否可行?它不会抛出冗余异常(例如 InitBeforeDistribute
),同时是无锁的?
以下是一个好的坏的例子:
class MyClass
{
private int m_isIniting = 0; // exclusive access "lock"
private volatile bool vm_isInited = false; // vol. because other methods will read it
public async Task InitBeforeDistribute()
{
if (Interlocked.Exchange(ref this.m_isIniting, -1) != 0)
throw new InvalidOperationException(
"Cannot init concurrently! Did you distribute before init was finished?");
try
{
if (this.vm_isInited)
return;
await Task.Delay(5000) // init asynchronously
.ConfigureAwait(false);
this.vm_isInited = true;
}
finally
{
Interlocked.Exchange(ref this.m_isConnecting, 0);
}
}
}
几点:
- 如果有blocking/awaiting访问锁的情况 perfect sense,那么这个例子没有(make sense,就是)。
- 因为我需要在方法中等待,所以我必须使用类似 SemaphoreSlim 如果我在哪里使用 "proper" 锁。放弃 上面例子的信号量让我不用担心 完成后处理 class 。 (我一直不喜欢 处理多个线程使用的项目的想法。这是次要 积极的,肯定的。)
- 如果经常调用该方法,可能会有一些性能 好处,当然应该衡量。
上面的例子在参考文献中没有意义。到 (3.) 所以这是另一个例子:
class MyClass
{
private volatile bool vm_isInited = false; // see above example
private int m_isWorking = 0; // exclusive access "lock"
private readonly ConcurrentQueue<Tuple<int, TaskCompletionSource<int>> m_squareWork =
new ConcurrentQueue<Tuple<int, TaskCompletionSource<int>>();
public Task<int> AddSquare(int number)
{
if (!this.vm_isInited) // see above example
throw new InvalidOperationException(
"You forgot to init! Did you already distribute?");
var work = new Tuple<int, TaskCompletionSource<int>(number, new TaskCompletionSource<int>()
this.m_squareWork.Enqueue(work);
Task do = DoSquare();
return work.Item2.Task;
}
private async Task DoSquare()
{
if (Interlocked.Exchange(ref this.m_isWorking, -1) != 0)
return; // let someone else do the work for you
do
{
try
{
Tuple<int, TaskCompletionSource<int> work;
while (this.m_squareWork.TryDequeue(out work))
{
await Task.Delay(5000) // Limiting resource that can only be
.ConfigureAwait(false); // used by one thread at a time.
work.Item2.TrySetResult(work.Item1 * work.Item1);
}
}
finally
{
Interlocked.Exchange(ref this.m_isWorking, 0);
}
} while (this.m_squareWork.Count != 0 &&
Interlocked.Exchange(ref this.m_isWorking, -1) == 0)
}
}
我应该注意这个 "lock-free" 示例中的某些特定负面方面吗?
大多数与 SO 上的 "lock-free" 代码相关的问题通常都反对它,声明它是针对 "experts" 的。很少(我可能在这一点上是错的)我看到了可以深入研究的 books/blogs/etc 的建议,如果有人愿意的话。如果有任何我应该研究的此类资源,请分享。任何建议将不胜感激!
更新:相关的好文章
.: Creating High-Performance Locks and Lock-free Code (for .NET) :.
关于
lock-free
算法的要点不是它们适用于experts
。
重点是Do you really need lock-free algorythm here?
我看不懂你的逻辑:Since it does not make sense to initialize more than once, whether it be from multiple threads or a single one, multiple calls should return immediately (or even throw an exception).
为什么您的用户不能简单地等待初始化结果,然后再使用您的资源?如果可以,只需使用
Lazy<T>
class 甚至Asynchronous Lazy Initialization
.您真的应该阅读有关 consensus number and CAS-operations 的内容以及为什么它在实现您自己的同步原语时很重要。
在您的代码中,您使用的是
Interlocked.Exchange
方法,这实际上不是CAS
,因为它 总是 交换值,并且它共识数等于2
。这意味着使用这种构造的原语仅适用于2
个线程(不是您的情况,但仍然2
)。我试图确定您的代码是否可以在
3
线程上正常工作,或者可能存在导致您的应用程序损坏的某些情况,但在30
分钟后我停止了。在尝试理解您的代码一段时间后,您的任何团队成员都会像我一样停下来。这是在浪费时间,不仅是您的时间,也是您团队的时间。除非真的需要,否则不要重新发明轮子。我在相关领域最喜欢的书是 Writing High-Performance .NET Code by Ben Watson, and my favorite blog is Stephen Cleary 的。如果你能具体说说你对什么样的书感兴趣,我可以再补充一些参考。
程序中没有锁不会使您的应用程序
lock-free
。在 .NET 应用程序中,您真的不应该将Exceptions
用于内部程序流。考虑到初始化线程没有被 OS 安排一段时间(出于各种原因,无论它们到底是什么)。在这种情况下,您应用中的所有其他线程都将在尝试访问您的共享资源时逐步死掉。我不能说这是
lock-free
代码。是的,里面没有锁,但是它不能保证程序的正确性,因此根据定义它不是无锁的。
Maurice Herlihy 和 Nir Shavit 的多处理器编程艺术,是无锁和无等待编程的重要资源。无锁是一种进度保证,而不是一种编程模式,因此要论证算法是无锁的,必须验证或证明进度保证。简单来说,无锁意味着一个线程的阻塞或停止不会阻塞其他线程的进程,或者如果一个线程被无限频繁地阻塞,那么另一个线程会无限频繁地进行。