几何基元_.NET异步协调基元中的两种技术比较

几何基元

几何基元

Last week in my post on updating my Windows Phone 7 application to Windows 8 I shared some code from Michael L. Perry using a concept whereby one protects access to a shared resource using a critical section in a way that works comfortably with the new await/async keywords. Protecting shared resources like files is a little more subtle now that asynchronous is so easy. We'll see this more and more as Windows 8 and Windows Phone 8 promote the idea that apps shouldn't block for anything.

上周,在我将Windows Phone 7应用程序更新到Windows 8的帖子中,我分享了Michael L. Perry的一些代码,其使用的一种概念是使用关键部分来保护对共享资源的访问,从而使其与新的await /异步关键字。 既然异步是如此简单,那么保护文件之类的共享资源就变得更加微妙了。 随着Windows 8和Windows Phone 8提倡应用程序不应阻塞任何东西的想法,我们将越来越多地看到这一点。

After that post, my friend and mentor (he doesn't know he's my mentor but I just decided that he is just now) Stephen Toub, expert on all things asynchronous, sent me an email with some excellent thoughts and feedback on this technique. I include some of that email here with permission as it will help us all learn!

发布该帖子后,我的朋友和导师(他不知道他是我的导师,但我只是确定他只是现在)。异步所有问题专家Stephen Toub向我发送了一封电子邮件,其中对这种技术提出了一些很好的想法和反馈。 我在此附上了部分电子邮件的许可,因为它将帮助我们所有人学习!

I hadn’t seen the Awaitable Critical Section helper you mention below before, but I just took a look at it, and while it’s functional, it’s not ideal.  For a client-side solution like this, it’s probably fine.  If this were a server-side solution, though, I’d be concerned about the overhead associated with this particular implementation.

我之前从未见过您在下面提到的“等待关键部分”帮助程序,但我只是对其进行了研究,尽管它可以正常工作,但并不理想。 对于这样的客户端解决方案,可能很好。 但是,如果这是服务器端解决方案,我会担心与此特定实现相关的开销。

I love Stephen Toubs's feedback in all things. Always firm but kind. Stephen Cleary makes a similar observation in the comments and also points out that immediately disabling the button works too. ;) It's also worth noting that Cleary's excellent AsyncEx library has lots of async-ready primitives and supports both Windows Phone 8 and 7.5.

我喜欢Stephen Toubs在所有方面的反馈。 总是坚定但友善。 Stephen Cleary在评论中也有类似的看法,并指出立即禁用该按钮也是可行的。 ;)还值得注意的是,Cleary出色的AsyncEx库具有许多异步就绪原语,并且支持Windows Phone 8和7.5。

The SemaphoreSlim class was updated on .NET 4.5 (and Windows Phone 8) to support async waits. You would have to build your own IDisposable Release, though. (In the situation you describe, I usually just disable the button at the beginning of the async handler and re-enable it at the end; but async synchronization would work too).

SemaphoreSlim类是更新.NET 4.5(和Windows Phone 8)支持异步等待。 但是,您将必须构建自己的IDisposable版本。 (在您描述的情况下,我通常只在异步处理程序的开头禁用按钮,然后在结尾处重新启用它;但是异步同步也可以工作)。

Ultimately what we're trying to do is create "Async Coordination Primitives" and Toub talked about this in February.

最终,我们试图做的是创建“异步协调基元”,而Toub在2月谈到了这一点。

Here's in layman's terms what we're trying to do, why it's interesting and a definition of a Coordinate Primitive (stolen from MSDN):

用通俗易懂的术语讲,我们正在尝试做的事情,为什么如此有趣以及定义了“坐标原语”(从MSDN窃取):

Asynchronous programming is hard because there is no simple method to coordinate between multiple operations, deal with partial failure (one of many operations fail but others succeed) and also define execution behavior of asynchronous callbacks, so they don't violate some concurrency constraint. For example, they don't attempt to do something in parallel. [Coordination Primitives] enable and promote concurrency by providing ways to express what coordination should happen.

异步编程很难,因为没有简单的方法可以在多个操作之间进行协调,处理部分失败(许多操作之一失败而其他操作成功)以及定义异步回调的执行行为,因此它们不违反某些并发约束。 例如,他们不会尝试并行执行某项操作。 [协调原语]通过提供表达应发生的协调的方式来启用和促进并发。

In this case, we're trying to handled locking when using async, which is just one kind of coordination primitive. From Stephen Toub's blog:

