เป็นไปได้ไหมที่จะรอเหตุการณ์แทนที่จะเป็นวิธีการอื่นแบบ async?


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

คุณสามารถใช้อินสแตนซ์ของSemaphoreSlim Classเป็นสัญญาณ:

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

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

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

อีกทางหนึ่งคุณสามารถใช้อินสแตนซ์ของTaskCompletionSource <T> Classเพื่อสร้างTask <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ในตัวเองไม่ได้ guaranted ที่หัวข้ออื่นจะถูกสร้างขึ้น แต่จะทำให้ทุกอย่างอื่นหลังจากคำสั่งให้ทำงานเป็นความต่อเนื่องในการTaskหรือ awaitable ให้คุณโทรawaitได้ที่ บ่อยกว่านั้นมันเป็นการดำเนินการแบบอะซิงโครนัสบางประเภทซึ่งอาจเป็นความสมบูรณ์ของ IO หรือสิ่งที่อยู่ในเธรดอื่น
casperOne

16
+1 ฉันต้องค้นหามันดังนั้นในกรณีที่คนอื่นสนใจ: SemaphoreSlim.WaitAsyncไม่เพียงแค่กดWaitลงบนเธรดพูลเธรด SemaphoreSlimมีคิวที่เหมาะสมของของที่จะใช้ในการดำเนินการTask WaitAsync
Stephen Cleary

14
TaskCompletionSource <T> + รอ .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
เนื่องจากฉันแยกวิเคราะห์ "รอเหตุการณ์" โดยทั่วไปเป็นสถานการณ์เดียวกับ 'ห่อ EAP ในงาน' ฉันจึงชอบวิธีนี้มากกว่า IMHO มันเป็นรหัสที่เรียบง่ายกว่า
James Manning

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(() => { });เสร็จทันทีหรือไม่
nawfal

5

Simple Helper Class:

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อย่างไร
เดนิส P

@DenisP อาจส่งผ่านเหตุการณ์ไปยังตัวสร้างสำหรับ EventAwaiter หรือไม่
CJBrew

@DenisP ฉันปรับปรุงรุ่นและเรียกใช้การทดสอบสั้น ๆ
เฟลิกซ์ Keil

ฉันเห็นการเพิ่ม IDisposable ด้วยขึ้นอยู่กับสถานการณ์ นอกจากนี้เพื่อหลีกเลี่ยงการพิมพ์เหตุการณ์สองครั้งเราสามารถใช้ Reflection เพื่อส่งผ่านชื่อเหตุการณ์ดังนั้นการใช้งานจึงง่ายขึ้น มิฉะนั้นฉันชอบรูปแบบขอบคุณ
เดนิส P

4

เป็นการดีที่คุณทำไม่ได้ แม้ว่าคุณจะสามารถบล็อกเธรด async ได้ แต่ก็เป็นการสิ้นเปลืองทรัพยากรและไม่เหมาะ

พิจารณาตัวอย่างที่ยอมรับซึ่งผู้ใช้ไปทานอาหารกลางวันในขณะที่รอปุ่มคลิก

หากคุณหยุดโค้ดอะซิงโครนัสของคุณขณะรออินพุตจากผู้ใช้นั่นเป็นเพียงการสูญเสียทรัพยากรในขณะที่เธรดนั้นหยุดชั่วคราว

ที่กล่าวมาจะดีกว่าถ้าในการดำเนินการแบบอะซิงโครนัสของคุณคุณตั้งค่าสถานะที่คุณต้องรักษาจนถึงจุดที่เปิดใช้งานปุ่มและคุณ "รอ" ที่คลิก ณ จุดที่คุณGetResultsวิธีหยุด

จากนั้นเมื่อปุ่มถูกคลิกบนพื้นฐานของรัฐที่คุณเก็บไว้คุณจะเริ่มต้นอีกงานที่ไม่ตรงกันจะยังคงทำงาน

เพราะSynchronizationContextจะได้รับการบันทึกในตัวจัดการเหตุการณ์ที่โทรGetResults(คอมไพเลอร์จะทำเช่นนี้เป็นผลมาจากการใช้awaitคำหลักที่ถูกนำมาใช้และความจริงที่ว่าSynchronizationContext.Currentควรจะ null ไม่ใช่ให้คุณอยู่ในการประยุกต์ใช้ 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 ทั้งในคำถามเดิมและในคำตอบของคุณ
svick

@svick ไม่จริง ผลตอบแทน GetResults เพียงแค่บอกว่า "เรียกใช้งานและเมื่องานเสร็จแล้วให้ดำเนินการต่อรหัสหลังจากนี้" ระบุว่ามีบริบทการประสานโทรเป็นประดามีกลับไปที่หัวข้อ UI เป็นมันจับบน คือไม่ได้เช่นเดียวกับไม่ได้อยู่ในอย่างน้อย TaskawaitawaitawaitTask.Wait()
casperOne

Wait()ผมไม่ได้พูดอะไรเกี่ยวกับ แต่รหัสในGetResults()จะทำงานบนเธรด UI ที่นี่ไม่มีเธรดอื่น ในคำอื่น ๆ ใช่awaitโดยทั่วไปจะทำงานตามที่คุณพูด แต่ที่นี่งานนั้นยังทำงานบนเธรด UI
svick

@svick ไม่มีเหตุผลที่จะทำให้การสันนิษฐานว่างานที่ทำงานบนเธรด UI ทำไมคุณทำสมมติฐานที่? เป็นไปได้แต่ไม่น่าเป็นไปได้ และการโทรคือการโทร UI สองครั้งแยกกันในทางเทคนิคหนึ่งสายไปจนถึงawaitรหัสหลังจากawaitนั้นจะไม่มีการปิดกั้น SynchronizationContextส่วนที่เหลือของรหัสประดามีกลับมาอยู่ในความต่อเนื่องและกำหนดเวลาผ่าน
casperOne

1
สำหรับคนอื่นที่ต้องการดูเพิ่มเติมดูที่นี่: chat.stackoverflow.com/rooms/17937 - @svick และฉันก็เข้าใจผิดกันโดยทั่วไป แต่ก็พูดในสิ่งเดียวกัน
casperOne

3

สตีเฟ่น Toub ตีพิมพ์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

ด้วยReactive Extensions (Rx.Net)

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

var res = await eventObservable.FirstAsync();

คุณสามารถเพิ่ม Rx ด้วย Nuget Package System.Reactive

ตัวอย่างการทดสอบ:

    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 ขอบคุณ! ฉันได้ทำการแก้ไขตั้งแต่เพื่อหลีกเลี่ยงการส่งผู้แทนกลับคืน แหล่งที่มามีอยู่ที่นี่เป็นส่วนหนึ่งของ Lara Web Engine ทางเลือกของ Blazor
cat_in_hat
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.