क्या किसी अन्य एसिंक्स विधि के बजाय किसी घटना की प्रतीक्षा करना संभव है?


156

मेरे C # / XAML मेट्रो ऐप में, एक बटन है जो एक लंबी चलने वाली प्रक्रिया को बंद कर देता है। इसलिए, जैसा कि सुझाया गया है, मैं यह सुनिश्चित करने के लिए async / प्रतीक्षा का उपयोग कर रहा हूं कि UI थ्रेड अवरुद्ध न हो।

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

कभी-कभी, GetResults के भीतर होने वाली सामग्री को जारी रखने से पहले अतिरिक्त उपयोगकर्ता इनपुट की आवश्यकता होगी। सादगी के लिए, मान लें कि उपयोगकर्ता को केवल "जारी रखें" बटन पर क्लिक करना है।

मेरा प्रश्न है: मैं GetResults के निष्पादन को इस तरह कैसे निलंबित कर सकता हूं कि यह किसी अन्य बटन के क्लिक जैसी घटना की प्रतीक्षा करता है ?

यहाँ एक बदसूरत तरीका है जिसे मैं देख रहा हूँ: "जारी रखें" बटन के लिए ईवेंट हैंडलर एक ध्वज सेट करता है ...

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

... और GetResults समय-समय पर यह चुनाव:

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

मतदान स्पष्ट रूप से भयानक है (चक्रों की व्यस्त प्रतीक्षा / बर्बादी) और मैं कुछ घटना-आधारित खोज रहा हूँ।

कोई विचार?

इस सरलीकृत उदाहरण में Btw, एक समाधान निश्चित रूप से GetResults () को दो भागों में विभाजित करने के लिए होगा, प्रारंभ बटन से पहला भाग और जारी रखें बटन से दूसरा भाग। वास्तव में, GetResults में होने वाली सामग्री अधिक जटिल है और निष्पादन के भीतर विभिन्न बिंदुओं पर विभिन्न प्रकार के उपयोगकर्ता इनपुट की आवश्यकता हो सकती है। इसलिए कई तरीकों में तर्क को तोड़ना गैर-तुच्छ होगा।

जवाबों:


225

आप एक संकेत के रूप में सेमाफोरस्लिम क्लास के एक उदाहरण का उपयोग कर सकते हैं :

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

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

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

वैकल्पिक रूप से, आप TaskCompletionSource <T> क्लास का उपयोग कर सकते हैं टास्क बनाने के लिए क्लास <T> जो बटन क्लिक के परिणाम का प्रतिनिधित्व करता है:

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)का समर्थन नहीं लगता WaitAsync()
svick

3
@DanielHilgarth नहीं, आप नहीं कर सकते। asyncइसका मतलब यह नहीं है कि "एक अलग धागे पर चलता है", या ऐसा कुछ। इसका मतलब सिर्फ यह है कि "आप awaitइस विधि में उपयोग कर सकते हैं "। और इस मामले में, अंदर अवरुद्ध GetResults()वास्तव में UI थ्रेड को ब्लॉक करेगा।
svick

2
@ Gabe awaitने इस बात की गारंटी नहीं दी है कि एक और धागा बनाया गया है, लेकिन यह उस बयान के बाद बाकी सब कुछ का कारण बनता है जिस पर Taskया आप जिस कॉल awaitपर प्रतीक्षा करते हैं उस पर निरंतरता के रूप में चलना चाहिए । प्रायः, यह कुछ अतुल्यकालिक आपरेशन की तरह है, जो आईओ पूरा होने, या कुछ है कि हो सकता है है एक और धागा पर।
कास्परऑन

16
+1। मुझे यह देखना था, इसलिए यदि कोई अन्य इच्छुक हो तो: SemaphoreSlim.WaitAsyncकेवल Waitएक थ्रेड पूल थ्रेड पर धक्का न दें । SemaphoreSlims की एक उचित कतार है Taskजिसे लागू करने के लिए उपयोग किया जाता है WaitAsync
स्टीफन क्लीयर

14
TaskCompletionSource <T> + wait .Task + .SetResult () मेरे परिदृश्य के लिए सही समाधान निकला - धन्यवाद! :-)
मैक्स

