Tại sao tôi không thể sử dụng toán tử 'await' trong phần thân của câu lệnh khóa?


348

Từ khóa đang chờ trong C # (.NET Async CTP) không được phép trong câu lệnh khóa.

Từ MSDN :

Không thể sử dụng biểu thức chờ trong hàm đồng bộ, trong biểu thức truy vấn, trong khối bắt hoặc cuối cùng của câu lệnh xử lý ngoại lệ, trong khối của câu lệnh khóa hoặc trong ngữ cảnh không an toàn.

Tôi cho rằng điều này là khó khăn hoặc không thể cho nhóm biên dịch thực hiện vì một số lý do.

Tôi đã thử làm việc với câu lệnh bằng cách sử dụng:

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();
}

Tuy nhiên điều này không hoạt động như mong đợi. Cuộc gọi đến Monitor.Exit trong ExitDis Dùng một lần. Dường như chặn vô thời hạn (hầu hết thời gian) gây ra bế tắc khi các luồng khác cố lấy khóa. Tôi nghi ngờ sự không đáng tin cậy của công việc của tôi xung quanh và lý do chờ tuyên bố không được phép trong tuyên bố khóa có liên quan nào đó.

Có ai biết tại sao chờ đợi không được phép trong cơ thể của một tuyên bố khóa?


27
Tôi tưởng tượng bạn đã tìm thấy lý do tại sao nó không được phép.
asawyer

3
Tôi có thể đề xuất liên kết này: hanselman.com/blog/ và cái này: blog.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx
hans

Tôi mới bắt đầu bắt kịp và tìm hiểu thêm một chút về lập trình async. Sau nhiều lần bế tắc trong các ứng dụng wpf của mình, tôi thấy bài viết này là một người bảo vệ an toàn tuyệt vời trong thực tiễn lập trình async. msdn.microsoft.com/en-us/magazine/
Lần

Khóa được thiết kế để ngăn truy cập async khi truy cập async sẽ phá vỡ mã của bạn, ergo nếu bạn đang sử dụng async bên trong khóa, bạn đã vô hiệu hóa khóa của mình .. vì vậy nếu bạn cần chờ một cái gì đó bên trong khóa thì bạn không sử dụng khóa chính xác
MikeT

Câu trả lời:


366

Tôi cho rằng điều này là khó khăn hoặc không thể cho nhóm biên dịch thực hiện vì một số lý do.

Không, hoàn toàn không khó hoặc không thể thực hiện - việc bạn tự thực hiện nó là một minh chứng cho thực tế đó. Thay vào đó, đó là một ý tưởng cực kỳ tồi tệ và vì vậy chúng tôi không cho phép nó, để bảo vệ bạn khỏi phạm sai lầm này.

gọi tới Monitor.Exit trong ExitDis Dùng một lần. Dường như chặn vô thời hạn (hầu hết thời gian) gây ra bế tắc khi các luồng khác cố gắng lấy khóa. Tôi nghi ngờ sự không đáng tin cậy của công việc của tôi xung quanh và lý do chờ tuyên bố không được phép trong tuyên bố khóa có liên quan nào đó.

Chính xác, bạn đã phát hiện ra lý do tại sao chúng tôi làm cho nó bất hợp pháp. Chờ đợi bên trong một khóa là một công thức để tạo ra bế tắc.

Tôi chắc chắn bạn có thể thấy lý do tại sao: mã tùy ý chạy trong khoảng thời gian chờ đợi trả lại quyền điều khiển cho người gọi và phương thức tiếp tục . Mã tùy ý đó có thể lấy ra các khóa tạo ra các đảo ngược thứ tự khóa và do đó các khóa chết.

Tồi tệ hơn, mã có thể tiếp tục trên một luồng khác (trong các kịch bản nâng cao; thông thường bạn chọn lại trên luồng đã chờ, nhưng không nhất thiết) trong trường hợp mở khóa sẽ mở khóa trên một luồng khác với luồng đã lấy ra khóa. Đó có phải là một ý tưởng tốt? Không.

Tôi lưu ý rằng đó cũng là một "thực hành tồi tệ nhất" để thực hiện yield returnbên trong a lock, vì lý do tương tự. Làm như vậy là hợp pháp, nhưng tôi ước chúng tôi đã biến nó thành bất hợp pháp. Chúng ta sẽ không phạm sai lầm tương tự cho "chờ đợi".


