다른 비동기 메서드 대신 이벤트를 기다리는 것이 가능합니까?


156

내 C # / XAML 메트로 앱에는 장기 실행 프로세스를 시작하는 버튼이 있습니다. 따라서 권장대로 UI 스레드가 차단되지 않도록 async / await를 사용하고 있습니다.

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 클래스 의 인스턴스를 신호로 사용할 수 있습니다 .

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

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

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

또는 TaskCompletionSource <T> 클래스 의 인스턴스를 사용 하여 버튼 클릭 결과를 나타내는 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자체는 다른 스레드가 생성되었다는 것을 보증하지는 않지만 명령문 이후 의 다른 모든 항목 은 Task호출하거나 대기 하는 연속으로 실행되도록 await합니다. 보다 더 자주는 아니지만, 그것은이다 일부 IO 완료, 또는 무언가가 될 수 비동기 작업의 종류, 이다 다른 스레드가.
casperOne

16
+1. 다른 사람이 관심이있는 경우를 대비하여 이것을 찾아야했습니다 . 스레드 풀 스레드로 SemaphoreSlim.WaitAsync밀어 넣지 마십시오 Wait. 구현하는 데 사용되는 SemaphoreSlim적절한 대기열이 있습니다. TaskWaitAsync
Stephen Cleary

14
TaskCompletionSource <T> + await .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
어떻게 작동하는지 모르겠습니다. Listen 메서드는 어떻게 내 사용자 지정 처리기를 비동기 적으로 실행합니까? new Task(() => { });즉시 완료 되지 않습니까?
nawfal '12

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어떻게 정리 하시겠습니까?
Denis P

@DenisP는 아마도 EventAwaiter의 생성자로 이벤트를 전달합니까?
CJBrew

@DenisP 버전을 개선하고 간단한 테스트를 실행했습니다.
Felix Keil

상황에 따라 IDisposable 추가도 볼 수 있습니다. 또한 이벤트를 두 번 입력하지 않아도되도록 Reflection을 사용하여 이벤트 이름을 전달할 수 있으므로 사용법이 훨씬 간단 해집니다. 그렇지 않으면 패턴이 마음에 듭니다. 감사합니다.
Denis P

4

이상적으로 는 그렇지 않습니다 . 비동기 스레드를 확실히 차단할 수는 있지만 이는 리소스 낭비이며 이상적이지 않습니다.

버튼을 클릭하기를 기다리는 동안 사용자가 점심 식사를하는 표준 예를 고려하십시오.

사용자의 입력을 기다리는 동안 비동기 코드를 중지 한 경우 해당 스레드가 일시 중지 된 동안 리소스를 낭비하는 것입니다.

즉, 비동기 작업에서 버튼이 활성화되어 있고 클릭 대기중인 지점까지 유지해야하는 상태를 설정하는 것이 좋습니다. 이때 GetResults메서드 가 중지됩니다 .

그런 다음, 저장 한 상태에 따라 단추 클릭하면 다른 비동기 작업 을 시작 하여 작업 을 계속합니다.

SynchronizationContext를 호출하는 이벤트 핸들러에서 캡처 되기 때문에 GetResults(컴파일러는 사용중인 await키워드를 사용 하여 결과를 수행하며 , UI 애플리케이션에있는 경우 SynchronizationContext.Current 는 널이 아니어야 한다는 사실 ) 사용할 수 있습니다 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버튼을 눌렀을 때 계속 결과를 얻는 방법입니다. 버튼을 누르지 않으면 이벤트 핸들러가 아무 작업도 수행하지 않습니다.


어떤 비동기 스레드? 원래 질문과 답변 모두에서 UI 스레드에서 실행 되지 않는 코드는 없습니다 .
svick

@svick 사실이 아닙니다. GetResults를 반환합니다 Task. await단순히 "작업을 실행하고 작업이 완료되면이 후 코드를 계속하십시오"라고 말합니다. 동기화 컨텍스트가있는 경우 호출은에서 캡처 될 때 UI 스레드로 다시 마샬링됩니다 await. await이다 하지 동일 Task.Wait()하지 조금도.
casperOne

에 대해 아무 말도하지 않았습니다 Wait(). 그러나 코드 GetResults()는 UI 스레드에서 실행되며 다른 스레드는 없습니다. 다시 말해, 예, await기본적으로 작업을 수행하지만, 여기서는 해당 작업이 UI 스레드에서도 실행됩니다.
svick

@svick 작업이 UI 스레드에서 실행된다고 가정 할 이유가 없습니다. 왜 그런 가정을합니까? 그건 가능 하지만 가능성. 그리고 호출은 기술적으로 하나씩 두 개의 별도 UI 호출 await이며 이후에는 코드 await가 차단됩니다. 나머지 코드는 계속해서 마샬링되고을 통해 예약됩니다 SynchronizationContext.
casperOne

1
: 더보고 싶은 사람은 여기를 참조 chat.stackoverflow.com/rooms/17937 - @svick와 나는 기본적으로 서로를 오해하지만, 똑같은 말을했다.
casperOne

3

Stephen 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

반응성 확장 (Rx.Net)

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

var res = await eventObservable.FirstAsync();

Nuget Package System으로 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 감사합니다! 대리인을 반환하지 않기 때문에 수정했습니다. 소스는 Blazor의 대안 인 Lara Web Engine의 일부로 제공 됩니다 .
cat_in_hat 20
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.