Có thể chờ đợi một sự kiện thay vì phương thức async khác không?


156

Trong ứng dụng tàu điện ngầm C # / XAML của tôi, có một nút khởi động quá trình dài. Vì vậy, như được đề xuất, tôi đang sử dụng async / await để đảm bảo luồng UI không bị chặn:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

Đôi khi, những thứ xảy ra trong GetResults sẽ yêu cầu thêm người dùng nhập trước khi có thể tiếp tục. Để đơn giản, giả sử người dùng chỉ cần nhấp vào nút "tiếp tục".

Câu hỏi của tôi là: làm thế nào tôi có thể tạm dừng việc thực thi GetResults theo cách mà nó đang chờ một sự kiện như nhấp vào nút khác?

Đây là một cách xấu để đạt được những gì tôi đang tìm kiếm: nút xử lý sự kiện cho nút tiếp tục "đặt cờ ...

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

... và GetResults định kỳ thăm dò ý kiến:

 buttonContinue.Visibility = Visibility.Visible;
 while (!_continue) await Task.Delay(100);  // poll _continue every 100ms
 buttonContinue.Visibility = Visibility.Collapsed;

Việc bỏ phiếu rõ ràng là khủng khiếp (bận rộn chờ đợi / lãng phí chu kỳ) và tôi đang tìm kiếm thứ gì đó dựa trên sự kiện.

Có ý kiến ​​gì không?

Btw trong ví dụ đơn giản này, tất nhiên một giải pháp sẽ chia tách GetResults () thành hai phần, gọi phần đầu tiên từ nút bắt đầu và phần thứ hai từ nút tiếp tục. Trong thực tế, những thứ xảy ra trong GetResults phức tạp hơn và các loại đầu vào người dùng khác nhau có thể được yêu cầu tại các điểm khác nhau trong quá trình thực thi. Vì vậy, chia logic thành nhiều phương thức sẽ không tầm thường.

Câu trả lời:


226

Bạn có thể sử dụng một thể hiện của Lớp SemaphoreSlim làm tín hiệu:

private SemaphoreSlim signal = new SemaphoreSlim(0, 1);

// set signal in event
signal.Release();

// wait for signal somewhere else
await signal.WaitAsync();

Ngoài ra, bạn có thể sử dụng một phiên bản của ClassCompletionSource <T> để tạo một tác vụ <T> thể hiện kết quả của nút bấm:

private TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

// complete task in event
tcs.SetResult(true);

// wait for task somewhere else
await tcs.Task;

7
@DanielHilgarth ManualResetEvent(Slim)dường như không hỗ trợ WaitAsync().
Svick

3
@DanielHilgarth Không, bạn không thể. asynckhông có nghĩa là những người chạy trên một chủ đề khác, hay một cái gì đó tương tự. Nó chỉ có nghĩa là bạn có thể sử dụng awaittrong phương thức này. Và trong trường hợp này, việc chặn bên trong GetResults()sẽ thực sự chặn luồng UI.
Svick

2
awaitBản thân @Gabe không đảm bảo rằng một luồng khác được tạo ra, nhưng nó khiến mọi thứ khác sau khi câu lệnh chạy như một phần tiếp theo Taskhoặc chờ đợi mà bạn gọi await. Thường xuyên hơn không, đó là một số loại hoạt động không đồng bộ, có thể là hoàn thành IO, hoặc một cái gì đó nằm trên một luồng khác.
casperOne

16
+1. Tôi đã phải tìm kiếm điều này, vì vậy chỉ trong trường hợp những người khác quan tâm: SemaphoreSlim.WaitAsynckhông chỉ đẩy Waitmột chuỗi chủ đề. SemaphoreSlimcó một hàng đợi thích hợp của Tasks được sử dụng để thực hiện WaitAsync.
Stephen Cleary

14
TaskCompletionSource <T> + đang chờ .Task +. SetResult () hóa ra là giải pháp hoàn hảo cho kịch bản của tôi - cảm ơn! :-)
Tối đa

75