75

जब आपके पास एक असामान्य चीज होती है awaitजिस पर आपको जरूरत होती है, तो सबसे आसान उत्तर अक्सर TaskCompletionSource(या कुछ- asyncआधारित आदिम आधारित होता है TaskCompletionSource)।

इस मामले में, आपकी आवश्यकता काफी सरल है, इसलिए आप TaskCompletionSourceसीधे उपयोग कर सकते हैं:

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

तार्किक रूप से, TaskCompletionSourceएक की तरह है async ManualResetEvent, सिवाय इसके कि आप केवल एक बार घटना को "सेट" कर सकते हैं और घटना का "परिणाम" हो सकता है (इस मामले में, हम इसका उपयोग नहीं कर रहे हैं, इसलिए हम सिर्फ परिणाम सेट करते हैं null)।


5
चूँकि मैं "ईवेंट का इंतजार करता हूं" जैसा कि मूल रूप से 'टास्क में लपेटे ईएपी' जैसी स्थिति है, मैं निश्चित रूप से इस दृष्टिकोण को पसंद करूंगा। IMHO, यह निश्चित रूप से सरल / आसान-से-कारण कोड के बारे में है।
जेम्स मैनिंग

8

यहां एक उपयोगिता वर्ग है जिसका मैं उपयोग करता हूं:

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

और यहां बताया गया है कि मैं इसका उपयोग कैसे करता हूं:

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

// ... make it change ...

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

1
मुझे नहीं पता कि यह कैसे काम करता है। मेरे कस्टम हैंडलर को असिंक्रोनस तरीके से सुनने की विधि कैसे है? new Task(() => { });तुरन्त पूरा नहीं होगा ?
नवाफाल

5

सरल सहायक श्रेणी:

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

उपयोग:

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

1
आप सदस्यता को कैसे साफ करेंगे example.YourEvent?
डेनिस पी

@DenisP शायद EventAwaiter के लिए कंस्ट्रक्टर में ईवेंट पास करें?
CJBrew

@ डेनिसपी I ने संस्करण में सुधार किया और एक छोटा परीक्षण चलाया।
फेलिक्स कील

मैं परिस्थितियों के आधार पर, साथ ही आईडीआईसोपायरी को जोड़कर देख सकता था। इसके अलावा, दो बार ईवेंट में टाइप करने से बचने के लिए, हम ईवेंट नाम को पास करने के लिए परावर्तन का भी उपयोग कर सकते हैं, इसलिए तब उपयोग और भी सरल हो जाता है। अन्यथा, मुझे पैटर्न पसंद है, धन्यवाद।
डेनिस पी

4

आदर्श रूप में, आप नहीं । जब आप निश्चित रूप से एसिंक्स थ्रेड को ब्लॉक कर सकते हैं, तो यह संसाधनों की बर्बादी है, और आदर्श नहीं है।

उस कैनोनिकल उदाहरण पर विचार करें जहां उपयोगकर्ता लंच पर जाता है जबकि बटन क्लिक होने की प्रतीक्षा कर रहा है।

यदि आपने उपयोगकर्ता से इनपुट की प्रतीक्षा करते हुए अपने अतुल्यकालिक कोड को रोक दिया है, तो यह केवल संसाधनों को बर्बाद कर रहा है जबकि उस धागे को रोक दिया गया है।

उस ने कहा, यह बेहतर है यदि आपके अतुल्यकालिक ऑपरेशन में, आप उस स्थिति को सेट करते हैं जिसे आपको उस बिंदु पर बनाए रखने की आवश्यकता है जहां बटन सक्षम है और आप एक क्लिक पर "प्रतीक्षा" कर रहे हैं। उस बिंदु पर, आपकी GetResultsविधि बंद हो जाती है

फिर, जब बटन पर क्लिक किया जाता है, तो आपके द्वारा संग्रहीत स्थिति के आधार पर, आप काम जारी रखने के लिए एक और अतुल्यकालिक कार्य शुरू करते हैं।

