我有一个线程安全类,它使用需要以独占方式访问的特定资源。在我的评估中,让各种方法的调用方阻塞Monitor.Enter
或等待SemaphoreSlim
以访问此资源是没有意义的。
例如,我有一些"昂贵"的异步初始化。由于多次初始化是没有意义的,无论是来自多个线程还是单个线程,多个调用都应该立即返回(甚至抛出异常(。相反,应该创建、init和,然后将实例分发到多个线程。
更新1:
CCD_ 3在任一方向上使用两个CCD_。InitBeforeDistribute
方法并不是真正的初始化,而是在两个方向上正确地建立连接。在设置连接之前,使管道可用于N
线程是没有意义的。一旦设置好,多个线程就可以发布工作,但实际上只有一个线程可以读取/写入流。我很抱歉用不恰当的例子命名混淆了这一点。
更新2:
如果InitBeforeDistribute
实现了具有适当等待逻辑的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);
}
}
}
一些要点:
- 如果存在阻止/等待访问锁的情况完美的意义,那么这个例子就没有意义了
- 由于我需要在方法中等待,我必须使用类似SemaphoreSlim如果我在哪里使用"正确"的锁。忘记上面例子的信号量让我不用担心(我总是不喜欢处理多个线程使用的项目的想法。这是一个小调肯定是肯定的。(
- 如果经常调用该方法,则可能会有一些性能好处,当然应该衡量
上面的例子在参考文献(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)
}
}
这个"无锁"的例子有没有一些具体的负面方面需要我注意?
大多数与SO上的"免锁"代码有关的问题通常都建议不要这样做,并表示这是为"专家"准备的。我很少(在这一点上我可能错了(看到人们可以深入研究的书籍/博客等的建议,如果人们有这样的倾向的话。如果有任何这样的资源我应该研究,请分享。如有任何建议,我们将不胜感激!
更新:伟大的文章相关
.:正在创建高性能锁和无锁代码(适用于.NET(:。
-
CCD_ 10算法的要点不在于它们是针对CCD_ 11的
要点是Do you really need lock-free algorythm here?
我不能理解你在这里的逻辑:由于多次初始化是没有意义的,无论是来自多个线程还是单个线程,多个调用都应该立即返回(甚至抛出异常(。
为什么您的用户不能简单地等待初始化结果,然后再使用您的资源?如果可以的话,只需使用
Lazy<T>
类,甚至Asynchronous Lazy Initialization
。 -
您真的应该了解一致数和CAS操作,以及为什么在实现自己的同步原语时它很重要。
在您的代码中,您使用的是
Interlocked.Exchange
方法,它不是真正的CAS
,因为它总是交换值,并且它的一致数等于2
。这意味着使用这种构造的基元将仅对2
线程正确工作(在您的情况下不是这样,但仍然是2
(。我试图定义您的代码是否能正确地用于
3
线程,或者在某些情况下可能会导致应用程序处于损坏状态,但在30
分钟后我停止了。你的任何团队成员都会像我一样在尝试理解你的代码一段时间后停止。这是浪费时间,不仅是你的,也是你的团队。除非迫不得已,否则不要重新发明轮子。 -
我在相关领域最喜欢的书是BenWatson的《编写高性能.NET代码》,我最喜欢的博客是StephenCleary的。如果你能更具体地说你对哪种书感兴趣,我可以添加更多的参考资料。
-
程序中没有锁定不会使您的应用程序成为
lock-free
。在.NET应用程序中,您确实不应该将Exceptions
用于内部程序流。考虑一下初始化线程在一段时间内没有被操作系统调度(由于各种原因,无论具体是什么原因(。在这种情况下,你的应用程序中的所有其他线程将在尝试访问你的共享资源时一步一步地死亡。我不能说这是
lock-free
代码。是的,它没有锁,但它不能保证程序的正确性,因此根据定义,它不是一个无锁的。
Maurice Herlihy和Nir Shavit的《多处理器编程艺术》是无锁和无等待编程的绝佳资源。无锁是一种进度保证,而不是一种编程模式,因此要证明算法是无锁的,必须验证或证明进度保证。简单地说,无锁意味着一个线程的阻塞或停止不会阻塞其他线程的进度,或者如果一个线程被无限频繁地阻塞,那么还有另一个线程会无限频繁地进行进度。