188
Làm thế nào để bạn xử lý một kịch bản mà bạn cần trả về một mục bộ đệm và nếu mục đó không tồn tại, bạn cần tính toán không đồng bộ nội dung sau đó thêm + trả lại mục nhập, đảm bảo không có ai gọi cho bạn trong lúc này?
Softlion

9
Tôi nhận ra rằng tôi đến bữa tiệc muộn ở đây, tuy nhiên tôi rất ngạc nhiên khi thấy bạn đặt bế tắc là lý do chính tại sao đây là một ý tưởng tồi. Tôi đã đi đến kết luận trong suy nghĩ của riêng mình rằng bản chất tái nhập của khóa / Màn hình sẽ là một phần lớn của vấn đề. Nghĩa là, bạn xếp hàng hai tác vụ vào nhóm luồng mà khóa (), trong một thế giới đồng bộ sẽ thực thi trên các luồng riêng biệt. Nhưng bây giờ với sự chờ đợi (nếu tôi cho phép), bạn có thể có hai tác vụ thực thi trong khối khóa vì luồng đã được sử dụng lại. Nảy sinh vui nhộn. Hay tôi đã hiểu nhầm điều gì?
Gareth Wilson

4
@GarethWilson: Tôi đã nói về những bế tắc vì câu hỏi được hỏi là về những bế tắc . Bạn đúng rằng các vấn đề tái nhập kỳ lạ là có thể và dường như có thể.
Eric Lippert

11
@Eric Lippert. Cho rằng SemaphoreSlim.WaitAsynclớp đã được thêm vào .NET framework sau khi bạn đăng câu trả lời này, tôi nghĩ rằng chúng ta có thể giả định rằng bây giờ có thể an toàn. Bất kể điều này, ý kiến ​​của bạn về khó khăn khi thực hiện một cấu trúc như vậy vẫn hoàn toàn hợp lệ.
Contango

7
"mã tùy ý chạy giữa thời gian chờ đợi trả lại quyền điều khiển cho người gọi và phương thức tiếp tục lại" - chắc chắn điều này đúng với bất kỳ mã nào, ngay cả khi không có async / await, trong ngữ cảnh đa luồng: các luồng khác có thể thực thi mã tùy ý ở bất kỳ thời gian, và nói mã tùy ý như bạn nói "có thể lấy ra các khóa tạo ra đảo ngược trật tự khóa, và do đó bế tắc." Vậy tại sao điều này có ý nghĩa đặc biệt với async / await? Tôi hiểu điểm thứ hai là "mã có thể tiếp tục trên một luồng khác" có ý nghĩa đặc biệt đối với async / await.
thịt xông khói

291

Sử dụng SemaphoreSlim.WaitAsyncphương pháp.

 await mySemaphoreSlim.WaitAsync();
 try {
     await Stuff();
 } finally {
     mySemaphoreSlim.Release();
 }

10
Vì phương pháp này đã được đưa vào .NET framework gần đây, tôi nghĩ rằng chúng ta có thể giả định rằng khái niệm khóa trong một thế giới không đồng bộ / chờ đợi hiện đã được chứng minh.
Contango

5
Để biết thêm thông tin, hãy tìm kiếm văn bản "SemaphoreSlim" trong bài viết này: Async / Await - Thực tiễn tốt nhất trong lập trình không đồng bộ
BobbyA

1
@JamesKo nếu tất cả những nhiệm vụ đó đang chờ kết quả của Stufftôi thì tôi không thấy cách nào khác ...
Ohad Schneider

7
Không nên khởi tạo nó mySemaphoreSlim = new SemaphoreSlim(1, 1)để hoạt động như thế lock(...)nào?
Serge

3
Đã thêm phiên bản mở rộng của câu trả lời này: stackoverflow.com/a/50139704/1844247
Sergey

67

Về cơ bản nó sẽ là điều sai trái để làm.

