Apakah mungkin untuk menunggu acara daripada metode async lain?

156

Di aplikasi metro C # / XAML saya, ada tombol yang memulai proses yang sudah berjalan lama. Jadi, seperti yang disarankan, saya menggunakan async / menunggu untuk memastikan utas UI tidak diblokir:

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

Kadang-kadang, hal-hal yang terjadi di dalam GetResults akan membutuhkan input pengguna tambahan sebelum dapat melanjutkan. Untuk kesederhanaan, katakanlah pengguna hanya perlu mengklik tombol "lanjutkan".

Pertanyaan saya adalah: bagaimana saya bisa menunda eksekusi GetResults sedemikian rupa sehingga menunggu acara seperti klik tombol lain?

Inilah cara yang jelek untuk mencapai apa yang saya cari: pengendali acara untuk tombol "lanjut menetapkan tanda ...

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

... dan GetResults melakukan polling secara berkala:

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

Pemungutan suara jelas mengerikan (menunggu sibuk / buang-buang siklus) dan saya sedang mencari sesuatu yang berbasis acara.

Ada ide?

Btw dalam contoh sederhana ini, satu solusi tentu saja untuk membagi GetResult () menjadi dua bagian, memanggil bagian pertama dari tombol mulai dan bagian kedua dari tombol melanjutkan. Pada kenyataannya, hal-hal yang terjadi di GetResults lebih kompleks dan berbagai jenis input pengguna dapat diperlukan pada titik yang berbeda dalam eksekusi. Jadi memecah logika menjadi beberapa metode akan menjadi non-sepele.

Maks
sumber

Jawaban:

225

Anda dapat menggunakan instance dari Kelas SemaphoreSlim sebagai sinyal:

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

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

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

Atau, Anda dapat menggunakan turunan dari kelas TaskCompletionSource <T> untuk membuat Tugas <T> yang mewakili hasil klik tombol:

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

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

// wait for task somewhere else
await tcs.Task;
dtb
sumber
7
@DanielHilgarth ManualResetEvent(Slim)sepertinya tidak mendukung WaitAsync().
svick
3
@DanielHilgarth Tidak, Anda tidak bisa. asynctidak berarti "berjalan di utas yang berbeda", atau sesuatu seperti itu. Itu hanya berarti "Anda dapat menggunakan awaitmetode ini". Dan dalam hal ini, memblokir di dalam GetResults()sebenarnya akan memblokir utas UI.
svick
2
@ Gabe awaitdengan sendirinya tidak menjamin bahwa utas lainnya dibuat, tetapi hal itu menyebabkan semua yang lain setelah pernyataan berjalan sebagai kelanjutan dari Taskatau menunggu yang Anda panggil await. Lebih sering daripada tidak, itu adalah beberapa jenis operasi asynchronous, yang bisa IO selesai, atau sesuatu yang merupakan di thread lain.
casperOne
16
+1. Saya harus melihat ini, jadi kalau-kalau ada yang tertarik: SemaphoreSlim.WaitAsynctidak hanya mendorong Waitke thread pool thread. SemaphoreSlimmemiliki antrian Tasks yang tepat yang digunakan untuk mengimplementasikan WaitAsync.
Stephen Cleary
14
TaskCompletionSource <T> + menunggu .Task + .SetResult () ternyata menjadi solusi sempurna untuk skenario saya - terima kasih! :-)
Max
75

Ketika Anda memiliki hal yang tidak biasa yang Anda perlukan await, jawaban yang paling mudah adalah sering TaskCompletionSource(atau asyncprimitif yang diaktifkan berdasarkan TaskCompletionSource).

Dalam hal ini, kebutuhan Anda cukup sederhana, sehingga Anda bisa TaskCompletionSourcelangsung menggunakan :

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

Secara logis, TaskCompletionSourceseperti async ManualResetEvent, kecuali bahwa Anda hanya dapat "mengatur" acara satu kali dan acara tersebut dapat memiliki "hasil" (dalam hal ini, kami tidak menggunakannya, jadi kami hanya mengatur hasilnya ke null).

Stephen Cleary
sumber
5
Karena saya mengurai "menunggu acara" karena pada dasarnya situasi yang sama seperti 'membungkus EAP dalam tugas', saya pasti lebih suka pendekatan ini. IMHO, ini jelas lebih sederhana / lebih mudah untuk alasan tentang kode.
James Manning
8

Berikut adalah kelas utilitas yang saya gunakan:

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

Dan inilah cara saya menggunakannya:

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

// ... make it change ...

await itChanged.Successfully;
someObject.PropertyChanged -= itChanged.Listen;
Anders Skovborg
sumber
1
Saya tidak tahu cara kerjanya. Bagaimana metode Dengarkan menjalankan penangan kustom saya secara sinkron? Tidak new Task(() => { });akan selesai secara instan?
nawfal
5

Kelas Pembantu Sederhana:

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

Pemakaian:

var valueChangedEventAwaiter = new EventAwaiter<YourEventArgs>(
                            h => example.YourEvent += h,
                            h => example.YourEvent -= h);
