关于c#:为什么我不能在lock语句体内使用’await’运算符?

Why can't I use the 'await' operator within the body of a lock statement?

锁定语句中不允许使用C#(.NET Async CTP)中的await关键字。

来自MSDN:

An
await expression cannot be used in a synchronous function, in a query
expression, in the catch or finally block of an exception handling
statement, in the block of a lock statement, or in an unsafe context.

我认为编译器团队出于某种原因要么难以实施,要么难以实施。

我尝试使用using语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Async
{
    public static async Task<IDisposable> Lock(object obj)
    {
        while (!Monitor.TryEnter(obj))
            await TaskEx.Yield();

        return new ExitDisposable(obj);
    }

    private class ExitDisposable : IDisposable
    {
        private readonly object obj;
        public ExitDisposable(object obj) { this.obj = obj; }
        public void Dispose() { Monitor.Exit(this.obj); }
    }
}

// example usage
using (await Async.Lock(padlock))
{
    await SomethingAsync();
}

但是这不能按预期工作。 在ExitDisposable.Dispose中对Monitor.Exit的调用似乎无限期地(大部分时间)阻塞,导致死锁,因为其他线程试图获取锁。 我怀疑我的工作不可靠以及锁定语句中不允许等待语句的原因在某种程度上是相关的。

有谁知道为什么在锁定声明的正文中不允许等待?


I assume this is either difficult or impossible for the compiler team to implement for some reason.

不,实现起来并不困难或不可能 - 您自己实施它的事实证明了这一事实。相反,这是一个非常糟糕的主意,因此我们不允许它,以保护您免于犯这个错误。

call to Monitor.Exit within ExitDisposable.Dispose seems to block indefinitely (most of the time) causing deadlocks as other threads attempt to acquire the lock. I suspect the unreliability of my work around and the reason await statements are not allowed in lock statement are somehow related.

正确的,你已经发现了我们为什么把它变成非法的。在锁内等待是一个产生死锁的方法。

我相信你可以理解为什么:在await将控制权返回给调用者并且方法恢复之间运行任意代码。任意代码可能会取出产生锁定顺序反转的锁,从而产生死锁。

更糟糕的是,代码可以在另一个线程上恢复(在高级场景中;通常你会在执行等待的线程上再次获取,但不一定),在这种情况下,解锁将解锁与不同线程上的锁相比。出锁。这是一个好主意吗?没有。

我注意到,出于同样的原因,在lock内执行yield return也是"最糟糕的做法"。这样做是合法的,但我希望我们把它变成非法的。我们不会为"等待"犯同样的错误。


使用SemaphoreSlim.WaitAsync方法。

1
2
3
4
5
6
 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }


基本上这样做是错误的。

有两种方法可以实现:

  • 保持锁定,只在块的末端释放它。
    这是一个非常糟糕的主意,因为你不知道异步操作需要多长时间。你应该只持有最少的时间锁。它也可能是不可能的,因为一个线程拥有一个锁,而不是一个方法 - 你甚至可能不会在同一个线程上执行其余的异步方法(取决于任务调度程序)。

  • 释放await中的锁,并在await返回时重新获取
    这违反了最不惊讶的原则IMO,其中异步方法应该像等效同步代码一样尽可能地行事 - 除非在锁定块中使用Monitor.Wait,否则您希望在块的持续时间内拥有锁定。

所以基本上这里有两个相互竞争的要求 - 你不应该在这里尝试第一个,如果你想采用第二种方法,你可以通过将两个分开的锁定块用await表达式分隔来使代码更清晰:

1
2
3
4
5
6
7
8
// Now it's clear where the locks will be acquired and released
lock (foo)
{
}
var result = await something;
lock (foo)
{
}

因此,通过禁止您等待锁定块本身,该语言迫使您思考您真正想要做的事情,并在您编写的代码中更清楚地做出选择。


这只是这个答案的延伸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Threading;
using System.Threading.Tasks;