Khi bạn có một điều bất thường mà bạn cần phải thực awaithiện, câu trả lời dễ nhất thường là TaskCompletionSource(hoặc một số asyncnguyên thủy được kích hoạt dựa trên TaskCompletionSource).

Trong trường hợp này, nhu cầu của bạn khá đơn giản, vì vậy bạn chỉ có thể sử dụng TaskCompletionSourcetrực tiếp:

private TaskCompletionSource<object> continueClicked;

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
  // Note: You probably want to disable this button while "in progress" so the
  //  user can't click it twice.
  await GetResults();
  // And re-enable the button here, possibly in a finally block.
}

private async Task GetResults()
{ 
  // Do lot of complex stuff that takes a long time
  // (e.g. contact some web services)

  // Wait for the user to click Continue.
  continueClicked = new TaskCompletionSource<object>();
  buttonContinue.Visibility = Visibility.Visible;
  await continueClicked.Task;
  buttonContinue.Visibility = Visibility.Collapsed;

  // More work...
}

private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
  if (continueClicked != null)
    continueClicked.TrySetResult(null);
}

Về mặt logic, TaskCompletionSourcegiống như một async ManualResetEvent, ngoại trừ việc bạn chỉ có thể "đặt" sự kiện một lần và sự kiện có thể có "kết quả" (trong trường hợp này, chúng tôi không sử dụng nó, vì vậy chúng tôi chỉ đặt kết quả thành null).


5
Vì tôi phân tích "chờ đợi một sự kiện" về cơ bản giống như tình huống 'bọc EAP trong một nhiệm vụ', tôi chắc chắn thích cách tiếp cận này. IMHO, nó chắc chắn đơn giản hơn / dễ hiểu hơn về mã.
James Manning

8

Đây là một lớp tiện ích mà tôi sử dụng:

public class AsyncEventListener
{
    private readonly Func<bool> _predicate;

    public AsyncEventListener() : this(() => true)
    {

    }

    public AsyncEventListener(Func<bool> predicate)
    {
        _predicate = predicate;
        Successfully = new Task(() => { });
    }

    public void Listen(object sender, EventArgs eventArgs)
    {
        if (!Successfully.IsCompleted && _predicate.Invoke())
        {
            Successfully.RunSynchronously();
        }
    }

    public Task Successfully { get; }
}

Và đây là cách tôi sử dụng nó:

var itChanged = new AsyncEventListener();
someObject.PropertyChanged += itChanged.Listen;

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;

1
Tôi không biết làm thế nào điều này hoạt động. Phương thức Nghe không đồng bộ thực thi trình xử lý tùy chỉnh của tôi như thế nào? Sẽ không new Task(() => { });được hoàn thành ngay lập tức?
nawfal

5

Lớp trợ giúp đơn giản:

public class EventAwaiter<TEventArgs>
{
    private readonly TaskCompletionSource<TEventArgs> _eventArrived = new TaskCompletionSource<TEventArgs>();

    private readonly Action<EventHandler<TEventArgs>> _unsubscribe;

    public EventAwaiter(Action<EventHandler<TEventArgs>> subscribe, Action<EventHandler<TEventArgs>> unsubscribe)
    {
        subscribe(Subscription);
        _unsubscribe = unsubscribe;
    }

    public Task<TEventArgs> Task => _eventArrived.Task;

    private EventHandler<TEventArgs> Subscription => (s, e) =>
        {
            _eventArrived.TrySetResult(e);
            _unsubscribe(Subscription);
        };
}

Sử dụng:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;

1
Làm thế nào bạn sẽ dọn sạch thuê bao example.YourEvent?
Denis P

@DenisP có lẽ chuyển sự kiện vào constructor cho EventAwaiter?
CJBrew

@DenisP Tôi đã cải tiến phiên bản và chạy thử nghiệm ngắn.
Felix Keil

Tôi có thể thấy thêm IDis Dùng, tùy thuộc vào hoàn cảnh. Ngoài ra, để tránh phải gõ vào sự kiện hai lần, chúng ta cũng có thể sử dụng Reflection để truyền tên sự kiện, do đó việc sử dụng thậm chí còn đơn giản hơn. Nếu không, tôi thích các mẫu, cảm ơn bạn.
Denis P