Có hai cách này có thể được thực hiện:

  • Giữ khóa, chỉ phát hành ở cuối khối .
    Đây là một ý tưởng thực sự tồi tệ vì bạn không biết hoạt động không đồng bộ sẽ diễn ra trong bao lâu. Bạn chỉ nên giữ ổ khóa trong một khoảng thời gian tối thiểu . Cũng có khả năng là không thể, vì một luồng sở hữu khóa, không phải là một phương thức - và bạn thậm chí có thể không thực thi phần còn lại của phương thức không đồng bộ trên cùng một luồng (tùy thuộc vào trình lập lịch tác vụ).

  • Phát hành khóa trong chờ đợi và truy vấn lại khi chờ đợi
    Điều này vi phạm nguyên tắc ít gây ngạc nhiên nhất IMO, trong đó phương pháp không đồng bộ sẽ hoạt động gần nhất có thể như mã đồng bộ tương đương - trừ khi bạn sử dụng Monitor.Waittrong khối khóa, bạn mong muốn sở hữu khóa trong suốt thời gian của khối.

Vì vậy, về cơ bản có hai yêu cầu cạnh tranh ở đây - bạn không nên thử thực hiện lần đầu tiên ở đây và nếu bạn muốn thực hiện cách tiếp cận thứ hai, bạn có thể làm cho mã rõ ràng hơn bằng cách có hai khối khóa tách biệt được phân tách bằng biểu thức chờ đợi:

// Now it's clear where the locks will be acquired and released
lock (foo)
{
}
var result = await something;
lock (foo)
{
}

Vì vậy, bằng cách cấm bạn chờ đợi trong khối khóa, ngôn ngữ buộc bạn phải suy nghĩ về những gì bạn thực sự muốn làm và làm cho lựa chọn đó rõ ràng hơn trong mã mà bạn viết.


5
Cho rằng SemaphoreSlim.WaitAsynclớp đã được thêm vào .NET framework sau khi bạn đăng câu trả lời này, tôi nghĩ rằng chúng ta có thể giả định rằng bây giờ có thể an toàn. Bất kể điều này, ý kiến ​​của bạn về khó khăn khi thực hiện một cấu trúc như vậy vẫn hoàn toàn hợp lệ.
Contango

7
@Contango: Vâng, điều đó không hoàn toàn giống như vậy. Cụ thể, semaphore không được gắn với một chủ đề cụ thể. Nó đạt được các mục tiêu tương tự để khóa, nhưng có sự khác biệt đáng kể.
Jon Skeet

@JonSkeet tôi biết đây là một chủ đề rất cũ và tất cả, nhưng tôi không chắc cách thức cuộc gọi () được bảo vệ bằng cách sử dụng các khóa đó theo cách thứ hai? khi một luồng đang thực thi một cái gì đó () thì bất kỳ luồng nào khác cũng có thể tham gia vào nó! Am i thiếu cái gì ở đây ?

@Joseph: Nó không được bảo vệ tại thời điểm đó. Đó là cách tiếp cận thứ hai, điều này cho thấy rõ rằng bạn đang có được / phát hành, sau đó có được / phát hành lại, có thể trên một luồng khác. Bởi vì cách tiếp cận đầu tiên là một ý tưởng tồi, theo câu trả lời của Eric.
Jon Skeet

41

Đây chỉ là một phần mở rộng cho câu trả lời này .

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();
        }
    }
}

Sử dụng:

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. 
        });
    }
}

1
Có thể nguy hiểm khi có khóa semaphore bên ngoài trykhối - nếu một ngoại lệ xảy ra giữa WaitAsynctrysemaphore sẽ không bao giờ được phát hành (bế tắc). Mặt khác, việc chuyển WaitAsynccuộc gọi vào trykhối sẽ đưa ra một vấn đề khác, khi semaphore có thể được giải phóng mà không cần khóa. Xem chủ đề liên quan trong đó vấn đề này đã được giải thích: stackoverflow.com/a/61806749/7889645
AndreyCh

16

Điều này đề cập đến http://bloss.msdn.com/b/pfxteam/archive/2012/02/12/10266988.aspx , http: // winrtst Storagehelper.codeplex.com/ cửa hàng ứng dụng Windows 8 và .net 4.5

Đây là góc của tôi về điều này:

Tính năng ngôn ngữ async / await làm cho nhiều thứ khá dễ dàng nhưng nó cũng giới thiệu một kịch bản hiếm khi gặp phải trước khi nó rất dễ sử dụng các cuộc gọi async: reentence.

Điều này đặc biệt đúng đối với các trình xử lý sự kiện, bởi vì đối với nhiều sự kiện, bạn không có bất kỳ manh mối nào về những gì đang xảy ra sau khi bạn trở về từ trình xử lý sự kiện. Một điều có thể thực sự xảy ra là, phương thức async mà bạn đang chờ trong trình xử lý sự kiện đầu tiên, được gọi từ một trình xử lý sự kiện khác vẫn nằm trên cùng một luồng.