await valueChangedEventAwaiter.Task;
Felix Keil
sumber
1
Bagaimana Anda membersihkan langganan example.YourEvent?
Denis P
@DenisP mungkin mengalihkan acara menjadi konstruktor untuk EventAwaiter?
CJBrew
@DenisP Saya memperbaiki versi dan menjalankan tes singkat.
Felix Keil
Saya bisa melihat menambahkan IDisposable juga, tergantung pada keadaan. Juga, untuk menghindari keharusan mengetikkan acara dua kali, kita juga bisa menggunakan Refleksi untuk meneruskan nama acara, jadi penggunaannya bahkan lebih sederhana. Kalau tidak, saya suka polanya, terima kasih.
Denis P
4

Idealnya, Anda tidak melakukannya . Meskipun Anda tentu saja dapat memblokir utas async, itu adalah pemborosan sumber daya, dan tidak ideal.

Pertimbangkan contoh kanonik di mana pengguna pergi makan siang sementara tombol sedang menunggu untuk diklik.

Jika Anda telah menghentikan kode asinkron Anda sambil menunggu input dari pengguna, maka itu hanya membuang sumber daya sementara utas itu dijeda.

Yang mengatakan, lebih baik jika dalam operasi asinkron Anda, Anda mengatur status yang perlu Anda pertahankan ke titik di mana tombol diaktifkan dan Anda "menunggu" di klik. Pada titik itu, GetResultsmetode Anda berhenti .

Kemudian, ketika tombol tersebut diklik, berdasarkan pada yang Anda telah disimpan, Anda mulai tugas asynchronous lain untuk melanjutkan pekerjaan.

Karena SynchronizationContextakan ditangkap dalam event handler yang memanggil GetResults(kompiler akan melakukan ini sebagai hasil dari menggunakan awaitkata kunci yang digunakan, dan fakta bahwa SynchronizationContext.Current harus non-nol, mengingat Anda berada dalam aplikasi UI), Anda dapat menggunakan async/await seperti itu:

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

ContinueToGetResultsAsyncadalah metode yang terus mendapatkan hasil jika tombol Anda ditekan. Jika tombol Anda tidak ditekan, maka pengendali acara Anda tidak melakukan apa pun.

casperOne
sumber
Apa utas async? Tidak ada kode yang tidak akan berjalan di utas UI, baik dalam pertanyaan asli maupun dalam jawaban Anda.
svick
@vick Tidak benar. GetResultsmengembalikan a Task. awaithanya mengatakan "jalankan tugas, dan ketika tugas selesai, lanjutkan kode setelah ini". Mengingat bahwa ada konteks sinkronisasi, panggilan tersebut dikembalikan ke utas UI, karena ditangkap pada await. awaitadalah tidak sama dengan Task.Wait(), tidak sedikit.
casperOne
Saya tidak mengatakan apa-apa tentang itu Wait(). Tetapi kode di GetResults()akan berjalan pada utas UI di sini, tidak ada utas lainnya. Dengan kata lain, ya, awaitpada dasarnya menjalankan tugas, seperti yang Anda katakan, tetapi di sini, tugas itu juga berjalan di utas UI.
svick
@ svick Tidak ada alasan untuk membuat asumsi bahwa tugas berjalan di utas UI, mengapa Anda membuat asumsi itu? Itu mungkin , tetapi tidak mungkin. Dan panggilan itu adalah dua panggilan UI yang terpisah, secara teknis, satu hingga awaitdan kemudian kode setelahnya await, tidak ada pemblokiran. Sisa kode di marshal kembali dalam kelanjutan dan dijadwalkan melalui SynchronizationContext.
casperOne
1
Untuk yang lain yang ingin melihat lebih banyak, lihat di sini: chat.stackoverflow.com/rooms/17937 - @svick dan saya pada dasarnya salah paham satu sama lain, tetapi mengatakan hal yang sama.
casperOne
3

Stephen Toub menerbitkan AsyncManualResetEventkelas ini di blog-nya .

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; 
        } 
    } 
}
Drew Noakes
sumber
0

Dengan Ekstensi Reaktif (Rx.Net)

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

var res = await eventObservable.FirstAsync();

Anda dapat menambahkan Rx dengan Sistem Paket Nuget. Reaktif

Sampel yang diuji:

    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");
    }
Felix Keil
sumber
0

Saya menggunakan kelas AsyncEvent saya sendiri untuk acara yang bisa ditunggu.

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

Untuk mendeklarasikan suatu peristiwa di kelas yang menimbulkan peristiwa:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

Untuk mengangkat acara:

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

Untuk berlangganan acara:

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

MyControl.Click += (sender, args) => {
    // synchronous code
    return Task.CompletedTask;
}
cat_in_hat
sumber
1
Anda telah sepenuhnya menemukan mekanisme pengendali event baru. Mungkin inilah yang akhirnya diterjemahkan oleh delegasi dalam .NET, tetapi tidak dapat mengharapkan orang untuk mengadopsi ini. Memiliki tipe kembali untuk delegasi (acara) itu sendiri dapat menunda orang untuk memulai. Namun usaha yang baik, sangat suka seberapa baik hal itu dilakukan.
nawfal
@nawfal Terima kasih! Saya telah memodifikasinya sejak untuk menghindari mengembalikan delegasi. Sumber tersedia di sini sebagai bagian dari Lara Web Engine, sebuah alternatif untuk Blazor.
cat_in_hat