4

Lý tưởng nhất là bạn không . Mặc dù bạn chắc chắn có thể chặn luồng không đồng bộ, nhưng đó là một sự lãng phí tài nguyên và không lý tưởng.

Xem xét ví dụ kinh điển nơi người dùng đi ăn trưa trong khi nút đang chờ để được nhấp.

Nếu bạn đã tạm dừng mã không đồng bộ của mình trong khi chờ đầu vào từ người dùng, thì đó chỉ là lãng phí tài nguyên trong khi luồng đó bị tạm dừng.

Điều đó nói rằng, sẽ tốt hơn nếu trong hoạt động không đồng bộ của bạn, bạn đặt trạng thái mà bạn cần duy trì đến điểm bật nút và bạn đang "chờ" khi nhấp. Tại thời điểm đó, GetResultsphương pháp của bạn dừng lại .

Sau đó, khi nút được nhấn vào, dựa trên trạng thái mà bạn đã lưu trữ, bạn bắt đầu một nhiệm vụ không đồng bộ để tiếp tục công việc.

Bởi vì SynchronizationContextsẽ được ghi lại trong trình xử lý sự kiện gọi GetResults(trình biên dịch sẽ thực hiện điều này do sử dụng awaittừ khóa đang được sử dụng và thực tế là SyncizationContext.C hiện tại không phải là null, do bạn đang ở trong ứng dụng UI), nên bạn có thể sử dụng async/await thích như vậy:

private async void Button_Click_1(object sender, RoutedEventArgs e) 
{
     await GetResults();

     // Show dialog/UI element.  This code has been marshaled
     // back to the UI thread because the SynchronizationContext
     // was captured behind the scenes when
     // await was called on the previous line.
     ...

     // Check continue, if true, then continue with another async task.
     if (_continue) await ContinueToGetResultsAsync();
}

private bool _continue = false;
private void buttonContinue_Click(object sender, RoutedEventArgs e)
{
    _continue = true;
}

private async Task GetResults()
{ 
     // Do lot of complex stuff that takes a long time
     // (e.g. contact some web services)
  ...
}

ContinueToGetResultsAsynclà phương pháp tiếp tục nhận được kết quả trong trường hợp nút của bạn được ấn. Nếu nút của bạn không được ấn , thì trình xử lý sự kiện của bạn không làm gì cả.


Chủ đề không đồng bộ là gì? Không có mã nào sẽ không chạy trên luồng UI, cả trong câu hỏi ban đầu và câu trả lời của bạn.
Svick

@svick Không đúng. GetResultstrả về a Task. awaitchỉ cần nói "chạy tác vụ và khi tác vụ hoàn thành, hãy tiếp tục mã sau này". Cho rằng có một bối cảnh đồng bộ hóa, cuộc gọi được sắp xếp lại theo luồng UI, khi nó được ghi lại trên await. awaitkhông giống như Task.Wait(), không phải trong ít nhất.
casperOne

Tôi đã không nói bất cứ điều gì về Wait(). Nhưng mã trong GetResults()sẽ chạy trên luồng UI ở đây, không có luồng nào khác. Nói cách khác, vâng, awaitvề cơ bản không chạy nhiệm vụ, như bạn nói, nhưng ở đây, nhiệm vụ đó cũng chạy trên luồng UI.
Svick

@svick Không có lý do nào để đưa ra giả định rằng tác vụ chạy trên luồng UI, tại sao bạn lại đưa ra giả định đó? Điều đó là có thể , nhưng không thể. Và cuộc gọi là hai cuộc gọi UI riêng biệt, về mặt kỹ thuật, một cuộc gọi đến awaitvà sau đó là mã sau awaitđó, không có chặn. Phần còn lại của mã được sắp xếp lại trong phần tiếp theo và được lên lịch thông qua SynchronizationContext.
casperOne

