Başka bir zaman uyumsuz yöntem yerine bir olay beklenebilir mi?


156

C # / XAML metro uygulamamda, uzun süren bir işlemi başlatan bir düğme var. Bu nedenle, önerildiği gibi, UI iş parçacığının engellenmediğinden emin olmak için async / await kullanıyorum:

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

Bazen, GetResults içinde gerçekleşen şeyler devam edebilmek için ek kullanıcı girişi gerektirir. Basitlik açısından, kullanıcının "devam" düğmesini tıklaması gerektiğini varsayalım.

Benim sorum: GetResults'un yürütülmesini , başka bir düğmeyi tıklamak gibi bir etkinliği bekleyecek şekilde nasıl askıya alabilirim ?

İşte aradığım şeyi elde etmek için çirkin bir yol: Devam et için olay işleyici "düğmesi bir bayrak ayarlar ...

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

... ve GetResults belirli aralıklarla yoklar:

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

Yoklama açıkça korkunç (meşgul bekleme / döngü israf) ve ben olay tabanlı bir şey arıyorum.

Herhangi bir fikir?

Bu basitleştirilmiş örnekte, bir çözüm elbette GetResults'u () iki parçaya bölmek, ilk düğmeyi başlat düğmesinden ve ikinci parçayı devam düğmesinden çağırmak olacaktır. Gerçekte, GetResults'ta gerçekleşen şeyler daha karmaşıktır ve yürütme içindeki farklı noktalarda farklı kullanıcı girişi türleri gerekebilir. Bu nedenle, mantığı birden çok yönteme ayırmak önemsiz olacaktır.

Yanıtlar:


225

SemaphoreSlim Sınıfının bir örneğini sinyal olarak kullanabilirsiniz:

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

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

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

Alternatif olarak, düğme tıklamasının sonucunu temsil eden bir Görev <T> oluşturmak için TaskCompletionSource <T> Sınıfının bir örneğini kullanabilirsiniz :

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)desteklemiyor gibi görünüyor WaitAsync().
svick

3
@DanielHilgarth Hayır, yapamazsın. async"farklı bir iş parçacığında çalışır" veya bunun gibi bir şey anlamına gelmez. Bu sadece “ awaitbu yöntemde kullanabilirsiniz” anlamına gelir . Ve bu durumda, içeride engelleme GetResults()aslında UI iş parçacığını engeller.
svick

2
@Gabe awaitkendi başına başka bir iş parçacığının oluşturulduğunu garanti etmez, ancak ifadeden sonra geri kalan Taskveya beklediğinizde beklemede olan her şeyin çalışmasına neden olur await. Çoğu zaman, öyle bazı IO tamamlama veya bir şey olabilir asenkron operasyon tür olan başka iş parçacığı üzerinde.
casperOne

16
+1. Ben sadece diğerleri ilgilenen durumunda bu kadar bakmak zorunda kaldı: SemaphoreSlim.WaitAsyncsadece Waitbir iş parçacığı havuzu iplik üzerine itmek değil . uygulamak için kullanılan SemaphoreSlimuygun bir Tasks sırasına sahiptir WaitAsync.
Stephen Cleary

14
TaskCompletionSource <T> + bekliyor .Task + .SetResult () senaryom için mükemmel bir çözüm olarak ortaya çıkıyor - teşekkürler! :-)
Max

75

Alışılmadık bir şey olduğunda await, en kolay cevap genellikle TaskCompletionSource(veya bazı asynctemelli ilkel TaskCompletionSource).

Bu durumda, ihtiyacınız oldukça basittir, böylece TaskCompletionSourcedoğrudan kullanabilirsiniz :

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

Mantıksal olarak, TaskCompletionSourcebir async ManualResetEventolay gibidir , ancak etkinliği yalnızca bir kez "ayarlayabilir" ve etkinliğin "sonucu" olabilir (bu durumda kullanmıyoruz, bu yüzden sonucu sadece olarak ayarladık null).


5
“Bir olayı bekliyor” temelde 'görevde EAP'yi sarmak' ile aynı durum ayrıştırıldığım için kesinlikle bu yaklaşımı tercih ederim. IMHO, kesinlikle daha basit / mantıklı bir kod.
James Manning

8

İşte kullandığım bir yardımcı sınıf:

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

İşte nasıl kullanacağım:

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

// ... make it change ...

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

1
Bunun nasıl çalıştığını bilmiyorum. Listen yöntemi özel işleyicimi eşzamansız olarak nasıl yürütüyor? Olmaz new Task(() => { });anında tamamlanacak?
nawfal

5

Basit Yardımcı Sınıfı:

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

Kullanımı:

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

1
Aboneliğini nasıl temizlersiniz example.YourEvent?
Denis P

