Programing

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

lottogame 2020. 6. 17. 20:23
반응형

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


내 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에서 발생하는 작업은 더 복잡하며 실행 내 다른 지점에서 다른 유형의 사용자 입력이 필요할 수 있습니다. 따라서 논리를 여러 방법으로 나누는 것은 쉽지 않습니다.


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;

당신이해야 할 이상한 일이있을 때 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).


내가 사용하는 유틸리티 클래스는 다음과 같습니다.

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;

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

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

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

즉, 비동기 작업에서 버튼이 활성화되어 있고 클릭 대기중인 지점까지 유지해야하는 상태를 설정하는 것이 좋습니다. 이때 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버튼을 눌렀을 때 계속 결과를 얻는 방법입니다. 버튼을 누르지 않으면 이벤트 핸들러가 아무 작업도 수행하지 않습니다.


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

간단한 도우미 클래스 :

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;

반응성 확장 (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");
    }

대기 가능한 이벤트에 대해 자체 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;
    }
}

To declare an event in the class that raises events:

public AsyncEvent MyNormalEvent;
public AsyncEvent<ProgressEventArgs> MyCustomEvent;

To raise the events:

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

To subscribe to the events:

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

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

참고URL : https://stackoverflow.com/questions/12858501/is-it-possible-to-await-an-event-instead-of-another-async-method

반응형