내 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;
답변
간단한 도우미 클래스 :
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;
답변
이상적으로 는 그렇지 않습니다 . 비동기 스레드를 확실히 차단할 수는 있지만 이는 리소스 낭비이며 이상적이지 않습니다.
버튼을 클릭하기를 기다리는 동안 사용자가 점심 식사를하는 표준 예를 고려하십시오.
사용자의 입력을 기다리는 동안 비동기 코드를 중지 한 경우 해당 스레드가 일시 중지 된 동안 리소스를 낭비하는 것입니다.
즉, 비동기 작업에서 버튼이 활성화되어 있고 클릭 대기중인 지점까지 유지해야하는 상태를 설정하는 것이 좋습니다. 이때 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;
}
}
}
답변
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");
}