1
Đối với những người khác muốn xem thêm, hãy xem tại đây: chat.stackoverflow.com/rooms/17937 - @svick và tôi về cơ bản đã hiểu lầm nhau, nhưng đã nói điều tương tự.
casperOne

3

Stephen Toub đã xuất bản AsyncManualResetEventlớp học này trên blog của mình .

public class AsyncManualResetEvent 
{ 
    private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();

    public Task WaitAsync() { return m_tcs.Task; } 

    public void Set() 
    { 
        var tcs = m_tcs; 
        Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), 
            tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default); 
        tcs.Task.Wait(); 
    }

    public void Reset() 
    { 
        while (true) 
        { 
            var tcs = m_tcs; 
            if (!tcs.Task.IsCompleted || 
                Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs) 
                return; 
        } 
    } 
}

0

Với phần mở rộng phản ứng (Rx.Net)

var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => example.YourEvent += h,
                h => example.YourEvent -= h);

var res = await eventObservable.FirstAsync();

Bạn có thể thêm Rx với Nuget Gói System.Reactive

Mẫu thử nghiệm:

    private static event EventHandler<EventArgs> _testEvent;

    private static async Task Main()
    {
        var eventObservable = Observable
            .FromEventPattern<EventArgs>(
                h => _testEvent += h,
                h => _testEvent -= h);

        Task.Delay(5000).ContinueWith(_ => _testEvent?.Invoke(null, new EventArgs()));

        var res = await eventObservable.FirstAsync();

        Console.WriteLine("Event got fired");
    }

0

Tôi đang sử dụng lớp AsyncEvent của riêng mình cho các sự kiện được chờ đợi.

public delegate Task AsyncEventHandler<T>(object sender, T args) where T : EventArgs;

public class AsyncEvent : AsyncEvent<EventArgs>
{
    public AsyncEvent() : base()
    {
    }
}

public class AsyncEvent<T> where T : EventArgs
{
    private readonly HashSet<AsyncEventHandler<T>> _handlers;

    public AsyncEvent()
    {
        _handlers = new HashSet<AsyncEventHandler<T>>();
    }

    public void Add(AsyncEventHandler<T> handler)
    {
        _handlers.Add(handler);
    }

    public void Remove(AsyncEventHandler<T> handler)
    {
        _handlers.Remove(handler);
    }

    public async Task InvokeAsync(object sender, T args)
    {
        foreach (var handler in _handlers)
        {
            await handler(sender, args);
        }
    }

    public static AsyncEvent<T> operator+(AsyncEvent<T> left, AsyncEventHandler<T> right)
    {
        var result = left ?? new AsyncEvent<T>();
        result.Add(right);
        return result;
    }

    public static AsyncEvent<T> operator-(AsyncEvent<T> left, AsyncEventHandler<T> right)
    {
        left.Remove(right);
        return left;
    }
}

Để khai báo một sự kiện trong lớp làm tăng sự kiện:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

Để nâng cao các sự kiện:

if (MyNormalEvent != null) await MyNormalEvent.InvokeAsync(this, new EventArgs());
if (MyCustomEvent != null) await MyCustomEvent.InvokeAsync(this, new ProgressEventArgs());

Để đăng ký các sự kiện:

MyControl.Click += async (sender, args) => {
    // await...
}

MyControl.Click += (sender, args) => {
    // synchronous code
    return Task.CompletedTask;
}

1
Bạn đã hoàn toàn phát minh ra một cơ chế xử lý sự kiện mới. Có thể đây là những gì đại biểu trong .NET được dịch sang cuối cùng, nhưng không thể mong mọi người chấp nhận điều này. Có một kiểu trả về cho chính đại biểu (của sự kiện) có thể khiến mọi người bắt đầu. Nhưng nỗ lực tốt, thực sự thích nó được thực hiện tốt như thế nào.
nawfal

@nawfal Cảm ơn! Tôi đã sửa đổi nó để tránh trả lại một đại biểu. Nguồn có sẵn ở đây là một phần của Lara Web Engine, một sự thay thế cho Blazor.
cat_in_
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.