다른 비동기 메서드 대신 이벤트를 기다리는 것이 가능합니까?
내 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;
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;
}
'Programing' 카테고리의 다른 글
전체 파일을 문자열 변수로 읽는 방법 (0) | 2020.06.17 |
---|---|
방금 발생한 MySQL 경고를 어떻게 표시합니까? (0) | 2020.06.17 |
어떤 경우에 HTTP_REFERER가 비어 있습니까? (0) | 2020.06.17 |
postgresql-테이블 세트 기본값에 부울 열 추가 (0) | 2020.06.17 |
Visual Studio 디버거에서 동적으로 할당 된 배열을 표시하는 방법은 무엇입니까? (0) | 2020.06.16 |