@DenisP belki de EventAwaiter için yapıcıya olayı iletir
CJBrew

@DenisP Sürümü geliştirdim ve kısa bir test yaptım.
Felix Keil

Koşullara bağlı olarak IDisposable ekleyebiliyordum. Ayrıca, olayı iki kez yazmak zorunda kalmamak için, olay adını iletmek için Reflection'ı da kullanabiliriz, bu nedenle kullanım daha da basittir. Aksi takdirde, kalıbı seviyorum, teşekkür ederim.
Denis P

4

İdeali, yok . Async iş parçacığını kesinlikle engelleyebilirsiniz, ancak bu bir kaynak kaybıdır ve ideal değildir.

Düğme tıklanmayı beklerken kullanıcının öğle yemeğine gittiği kanonik örneği düşünün.

Kullanıcının girdisini beklerken eşzamansız kodunuzu durdurduysanız, iş parçacığı duraklatıldığında kaynakları boşa harcar.

Bununla birlikte, eşzamansız işleminizde, bakmanız gereken durumu düğmenin etkinleştirildiği noktaya ayarlamanız ve bir tıklamayla "beklemeniz" daha iyi olur. Bu noktada, GetResultsyönteminiz durur .

Düğme Sonra, bir tıklandığında, sen depoladığı durumuna dayalı Başlamadan başka asenkron görevi çalışmaya devam.

Çağıran SynchronizationContextolay işleyicisinde yakalanacağı için GetResults(derleyici, kullanılan awaitanahtar kelimeyi kullanmanın bir sonucu olarak ve bir UI uygulamasında olduğunuzda SynchronizationContext.Current'in boş olmamalıdır), kullanabilir async/await beğenebilir:

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

ContinueToGetResultsAsyncdüğmesine basıldığında sonuçları almaya devam eden yöntemdir. Düğmeniz ise değil itti, sonra olay işleyicisi hiçbir şey yapmaz.


Ne zaman uyumsuz iş parçacığı? Dair hiçbir kod yoktur değil UI iş parçacığı üzerinde çalışacak özgün söz konusu ve cevap hem de.
svick

@svick Doğru değil. GetResultsdöndürür a Task. awaitsadece "görevi çalıştır ve görev tamamlandığında, bundan sonra koda devam et" der. Bir senkronizasyon bağlamı olduğu göz önüne alındığında, çağrı, yakalandığı gibi UI iş parçacığına geri alınır await. awaitolduğu değil aynı Task.Wait()hiçbir şekilde.
casperOne

Hakkında hiçbir şey söylemedim Wait(). Ancak içindeki kod GetResults()burada UI iş parçacığında çalışacak, başka iş parçacığı yok. Başka bir deyişle, evet, awaittemelde, sizin söylediğiniz gibi görevi çalıştırır, ancak burada, bu görev UI iş parçacığında da çalışır.
svick

@svick Görevin UI iş parçacığında çalıştığını varsaymak için bir neden yok, neden bu varsayımı yapıyorsunuz? Bu var mümkün , ama muhtemel. Ve çağrı, iki ayrı UI çağrısıdır, teknik olarak, bir taneye kadar awaitve daha sonra kod await, engelleme yoktur. Kodun geri kalanı devam ettirilir ve SynchronizationContext.
casperOne

1
Daha fazlasını görmek isteyenler için buraya bakın: chat.stackoverflow.com/rooms/17937 - @svick ve ben temelde birbirimizi yanlış anladık, ama aynı şeyi söylüyorduk.
casperOne

3

Stephen Toub bu yayınlanan AsyncManualResetEventsınıfını kendi blogunda .

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

İle Reaktif Uzantıları (Rx.Net)

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

var res = await eventObservable.FirstAsync();

Nuget Paket Sistemi ile Rx ekleyebilirsiniz.

Test edilmiş örnek:

    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

Beklenebilecek olaylar için kendi AsyncEvent sınıfımı kullanıyorum.

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

Sınıfta olayları arttıran bir olay beyan etmek için:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

Olayları gündeme getirmek için:

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

Etkinliklere abone olmak için:

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

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

1
Tamamen yeni bir olay giderici mekanizma icat ettiniz. Belki de .NET'teki delegelerin sonunda çevrildikleri budur, ancak insanların bunu benimsemelerini bekleyemezler. Delege (etkinliğin) kendisi için bir geri dönüş türüne sahip olmak insanları başlatabilir. Ama iyi çaba, gerçekten ne kadar iyi yapıldığını seviyorum.
nawfal

@nawfal Teşekkürler! Temsilciyi iade etmemek için değiştirdim. Kaynak burada Blazor'a alternatif olan Lara Web Engine'in bir parçası olarak mevcuttur .
cat_in_hat
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.