"이벤트가 발생할 때까지 코드 흐름을 차단하는 방법은 무엇입니까?"
당신은 접근이 잘못되었습니다. 이벤트 중심은 이벤트를 차단하고 기다리는 것을 의미하지 않습니다. 당신은 기다릴 필요가 없습니다, 적어도 당신은 항상 그것을 피하려고 열심히 노력합니다. 대기 중은 리소스 낭비, 스레드 차단 및 교착 상태 또는 좀비 스레드 위험이 발생할 수 있습니다 (신호가 발생하지 않는 경우).
이벤트를 기다리기 위해 스레드를 차단하는 것은 이벤트 아이디어와 모순되므로 안티 패턴입니다.
일반적으로 비동기 API 또는 이벤트 중심 API를 구현하는 두 가지 (현대) 옵션이 있습니다. API 비동기를 구현하고 싶지 않기 때문에 이벤트 중심 API가 남아 있습니다.
이벤트 중심 API의 핵심은 호출자가 결과를 동 기적으로 기다리거나 결과를 폴링하는 대신 결과가 준비되거나 작업이 완료되면 호출자가 계속해서 알림을 보내도록하는 것입니다. 한편, 발신자는 다른 작업을 계속 실행할 수 있습니다.
스레딩 관점에서 문제를 볼 때 이벤트 중심 API를 사용하면 버튼의 이벤트 핸들러를 실행하는 UI 스레드와 같은 호출 스레드가 UI 요소 렌더링 또는 처리와 같은 UI 관련 작업을 계속 처리 할 수 있습니다. 마우스 이동 및 키 누름과 같은 사용자 입력. 비록 덜 편리하지만 비동기 API와 같은 효과.
Utility.PickPoint()
실제로 수행 하려는 작업, 실제로 수행중인 작업 및 작업 결과 및 사용자가 그리드를 클릭해야하는 이유에 대한 자세한 정보를 제공하지 않았으므로 더 나은 솔루션을 제공 할 수 없습니다 . 요구 사항을 구현하는 방법에 대한 일반적인 패턴을 제공 할 수 있습니다.
당신의 흐름이나 목표는 분명히 일련의 작업으로 만들기 위해 최소한 두 단계로 나뉩니다.
- 사용자가 버튼을 클릭하면 조작 1을 실행하십시오.
- 사용자가 컴퓨터를 클릭하면 작업 2 (계속 / 완료 작업 1)를 실행합니다.
Grid
최소한 두 가지 제약 조건이 있습니다.
- 선택 사항 : API 클라이언트가 반복하기 전에 시퀀스를 완료해야합니다. 작업 2가 완료되면 시퀀스가 완료됩니다.
- 작업 1은 항상 작업 2 전에 실행됩니다. 작업 1은 시퀀스를 시작합니다.
- API 클라이언트가 조작 2를 실행하기 전에 조작 1을 완료해야합니다.
비 차단 상호 작용을 허용하려면 API 클라이언트에게 두 가지 알림이 필요합니다.
- 작업 1 완료 (또는 상호 작용 필요)
- 작업 2 (또는 목표) 완료
두 개의 공개 메소드와 두 개의 공개 이벤트를 노출하여 API가이 동작 및 제한 조건을 구현하도록해야합니다.
유틸리티 API 구현 / 리 팩터
Utility.cs
class Utility
{
public event EventHandler InitializePickPointCompleted;
public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
private bool IsPickPointInitialized { get; set; }
private bool IsExecutingSequence { get; set; }
// The prefix 'Begin' signals the caller or client of the API,
// that he also has to end the sequence explicitly
public void BeginPickPoint(param)
{
// Implement constraint 1
if (this.IsExecutingSequence)
{
// Alternatively just return or use Try-do pattern
throw new InvalidOperationException("BeginPickPoint is already executing. Call EndPickPoint before starting another sequence.");
}
// Set the flag that a current sequence is in progress
this.IsExecutingSequence = true;
// Execute operation until caller interaction is required.
// Execute in background thread to allow API caller to proceed with execution.
Task.Run(() => StartOperationNonBlocking(param));
}
public void EndPickPoint(param)
{
// Implement constraint 2 and 3
if (!this.IsPickPointInitialized)
{
// Alternatively just return or use Try-do pattern
throw new InvalidOperationException("BeginPickPoint must have completed execution before calling EndPickPoint.");
}
// Execute operation until caller interaction is required.
// Execute in background thread to allow API caller to proceed with execution.
Task.Run(() => CompleteOperationNonBlocking(param));
}
private void StartOperationNonBlocking(param)
{
... // Do something
// Flag the completion of the first step of the sequence (to guarantee constraint 2)
this.IsPickPointInitialized = true;
// Request caller interaction to kick off EndPickPoint() execution
OnInitializePickPointCompleted();
}
private void CompleteOperationNonBlocking(param)
{
// Execute goal and get the result of the completed task
Point result = ExecuteGoal();
// Reset API sequence
this.IsExecutingSequence = false;
this.IsPickPointInitialized = false;
// Notify caller that execution has completed and the result is available
OnPickPointCompleted(result);
}
private void OnInitializePickPointCompleted()
{
// Set the result of the task
this.InitializePickPointCompleted?.Invoke(this, EventArgs.Empty);
}
private void OnPickPointCompleted(Point result)
{
// Set the result of the task
this.PickPointCompleted?.Invoke(this, new PickPointCompletedEventArgs(result));
}
}
PickPointCompletedEventArgs.cs
class PickPointCompletedEventArgs : EventArgs
{
public Point Result { get; }
public PickPointCompletedEventArgs(Point result)
{
this.Result = result;
}
}
API 사용
MainWindow.xaml.cs
partial class MainWindow : Window
{
private Utility Api { get; set; }
public MainWindow()
{
InitializeComponent();
this.Api = new Utility();
}
private void StartPickPoint_OnButtonClick(object sender, RoutedEventArgs e)
{
this.Api.InitializePickPointCompleted += RequestUserInput_OnInitializePickPointCompleted;
// Invoke API and continue to do something until the first step has completed.
// This is possible because the API will execute the operation on a background thread.
this.Api.BeginPickPoint();
}
private void RequestUserInput_OnInitializePickPointCompleted(object sender, EventArgs e)
{
// Cleanup
this.Api.InitializePickPointCompleted -= RequestUserInput_OnInitializePickPointCompleted;
// Communicate to the UI user that you are waiting for him to click on the screen
// e.g. by showing a Popup, dimming the screen or showing a dialog.
// Once the input is received the input event handler will invoke the API to complete the goal
MessageBox.Show("Please click the screen");
}
private void FinishPickPoint_OnGridMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
this.Api.PickPointCompleted += ShowPoint_OnPickPointCompleted;
// Invoke API to complete the goal
// and continue to do something until the last step has completed
this.Api.EndPickPoint();
}
private void ShowPoint_OnPickPointCompleted(object sender, PickPointCompletedEventArgs e)
{
// Cleanup
this.Api.PickPointCompleted -= ShowPoint_OnPickPointCompleted;
// Get the result from the PickPointCompletedEventArgs instance
Point point = e.Result;
// Handle the result
MessageBox.Show(point.ToString());
}
}
MainWindow.xaml
<Window>
<Grid MouseLeftButtonUp="FinishPickPoint_OnGridMouseLeftButtonUp">
<Button Click="StartPickPoint_OnButtonClick" />
</Grid>
</Window>
비고
백그라운드 스레드에서 발생한 이벤트는 동일한 스레드에서 처리기를 실행합니다. DispatcherObject
백그라운드 스레드에서 실행되는 핸들러에서 유사한 UI 요소에 액세스 하려면 Dispatcher
사용 중 Dispatcher.Invoke
또는 Dispatcher.InvokeAsync
교차 스레드 예외를 피하기 위해 중요한 조작을 큐에 넣어야합니다 .
몇 가지 생각-귀하의 의견에 답변
콘솔 응용 프로그램의 예를 들어서 "더 나은"차단 솔루션을 찾기 위해 나에게 다가 가고 있었기 때문에 나는 당신의 인식이나 관점이 완전히 잘못되었다고 확신했습니다.
"이 두 줄의 코드로 콘솔 응용 프로그램을 고려하십시오.
var str = Console.ReadLine();
Console.WriteLine(str);
디버그 모드에서 응용 프로그램을 실행하면 어떻게됩니까? 첫 번째 코드 줄에서 멈추고 콘솔 UI에 값을 입력 한 다음 무언가를 입력하고 Enter 키를 누르면 다음 줄을 실행하고 실제로 입력 한 내용을 인쇄합니다. WPF 응용 프로그램에서 정확히 같은 동작을 생각하고있었습니다. "
콘솔 응용 프로그램은 완전히 다릅니다. 스레딩 개념이 다릅니다. 콘솔 응용 프로그램에는 GUI가 없습니다. 입력 / 출력 / 오류 스트림 만 있습니다. 콘솔 응용 프로그램의 아키텍처를 풍부한 GUI 응용 프로그램과 비교할 수 없습니다. 작동하지 않습니다. 당신은 정말로 이것을 이해하고 받아 들여야합니다.
WPF는 렌더링 스레드와 UI 스레드를 중심으로 빌드됩니다. 이러한 스레드는 사용자 입력 처리와 같이 OS와 통신하기 위해 항상 회전하여 응용 프로그램의 응답 성을 유지합니다 . 이 스레드는 프레임 워크가 필수적인 백그라운드 작업 (마우스 이벤트에 응답하는 것과 같은 것)을 멈추게하므로 일시 정지 / 차단하고 싶지 않습니다.
대기 = 스레드 차단 = 응답하지 않음 = 불량 UX = 성가신 사용자 / 고객 = 사무실 문제.
때로는 응용 프로그램 흐름에서 입력 또는 루틴이 완료되기를 기다려야합니다. 그러나 우리는 메인 스레드를 차단하고 싶지 않습니다.
그렇기 때문에 사람들은 복잡한 비동기 프로그래밍 모델을 개발하여 메인 스레드를 차단하지 않고 개발자가 복잡하고 잘못된 멀티 스레딩 코드를 작성하지 않고도 기다릴 수 있습니다.
모든 최신 응용 프로그램 프레임 워크는 간단하고 효율적인 코드를 개발할 수 있도록 비동기 작업 또는 비동기 프로그래밍 모델을 제공합니다.
비동기 프로그래밍 모델에 저항하기 위해 노력하고 있다는 사실은 저에게 이해력이 부족하다는 것을 보여줍니다. 모든 현대 개발자는 동기식 API보다 비동기식 API를 선호합니다. 심각한 개발자는 await
키워드 를 사용 하거나 자신의 방법을 선언 하지 않습니다 async
. 아무도 비동기 API에 대해 불평하고 사용하기 불편한 사람이 처음으로 만났습니다.
UI 관련 문제를 해결하거나 UI 관련 작업을보다 쉽게 수행 할 수있는 프레임 워크를 확인하려면 비동기식으로 기대할 수 있습니다.
비동기 적이 지 않은 UI 관련 API는 프로그래밍 스타일을 복잡하게하므로 오류가 발생하기 쉽고 코드를 유지하기가 어렵 기 때문에 낭비입니다.
다른 관점 : 대기가 UI 스레드를 차단한다는 사실을 인정하면 대기가 끝날 때까지 UI가 정지되므로 매우 나쁘고 바람직하지 않은 사용자 경험이 생길 수 있습니다. 이제 깨달았습니다. 개발자가 정확히 이것을 수행-대기 구현?
타사 플러그인의 기능과 루틴이 완료 될 때까지 걸리는 시간을 모릅니다. 이것은 단순히 나쁜 API 디자인입니다. API가 UI 스레드에서 작동하는 경우 API 호출자는 비 블로킹 호출을 수행 할 수 있어야합니다.
내 예에서 볼 수 있듯이 이벤트 중심 접근 방식을 사용하는 것보다 저렴하거나 우아한 솔루션을 거부하는 경우.
원하는 작업을 수행합니다. 루틴 시작-사용자 입력 대기-실행 계속-목표 달성.
대기 / 차단이 왜 나쁜 응용 프로그램 디자인인지 설명하기 위해 여러 번 시도했습니다. 다시, 콘솔 UI를 리치 그래픽 UI와 비교할 수 없습니다. 예를 들어 입력 처리만으로 입력 스트림을 듣는 것보다 훨씬 더 복잡합니다. 나는 당신의 경험 수준과 시작 위치를 모르지만 비동기 프로그래밍 모델을 수용해야합니다. 나는 당신이 그것을 피하려고하는 이유를 모른다. 그러나 전혀 현명하지 않습니다.
오늘날 비동기 프로그래밍 모델은 모든 플랫폼, 컴파일러, 모든 환경, 브라우저, 서버, 데스크톱, 데이터베이스 등 모든 곳에서 구현됩니다. 이벤트 중심 모델은 동일한 목표를 달성 할 수 있지만 백그라운드 스레드에 의존하여 사용하기 (이벤트 구독 / 구독 취소)가 덜 편리합니다. 이벤트 중심은 구식이며 비동기 라이브러리를 사용할 수 없거나 적용 할 수없는 경우에만 사용해야합니다.
"Autodesk Revit에서 정확한 동작을 보았습니다."
행동 (당신이 경험하거나 관찰하는 것)은이 경험이 어떻게 구현되는지와는 많이 다릅니다. 다른 두 가지. Autodesk는 비동기 라이브러리 또는 언어 기능 또는 다른 스레딩 메커니즘을 사용하고있을 가능성이 큽니다. 또한 상황과 관련이 있습니다. 마음에 드는 메소드가 백그라운드 스레드에서 실행될 때 개발자는이 스레드를 차단하도록 선택할 수 있습니다. 그는 이것을해야 할 아주 좋은 이유가 있거나 디자인을 잘못 선택했습니다. 당신은 완전히 잘못된 길을 가고 있습니다;) 차단은 좋지 않습니다.
(Autodesk 소스 코드는 오픈 소스입니까, 어떻게 구현되는지 어떻게 알 수 있습니까?)
난 당신을 화나게하고 싶지 않아요, 믿어주세요. 그러나 API 비동기를 구현하도록 다시 고려하십시오. 개발자가 비동기 / 대기를 사용하지 않는 것은 당신의 머리에만 있습니다. 당신은 분명히 잘못된 사고 방식을 가지고 있습니다. 콘솔 응용 프로그램 인수는 잊어 버려요.
UI 관련 API는 가능할 때마다 반드시 async / await를 사용해야합니다. 그렇지 않으면 API의 클라이언트에 비 차단 코드를 작성하기위한 모든 작업을 남겨 둡니다. API에 대한 모든 호출을 백그라운드 스레드로 랩핑해야합니다. 또는 덜 편안한 이벤트 처리를 사용하십시오. 저를 믿으십시오-모든 개발자 async
는 이벤트 처리보다 멤버를 꾸미십시오 . 이벤트를 사용할 때마다 잠재적 인 메모리 누수 위험이 있습니다 (일부 상황에 따라 다름). 그러나 부주의하게 프로그래밍 할 때 위험은 거의 없으며 드물지 않습니다.
차단이 왜 나쁜지 이해하기를 바랍니다. 현대 비동기 API를 작성하기 위해 async / await를 사용하기로 결정했으면합니다. 그럼에도 불구하고 이벤트를 사용하여 비 블로킹을 기다리는 가장 일반적인 방법을 보여 주었지만 async / await를 사용하는 것이 좋습니다.
"API를 사용하면 프로그래머가 UI 등에 액세스 할 수 있습니다. 이제 프로그래머가 단추를 클릭 할 때 최종 사용자에게 UI에서 포인트를 선택하도록 추가 기능을 개발하려고합니다."
플러그인이 UI 요소에 직접 액세스하지 못하게하려면 추상 객체를 통해 이벤트를 위임하거나 내부 구성 요소를 노출하는 인터페이스를 제공해야합니다.
API는 내부적으로 애드 인 대신 UI 이벤트를 구독 한 다음 해당 "래퍼"이벤트를 API 클라이언트에 노출시켜 이벤트를 위임합니다. API는 특정 애플리케이션 구성 요소에 액세스하기 위해 추가 기능을 연결할 수있는 후크를 제공해야합니다. 플러그인 API는 외부에서 내부에 액세스 할 수 있도록 어댑터 또는 파사드처럼 작동합니다.
어느 정도의 격리를 허용합니다.
Visual Studio에서 플러그인을 관리하거나 구현할 수있는 방법을 살펴보십시오. Visual Studio 용 플러그인을 작성하고이 작업을 수행하는 방법에 대한 조사를 수행하려고합니다. Visual Studio는 인터페이스 또는 API를 통해 내부를 노출한다는 것을 알게 될 것입니다. EG 당신은 코드 에디터를 조작하거나 에디터의 실제 접근 없이 에디터의 컨텐츠에 대한 정보를 얻을 수 있습니다.
Aync/Await
작업 A를 수행하고 해당 작업을 저장하는 방법입니다. 이제 상태를 사용자가 그리드를 클릭해야합니다.