Đây là một tình huống thực tế tôi gặp trong ứng dụng Windows 8 App store: Ứng dụng của tôi có hai khung: đi vào và rời khỏi khung tôi muốn tải / an toàn một số dữ liệu vào tệp / lưu trữ. Các sự kiện OnNavigatedTo / From được sử dụng để lưu và tải. Việc lưu và tải được thực hiện bởi một số chức năng tiện ích async (như http: // winrtst Storagehelper.codeplex.com/ ). Khi điều hướng từ khung 1 đến khung 2 hoặc theo hướng khác, tải không đồng bộ và các hoạt động an toàn được gọi và chờ đợi. Trình xử lý sự kiện trở thành không đồng bộ trả về void => chúng không thể được chờ đợi.

Tuy nhiên, thao tác mở tệp đầu tiên (giả sử: bên trong chức năng lưu) của tiện ích cũng không đồng bộ và do đó, lần đầu tiên chờ trả lại quyền điều khiển cho khung, sau đó sau đó gọi tiện ích khác (tải) thông qua trình xử lý sự kiện thứ hai. Tải bây giờ cố gắng mở cùng một tệp và nếu bây giờ tệp được mở cho hoạt động lưu, không thành công với ngoại lệ ACCESSDENIED.

Một giải pháp tối thiểu đối với tôi là bảo mật truy cập tệp thông qua việc sử dụng và AsyncLock.

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);
    }
}

Xin lưu ý rằng về cơ bản khóa của anh ấy khóa tất cả các thao tác tệp cho tiện ích chỉ bằng một khóa, mạnh mẽ không cần thiết nhưng hoạt động tốt cho kịch bản của tôi.

Đây là dự án thử nghiệm của tôi: một cửa hàng ứng dụng windows 8 ứng dụng với một số cuộc gọi thử nghiệm cho phiên bản gốc từ http://winrtstoragehelper.codeplex.com/ và phiên bản sửa đổi của tôi có sử dụng các AsyncLock từ Stephen Toub http: //blogs.msdn. com / b / pfxteam / lưu trữ / 2012/02/12 / 10266988.aspx .

Tôi cũng có thể đề xuất liên kết này: http://www.hanselman.com/blog/ComparedTwoT kỹ thuật


7

Stephen Taub đã triển khai một giải pháp cho câu hỏi này, xem Nguyên tắc phối hợp xây dựng Async, Phần 7: AsyncReaderWriterLock .

Stephen Taub được đánh giá cao trong ngành, vì vậy bất cứ điều gì anh viết đều có khả năng vững chắc.

Tôi sẽ không sao chép mã mà anh ấy đã đăng trên blog của mình, nhưng tôi sẽ chỉ cho bạn cách sử dụng nó:

/// <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.
        }
    }
}

Nếu bạn muốn một phương thức được đưa vào .NET framework, SemaphoreSlim.WaitAsyncthay vào đó hãy sử dụng . Bạn sẽ không nhận được khóa đọc / ghi, nhưng bạn sẽ được thử và thực hiện thử nghiệm.


Tôi tò mò muốn biết liệu có bất kỳ sự cẩn thận nào khi sử dụng mã này không. Nếu bất cứ ai có thể chứng minh bất kỳ vấn đề với mã này, tôi muốn biết. Tuy nhiên, điều đúng là khái niệm khóa async / await khóa chắc chắn đã được chứng minh rõ ràng, như SemaphoreSlim.WaitAsynctrong khung .NET. Tất cả các mã này là thêm một khái niệm khóa người đọc / nhà văn.
Contango

3

Hmm, trông xấu xí, dường như làm việc.

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();
        }
    }
}

0

Tôi đã thử sử dụng Monitor (mã bên dưới) có vẻ hoạt động nhưng có GOTCHA ... khi bạn có nhiều luồng, nó sẽ cung cấp cho ... System.Threading.Syn syncizationLockException Phương thức đồng bộ hóa đối tượng được gọi từ một khối mã không đồng bộ.

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;
        }
    }
}

Trước đó, tôi chỉ đơn giản là làm điều này, nhưng nó nằm trong bộ điều khiển ASP.NET nên nó dẫn đến bế tắc.

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.