क्योंकि SynchronizationContextईवेंट हैंडलर में कॉल करने के लिए कब्जा कर लिया जाएगा GetResults(कंपाइलर इसका उपयोग awaitकीवर्ड के उपयोग के परिणामस्वरूप करेगा , और तथ्य यह है कि SynchronizationContext.Current गैर-अशक्त होना चाहिए, बशर्ते कि आप UI एप्लिकेशन में हैं, आप उपयोग कर सकते हैं async/await जैसे:

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

ContinueToGetResultsAsyncवह विधि है जो आपके बटन को धकेलने की स्थिति में परिणाम प्राप्त करना जारी रखती है। यदि आपका बटन पुश नहीं किया जाता है , तो आपका ईवेंट हैंडलर कुछ नहीं करता है।


क्या async धागा? ऐसा कोई कोड नहीं है जो मूल प्रश्न और आपके उत्तर दोनों में UI थ्रेड पर नहीं चलेगा।
13

@svick सच नहीं है। GetResultsएक रिटर्न Taskawaitबस कहता है "कार्य को चलाएं, और जब कार्य पूरा हो जाए, तो इसके बाद कोड जारी रखें"। यह देखते हुए कि एक तुल्यकालन संदर्भ है, कॉल को UI थ्रेड पर वापस भेज दिया जाता है, क्योंकि यह पर कैप्चर किया गया है awaitawaitहै नहीं के रूप में ही Task.Wait()नहीं, कम से कम में।
कैस्परऑन

मैंने कुछ नहीं कहा Wait()। लेकिन कोड GetResults()यूआई थ्रेड पर यहां चलेगा, कोई अन्य थ्रेड नहीं है। दूसरे शब्दों में, हां, awaitमूल रूप से कार्य चलाता है, जैसे आप कहते हैं, लेकिन यहां, वह कार्य भी UI थ्रेड पर चलता है।
svick

@svick इस धारणा को बनाने का कोई कारण नहीं है कि कार्य UI थ्रेड पर चलता है, आप उस धारणा को क्यों बनाते हैं? यह संभव है , लेकिन संभावना नहीं है। और कॉल दो अलग-अलग यूआई कॉल है, तकनीकी रूप से, एक तक awaitऔर फिर बाद में कोड await, कोई अवरोध नहीं है। बाकी कोड एक निरंतरता में वापस मार्श किया जाता है और कोड के माध्यम से निर्धारित किया जाता है SynchronizationContext
कैस्परऑन

1
जो लोग अधिक देखना चाहते हैं, उनके लिए यहां देखें: chat.stackoverflow.com/rooms/17937 - @svick और मैं मूल रूप से एक-दूसरे को गलत समझते थे, लेकिन एक ही बात कह रहे थे।
कास्परऑन

3

स्टीफन टूब ने अपने ब्लॉग पर इस AsyncManualResetEventवर्ग को प्रकाशित किया ।

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

साथ रिएक्टिव एक्सटेंशन (Rx.Net)

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

var res = await eventObservable.FirstAsync();

आप Nuget Package System.eactive के साथ Rx जोड़ सकते हैं

परीक्षण किया गया नमूना:

    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

मैं अपने खुद के AsyncEvent क्लास का उपयोग कर रहा हूँ प्रतीक्षा करने योग्य घटनाओं के लिए।

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

घटनाओं को बढ़ाने वाले वर्ग में एक घटना घोषित करने के लिए:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

घटनाओं को बढ़ाने के लिए:

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

घटनाओं की सदस्यता के लिए:

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

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

1
आपने पूरी तरह से एक नया ईवेंट हैंडलर तंत्र का आविष्कार किया है। शायद यह वही है जो .NET में प्रतिनिधियों को अंततः अनुवादित किया जाता है, लेकिन लोगों को इसे अपनाने की उम्मीद नहीं कर सकते हैं। प्रतिनिधि (घटना के) के लिए एक वापसी प्रकार होने से लोगों को शुरू करने के लिए बंद कर सकते हैं। लेकिन अच्छा प्रयास, वास्तव में यह कितना अच्छा होता है।
nawfal

@nawfal धन्यवाद! मैंने प्रतिनिधि के वापस आने से बचने के लिए इसे संशोधित किया है। यह स्रोत लारा वेब इंजन के हिस्से के रूप में उपलब्ध है , जो ब्लेज़र का विकल्प है।
cat_in_hat
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.