public class SemaphoreLocker
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task LockAsync(Func<Task> worker)
    {
        await _semaphore.WaitAsync();
        try
        {
            await worker();
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test
{
    private static readonly SemaphoreLocker _locker = new SemaphoreLocker();

    public async Task DoTest()
    {
        await _locker.LockAsync(async () =>
        {
            // [asyn] calls can be used within this block
            // to handle a resource by one thread.
        });
    }
}

这是http://blogs.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx,http://winrtstoragehelper.codeplex.com/,Windows 8 app store和.net 4.5的参考

这是我对此的看法:

async / await语言功能使很多事情变得相当简单,但它也引入了一个场景
在很容易使用异步调用之前很少遇到:重入。

对于事件处理程序尤其如此,因为对于许多事件,您没有任何关于从事件处理程序返回后发生的事情的线索。
实际上可能发生的一件事是,你在第一个事件处理程序中等待的异步方法,从另一个仍然在
相同的线程。

这是我在Windows 8 App Store应用程序中遇到的真实场景:
我的应用程序有两个框架:进入和离开框架我想加载/保护一些数据到文件/存储。
OnNavigatedTo / From事件用于保存和加载。保存和加载由一些异步实用程序功能(如http://winrtstoragehelper.codeplex.com/)完成。
当从第1帧导航到第2帧或在另一个方向上导航时,将调用并等待异步加载和安全操作。
事件处理程序变为异步返回void =>他们无法等待。

但是,该实用程序的第一个文件打开操作(让我们说:在保存函数内)也是异步的
所以第一个await将控制返回给框架,稍后通过第二个事件处理程序调用另一个实用程序(load)。
加载现在尝试打开相同的文件,如果
此文件现在已打开以进行保存操作,因ACCESSDENIED异常而失败。

对我来说,最小的解决方案是通过using和AsyncLock保护文件访问。

1
2
3
4
5
6
7
8
9
10
11
12
private static readonly AsyncLock m_lock = new AsyncLock();
...

using (await m_lock.LockAsync())
{
    file = await folder.GetFileAsync(fileName);
    IRandomAccessStream readStream = await file.OpenAsync(FileAccessMode.Read);
    using (Stream inStream = Task.Run(() => readStream.AsStreamForRead()).Result)
    {
        return (T)serializer.Deserialize(inStream);
    }
}

请注意,他的锁基本上只用一个锁来锁定该实用程序的所有文件操作,这是不必要的强,但对我的方案工作正常。

这是我的测试项目:一个Windows 8应用程序商店应用程序,其中包含一些来自http://winrtstoragehelper.codeplex.com/的原始版本的测试调用,以及我使用Stephen Toub http://blogs.msdn的AsyncLock的修改版本。 COM / b / pfxteam /存档/ 2012/02/12 / 10266988.aspx。

我可以建议这个链接:
http://www.hanselman.com/blog/ComparingTwoTechniquesInNETAsynchronousCoordinationPrimitives.aspx


Stephen Taub已经实现了这个问题的解决方案,请参阅构建异步协调基元,第7部分:AsyncReaderWriterLock。

Stephen Taub在业界备受推崇,所以他所写的任何内容都可能是稳固的。

我不会重现他在博客上发布的代码,但我将展示如何使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
///     Demo class for reader/writer lock that supports async/await.
///     For source, see Stephen Taub's brilliant article,"Building Async Coordination
///     Primitives, Part 7: AsyncReaderWriterLock".
/// </summary>
public class AsyncReaderWriterLockDemo
{
    private readonly IAsyncReaderWriterLock _lock = new AsyncReaderWriterLock();

    public async void DemoCode()
    {          
        using(var releaser = await _lock.ReaderLockAsync())
        {
            // Insert reads here.
            // Multiple readers can access the lock simultaneously.
        }

        using (var releaser = await _lock.WriterLockAsync())
        {
            // Insert writes here.
            // If a writer is in progress, then readers are blocked.
        }
    }
}

如果您是一个融入.NET框架的方法,请改用SemaphoreSlim.WaitAsync。您将无法获得读取器/写入器锁定,但您将获得经过测试和测试的实现。


嗯,看起来很丑,似乎工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static class Async
{
    public static Task<IDisposable> Lock(object obj)
    {
        return TaskEx.Run(() =>
            {
                var resetEvent = ResetEventFor(obj);

                resetEvent.WaitOne();
                resetEvent.Reset();

                return new ExitDisposable(obj) as IDisposable;
            });
    }

    private static readonly IDictionary<object, WeakReference> ResetEventMap =
        new Dictionary<object, WeakReference>();

    private static ManualResetEvent ResetEventFor(object @lock)
    {
        if (!ResetEventMap.ContainsKey(@lock) ||
            !ResetEventMap[@lock].IsAlive)
        {
            ResetEventMap[@lock] =
                new WeakReference(new ManualResetEvent(true));
        }

        return ResetEventMap[@lock].Target as ManualResetEvent;
    }

    private static void CleanUp()
    {
        ResetEventMap.Where(kv => !kv.Value.IsAlive)
                     .ToList()
                     .ForEach(kv => ResetEventMap.Remove(kv));
    }

    private class ExitDisposable : IDisposable
    {
        private readonly object _lock;

        public ExitDisposable(object @lock)
        {
            _lock = @lock;
        }

        public void Dispose()
        {
            ResetEventFor(_lock).Set();
        }

        ~ExitDisposable()
        {
            CleanUp();
        }
    }
}

我确实尝试使用监视器(下面的代码)似乎工作,但有一个GOTCHA ...当你有多个线程时它会给... System.Threading.SynchronizationLockException对象同步方法是从一个不同步的代码块调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using System.Threading;
using System.Threading.Tasks;

namespace MyNamespace
{
    public class ThreadsafeFooModifier :
    {
        private readonly object _lockObject;

        public async Task<FooResponse> ModifyFooAsync()
        {
            FooResponse result;
            Monitor.Enter(_lockObject);
            try
            {
                result = await SomeFunctionToModifyFooAsync();
            }
            finally
            {
                Monitor.Exit(_lockObject);
            }
            return result;
        }
    }
}

在此之前,我只是这样做,但它是在ASP.NET控制器中,因此导致死锁。

public async Task ModifyFooAsync()
{
lock(lockObject)
{
return SomeFunctionToModifyFooAsync.Result;
}
}