在这种情况下,我们尝试使用异步时处理锁定,这只是一种协调原语。 从Stephen Toub的博客中:

Here, we’ll look at building support for an async mutual exclusion mechanism that supports scoping via ‘using.’

在这里,我们将研究对异步互斥机制的支持,该机制支持通过“使用”进行范围界定。

I previously blogged about a similar solution (http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx), which would result in a helper class like this:

我之前曾在博客中介绍过类似的解决方案( http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx ),这将导致如下所示的帮助程序类:

Here Toub uses the new lightweight SemaphoreSlim class and indulges our love of the "using" pattern to create something very lightweight.

在这里,Toub使用了新的轻量级SemaphoreSlim类,并沉迷于我们对“使用”模式的热爱,以创建非常轻量的东西。

public sealed class AsyncLock
{
private readonly SemaphoreSlim m_semaphore = new SemaphoreSlim(1, 1);
private readonly Task<IDisposable> m_releaser;

public AsyncLock()
{
m_releaser = Task.FromResult((IDisposable)new Releaser(this));
}

public Task<IDisposable> LockAsync()
{
var wait = m_semaphore.WaitAsync();
return wait.IsCompleted ?
m_releaser :
wait.ContinueWith((_, state) => (IDisposable)state,
m_releaser.Result, CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}

private sealed class Releaser : IDisposable
{
private readonly AsyncLock m_toRelease;
internal Releaser(AsyncLock toRelease) { m_toRelease = toRelease; }
public void Dispose() { m_toRelease.m_semaphore.Release(); }
}
}

How lightweight and how is this different from the previous solution? Here's Stephen Toub, emphasis mine.

与以前的解决方案相比,轻便程度如何?有什么不同? 这是斯蒂芬·图布(Stephen Toub),重点是我的。

There are a few reasons I’m not enamored with the referenced AwaitableCriticalSection solution. 

有几个原因使我不喜欢所引用的AwaitableCriticalSection解决方案。

First, it has unnecessary allocations; again, not a big deal for a client library, but potentially more impactful for a server-side solution.  An example of this is that often with locks, when you access them they’re uncontended, and in such cases you really want acquiring and releasing the lock to be as low-overhead as possible; in other words, accessing uncontended locks should involve a fast path.  With AsyncLock above, you can see that on the fast path where the task we get back from WaitAsync is already completed, we’re just returning a cached already-completed task, so there’s no allocation (for the uncontended path where there’s still count left in the semaphore, WaitAsync will use a similar trick and will not incur any allocations).

首先,它有不必要的分配; 同样,对于客户端库而言,这没什么大不了的,但是对于服务器端解决方案而言,影响可能更大。 一个例子是,经常使用锁,当您访问它们时,它们是不受竞争的,在这种情况下,您确实希望获取和释放锁的开销尽可能小。 换句话说,访问无竞争的锁应该涉及一条快速路径 通过上面的AsyncLock,您可以看到在从WaitAsync取回的任务已经完成的快速路径上,我们只是返回已缓存的已完成任务,因此没有分配(对于仍然剩余计数的无竞争路径)在信号量中,WaitAsync将使用类似的技巧,并且不会产生任何分配。

Lots here to parse. One of the interesting meta-points is that a simple client-side app with a user interacting (like my app) has VERY different behaviors than a high-throughput server-side application. Translation? I can get away with a lot more on the client side...but should I when I don't have to?

很多在这里解析。 有趣的元数据之一是,与用户交互的简单客户端应用程序(例如我的应用程序)的行为与高吞吐量服务器端应用程序的行为非常不同。 翻译? 我可以在客户端获得更多收益……但是在不需要的时候我应该这样做吗?

His solution requires fewer allocations and zero garbage collections.

他的解决方案需要更少的分配和零垃圾回收。

Overall, it’s also just much more unnecessary overhead.  A basic microbenchmark shows that in the uncontended case, AsyncLock above is about 30x faster with 0 GCs (versus a bunch of GCs in the AwaitableCriticalSection example.  And in the contended case, it looks to be about 10-15x faster.

总体而言,这也只是不必要的开销。 一个基本的微基准测试表明,在无竞争的情况下,使用0个GC时,上面的AsyncLock大约快30倍(与AwaitableCriticalSection示例中的一堆GC相比。在有竞争的情况下,它看起来要快10-15倍。

Here's the microbenchmark comparing the two...remembering of course there's, "lies, damned lies, and microbenchmarks," but this one is pretty useful. ;)

这是比较这两个基准的微基准...当然,记住“谎言,该死的谎言和微基准”,但是这个非常有用。 ;)

class Program
{
static void Main()
{
const int ITERS = 100000;
while (true)
{
Run("Uncontended AL ", () => TestAsyncLockAsync(ITERS, false));
Run("Uncontended ACS", () => TestAwaitableCriticalSectionAsync(ITERS, false));
Run("Contended AL ", () => TestAsyncLockAsync(ITERS, true));
Run("Contended ACS", () => TestAwaitableCriticalSectionAsync(ITERS, true));
Console.WriteLine();
}
}

static void Run(string name, Func<Task> test)
{
var sw = Stopwatch.StartNew();
test().Wait();
sw.Stop();
Console.WriteLine("{0}: {1}", name, sw.ElapsedMilliseconds);
}

static async Task TestAsyncLockAsync(int iters, bool contended)
{
var mutex = new AsyncLock();
if (contended)
{
var waits = new Task<IDisposable>[iters];
using (await mutex.LockAsync())
for (int i = 0; i < iters; i++)
waits[i] = mutex.LockAsync();
for (int i = 0; i < iters; i++)
using (await waits[i]) { }
}
else
{
for (int i = 0; i < iters; i++)
using (await mutex.LockAsync()) { }
}
}

static async Task TestAwaitableCriticalSectionAsync(int iters, bool contended)
{
var mutex = new AwaitableCriticalSection();
if (contended)
{
var waits = new Task<IDisposable>[iters];
using (await mutex.EnterAsync())
for (int i = 0; i < iters; i++)
waits[i] = mutex.EnterAsync();
for (int i = 0; i < iters; i++)
using (await waits[i]) { }
}
else
{
for (int i = 0; i < iters; i++)
using (await mutex.EnterAsync()) { }
}
}
}

Stephen Toub is using Semaphore Slim, the "lightest weight" option available, rather than RegisterWaitForSingleObject:

Stephen Toub使用的是Semaphore Slim,它是可用的“最轻量”选项,而不是RegisterWaitForSingleObject:

Second, and more importantly, the AwaitableCriticalSection is using a fairly heavy synchronization mechanism to provide the mutual exclusion.  The solution is using Task.Factory.FromAsync(IAsyncResult, …), which is just a wrapper around ThreadPool.RegisterWaitForSingleObject (see http://blogs.msdn.com/b/pfxteam/archive/2012/02/06/10264610.aspx).  Each call to this is asking the ThreadPool to have a thread block waiting on the supplied ManualResetEvent, and then to complete the returned Task when the event is set.  Thankfully, the ThreadPool doesn’t burn one thread per event, and rather groups multiple events together per thread, but still, you end up wasting some number of threads (IIRC, it’s 63 events per thread), so in a server-side environment, this could result in degraded behavior.

其次,更重要的是,AwaitableCriticalSection使用一种相当繁重的同步机制来提供互斥。 解决方案使用Task.Factory.FromAsync(IAsyncResult,…),它只是ThreadPool.RegisterWaitForSingleObject的包装(请参阅http://blogs.msdn.com/b/pfxteam/archive/2012/02/06/10264610。 aspx )。 对此的每次调用都要求ThreadPool具有等待提供的ManualResetEvent的线程块,然后在设置事件时完成返回的Task。 值得庆幸的是,ThreadPool不会为每个事件刻录一个线程,而是为每个线程将多个事件组合在一起,但是仍然会浪费一定数量的线程(IIRC,每个线程63个事件),因此在服务器端环境中,这可能会导致行为下降。

All in all, a education for me - and I hope you, Dear Reader - as well as a few important lessons.

总而言之,对我的教育-希望您,亲爱的读者-以及一些重要的课程。

  • Know what's happening underneath if you can.

    如果可以的话,知道底层发生了什么。
  • Code Reviews are always a good thing.

    代码审查始终是一件好事。
  • Ask someone smarter.

    问一个更聪明的人。
  • Performance may not matter in one context but it can in another.

    在一种情况下,性能可能并不重要,但在另一种情况下,性能却可能无关紧要。
  • You can likely get away with this or that, until you totally can't. (Client vs. Server)

    您可能会逃避这个或那个,直到您完全无法做到。 (客户端与服务器)

Thanks Stephen Toub and Stephen Cleary!

感谢Stephen Toub和Stephen Cleary!

相关阅读 (Related Reading)

翻译自: https://www.hanselman.com/blog/comparing-two-techniques-in-net-asynchronous-coordination-primitives

几何基元

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值