„Jak zablokować przepływ kodu do momentu uruchomienia zdarzenia?”
Twoje podejście jest złe. Sterowane zdarzeniem nie oznacza blokowania i czekania na zdarzenie. Nigdy nie czekaj, przynajmniej zawsze starasz się tego uniknąć. Oczekiwanie marnuje zasoby, blokuje wątki i może wprowadzić ryzyko impasu lub wątku zombie (w przypadku, gdy sygnał zwolnienia nigdy nie zostanie podniesiony).
Powinno być jasne, że blokowanie wątku w celu oczekiwania na zdarzenie jest anty-wzorcem, ponieważ jest sprzeczne z ideą zdarzenia.
Zasadniczo masz dwie (nowoczesne) opcje: zaimplementuj asynchroniczny interfejs API lub interfejs API sterowany zdarzeniami. Ponieważ nie chcesz implementować interfejsu API asynchronicznie, pozostaje ci interfejs API sterowany zdarzeniami.
Kluczem API sterowanego zdarzeniami jest to, że zamiast zmuszać dzwoniącego do synchronicznego oczekiwania na wynik lub odpytywania o wynik, pozwalasz dzwoniącemu kontynuować i wysyłać mu powiadomienie, gdy wynik jest gotowy lub operacja jest zakończona. W międzyczasie osoba dzwoniąca może kontynuować wykonywanie innych operacji.
Patrząc na problem z perspektywy wątków, API sterowane zdarzeniami pozwala wątkowi wywołującemu, np. Wątkowi interfejsu użytkownika, który wykonuje procedurę obsługi przycisku, mieć swobodę kontynuowania obsługi np. Innych operacji związanych z interfejsem użytkownika, takich jak renderowanie elementów interfejsu użytkownika lub obsługi danych wejściowych użytkownika, takich jak ruchy myszy i naciśnięcia klawiszy. Interfejs API sterowany zdarzeniami ma taki sam efekt lub cel jak interfejs API asynchroniczny, chociaż jest znacznie mniej wygodny.
Ponieważ nie podałeś wystarczająco dużo szczegółów na temat tego, co naprawdę próbujesz zrobić, co Utility.PickPoint()
faktycznie robi i jaki jest wynik zadania lub dlaczego użytkownik musi kliknąć opcję `Siatka, nie mogę zaoferować lepszego rozwiązania . Mogę tylko przedstawić ogólny wzorzec, w jaki sposób spełnić twoje wymagania.
Twój przepływ lub cel jest oczywiście podzielony na co najmniej dwa kroki, aby uczynić z niego sekwencję operacji:
- Wykonaj operację 1, gdy użytkownik kliknie przycisk
- Wykonaj operację 2 (kontynuuj / zakończ operację 1), gdy użytkownik kliknie na
Grid
z co najmniej dwoma ograniczeniami:
- Opcjonalnie: sekwencja musi zostać zakończona, zanim klient API będzie mógł ją powtórzyć. Sekwencja jest zakończona po zakończeniu operacji 2.
- Operacja 1 jest zawsze wykonywana przed operacją 2. Operacja 1 rozpoczyna sekwencję.
- Operacja 1 musi zostać zakończona, zanim klient API będzie mógł wykonać operację 2
Wymaga to dwóch powiadomień (zdarzeń) dla klienta interfejsu API, aby umożliwić interakcję nieblokującą:
- Operacja 1 zakończona (lub wymagana interakcja)
- Operacja 2 (lub bramka) zakończona
Powinieneś pozwolić swojemu API zaimplementować to zachowanie i ograniczenia, ujawniając dwie metody publiczne i dwa zdarzenia publiczne.
Ponieważ ta implementacja zezwala tylko na pojedyncze (nie współbieżne) wywołanie interfejsu API, zaleca się również ujawnienie IsBusy
właściwości wskazującej działającą sekwencję. Umożliwia to odpytywanie bieżącego stanu przed rozpoczęciem nowej sekwencji, chociaż zaleca się czekanie na zakończenie zdarzenia w celu wykonania kolejnych wywołań.
Implementuj / refaktoryzuj API narzędzia
Utility.cs
class Utility
{
public event EventHandler InitializePickPointCompleted;
public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
public bool IsBusy { get; set; }
private bool IsPickPointInitialized { 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.IsBusy)
{
// 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.IsBusy = 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 (allow next client invocation)
this.IsBusy = 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 : AsyncCompletedEventArgs
{
public Point Result { get; }
public PickPointCompletedEventArgs(Point result)
{
this.Result = result;
}
}
Użyj interfejsu 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>
Uwagi
Zdarzenia wywołane w wątku w tle wykonają swoje procedury obsługi w tym samym wątku. Dostęp do DispatcherObject
podobnego elementu interfejsu użytkownika z modułu obsługi, który jest wykonywany w wątku w tle, wymaga kolejkowania operacji krytycznej przy Dispatcher
użyciu jednego z nich Dispatcher.Invoke
lub Dispatcher.InvokeAsync
uniknięcia wyjątków między wątkami.
Przeczytaj uwagi o tym, DispatcherObject
aby dowiedzieć się więcej o tym zjawisku zwanym powinowactwem dyspozytora lub powinowactwem do wątku.
Dla wygodnego użycia interfejsu API sugeruję, aby wszystkie zdarzenia przenieść do pierwotnego kontekstu dzwoniącego, przechwytując go i używając go SynchronizationContext
lub używając AsyncOperation
(lubAsyncOperationManager
).
Powyższy przykład można łatwo ulepszyć, zapewniając anulowanie (zalecane) np. Przez ujawnienie Cancel()
metody np. PickPointCancel()
I raportowanie postępów (najlepiej przy użyciu Progress<T>
).
Kilka przemyśleń - odpowiedz na swoje komentarze
Ponieważ zbliżałeś się do mnie, by znaleźć „lepsze” rozwiązanie blokujące, biorąc pod uwagę przykład aplikacji konsolowych, przekonałem cię, że twoje postrzeganie lub punkt widzenia są całkowicie błędne.
„Rozważ aplikację konsolową z tymi dwoma wierszami kodu.
var str = Console.ReadLine();
Console.WriteLine(str);
Co się stanie, gdy uruchomisz aplikację w trybie debugowania. Zatrzyma się w pierwszym wierszu kodu i zmusi cię do wprowadzenia wartości w interfejsie użytkownika konsoli, a następnie po wprowadzeniu czegoś i naciśnięciu klawisza Enter wykona następny wiersz i wydrukuje to, co wpisałeś. Myślałem o tym samym zachowaniu, ale w aplikacji WPF ”.
Aplikacja konsolowa to coś zupełnie innego. Koncepcja wątków jest nieco inna. Aplikacje konsolowe nie mają GUI. Tylko strumienie wejściowe / wyjściowe / błędów. Nie można porównać architektury aplikacji konsoli do bogatej aplikacji GUI. To nie zadziała. Naprawdę musisz to zrozumieć i zaakceptować.
Również nie daj się zwieść wyglądowi . Czy wiesz, co się dzieje w środku Console.ReadLine
? Jak to jest realizowane ? Czy blokuje główny wątek i równolegle czyta wejście? A może to tylko ankieta?
Oto oryginalne wdrożenie Console.ReadLine
:
public virtual String ReadLine()
{
StringBuilder sb = new StringBuilder();
while (true)
{
int ch = Read();
if (ch == -1)
break;
if (ch == '\r' || ch == '\n')
{
if (ch == '\r' && Peek() == '\n')
Read();
return sb.ToString();
}
sb.Append((char)ch);
}
if (sb.Length > 0)
return sb.ToString();
return null;
}
Jak widać, jest to prosta synchroniczna operacja. Przeszukuje dane wejściowe użytkownika w „nieskończonej” pętli. Bez magicznego bloku i kontynuuj.
WPF jest zbudowany wokół wątku renderującego i wątku interfejsu użytkownika. Nici te wciąż zawsze przędzenia w celu komunikowania się z systemem operacyjnym jak przenoszenia danych wprowadzanych przez użytkownika - utrzymanie aplikacji reaguje . Nigdy nie chcesz wstrzymywać / blokować tego wątku, ponieważ zatrzyma on szkielet w wykonywaniu niezbędnych prac w tle, takich jak reagowanie na zdarzenia myszy - nie chcesz, aby mysz się zawieszała:
czekanie = blokowanie wątków = brak reakcji = złe UX = zirytowani użytkownicy / klienci = problemy w biurze.
Czasami przepływ aplikacji wymaga oczekiwania na wejście lub procedurę. Ale nie chcemy blokować głównego wątku.
Dlatego ludzie wymyślili złożone modele programowania asynchronicznego, aby umożliwić czekanie bez blokowania głównego wątku i bez zmuszania programisty do pisania skomplikowanego i błędnego wielowątkowego kodu.
Każda nowoczesna struktura aplikacji oferuje operacje asynchroniczne lub asynchroniczny model programowania, aby umożliwić opracowanie prostego i wydajnego kodu.
Fakt, że usiłujesz się oprzeć asynchronicznemu modelowi programowania, pokazuje mi pewien brak zrozumienia. Każdy współczesny programista woli interfejs asynchroniczny niż interfejs synchroniczny. Żaden poważny programista nie chce używać await
słowa kluczowego ani deklarować swojej metody async
. Nikt. Jesteś pierwszym, z którym się spotykam, który skarży się na asynchroniczne interfejsy API i który uważa, że korzystanie z nich jest niewygodne.
Gdybym sprawdził twoje środowisko, które ma na celu rozwiązanie problemów związanych z interfejsem użytkownika lub ułatwiło zadania związane z interfejsem użytkownika, oczekiwałbym, że będzie on asynchroniczny - do końca.
Interfejs API związany z interfejsem użytkownika, który nie jest asynchroniczny, jest marnotrawstwem, ponieważ komplikuje mój styl programowania, dlatego mój kod staje się bardziej podatny na błędy i trudny do utrzymania.
Z innej perspektywy: kiedy przyznajesz, że czekanie blokuje wątek interfejsu użytkownika, powoduje bardzo złe i niepożądane wrażenia użytkownika, ponieważ interfejs użytkownika zawiesza się, aż do zakończenia oczekiwania, teraz, gdy zdajesz sobie z tego sprawę, dlaczego oferujesz interfejs API lub model wtyczki, który zachęca programistę do zrobienia dokładnie tego - wdrożyć czekanie?
Nie wiesz, co zrobi wtyczka innej firmy i ile czasu zajmie procedura do jej zakończenia. To po prostu zły projekt API. Gdy interfejs API działa w wątku interfejsu użytkownika, osoba wywołująca interfejs API musi być w stanie wykonywać nieblokujące wywołania do niego.
Jeśli odrzucasz jedyne tanie lub pełne wdzięku rozwiązanie, zastosuj podejście oparte na zdarzeniach, jak pokazano w moim przykładzie.
Robi to, co chcesz: uruchomić procedurę - czekać na dane wejściowe użytkownika - kontynuować wykonywanie - osiągnąć cel.
Naprawdę kilkakrotnie próbowałem wyjaśnić, dlaczego oczekiwanie / blokowanie jest złym projektem aplikacji. Ponownie nie można porównać interfejsu konsoli do bogatego interfejsu graficznego, w którym np. Sama obsługa danych wejściowych jest o wiele bardziej złożona niż tylko słuchanie strumienia wejściowego. Naprawdę nie znam twojego poziomu doświadczenia i miejsca, w którym zacząłeś, ale powinieneś zacząć stosować model programowania asynchronicznego. Nie znam powodu, dla którego próbujesz tego uniknąć. Ale to wcale nie jest mądre.
Obecnie modele programowania asynchronicznego są wdrażane wszędzie, na każdej platformie, kompilatorze, każdym środowisku, przeglądarce, serwerze, komputerze stacjonarnym, bazie danych - wszędzie. Model sterowany zdarzeniami pozwala osiągnąć ten sam cel, ale jest mniej wygodny w użyciu (subskrybuj / wypisz się do / ze zdarzeń, czytaj dokumenty (gdy są dokumenty), aby dowiedzieć się o zdarzeniach), opierając się na wątkach w tle. Sterowane zdarzeniami są przestarzałe i powinny być używane tylko wtedy, gdy biblioteki asynchroniczne są niedostępne lub nie mają zastosowania.
Na marginesie: Framwork .NET (.NET Standard) oferuje TaskCompletionSource
(między innymi) prosty sposób na konwersję istniejącego API sterowanego parzystym na asynchroniczny API.
„Widziałem dokładne zachowanie w programie Autodesk Revit”.
Zachowanie (to, czego doświadczasz lub obserwujesz) znacznie różni się od sposobu implementacji tego doświadczenia. Dwie różne rzeczy. Twój Autodesk najprawdopodobniej używa asynchronicznych bibliotek lub funkcji językowych lub innego mechanizmu wątków. I jest również związany z kontekstem. Gdy metoda, o której myślisz, jest wykonywana w wątku w tle, programista może zablokować ten wątek. Ma albo bardzo dobry powód, aby to zrobić, albo po prostu dokonał złego wyboru projektu. Jesteś całkowicie na złym torze;) Blokowanie nie jest dobre.
(Czy kod źródłowy Autodesk jest oprogramowaniem typu open source? Lub skąd wiesz, jak jest on wdrażany?)
Nie chcę cię obrażać, proszę uwierz mi. Zastanów się jednak, czy zaimplementować interfejs API asynchronicznie. Tylko w twojej głowie programiści nie lubią używać asynchronizacji / czekania. Oczywiście masz niewłaściwy sposób myślenia. I zapomnij o argumencie aplikacji konsolowej - to nonsens;)
Interfejs API związany z interfejsem użytkownika MUSI używać asynchronicznie / oczekuj, gdy tylko jest to możliwe. W przeciwnym razie pozostawiasz całą pracę, aby napisać nieblokujący kod do klienta interfejsu API. Zmusiłbyś mnie do zawinięcia każdego wywołania twojego API w wątek w tle. Lub użyć mniej wygodnej obsługi zdarzeń. Uwierz mi - każdy programista raczej ozdabia swoich członków async
, niż zajmuje się obsługą zdarzeń. Za każdym razem, gdy korzystasz ze zdarzeń, możesz ryzykować wyciek pamięci - zależy od pewnych okoliczności, ale ryzyko jest realne i nierzadkie przy programowaniu beztroskim.
Naprawdę mam nadzieję, że rozumiesz, dlaczego blokowanie jest złe. Naprawdę mam nadzieję, że zdecydujesz się użyć asynchronicznego / oczekuj na napisanie nowoczesnego asynchronicznego API. Niemniej jednak pokazałem ci bardzo częsty sposób oczekiwania na nieblokowanie przy użyciu zdarzeń, chociaż zachęcam do użycia asynchronizacji / oczekiwania.
„Interfejs API pozwoli programiście na dostęp do interfejsu użytkownika itp. Załóżmy teraz, że programista chce opracować dodatek, który po kliknięciu przycisku użytkownik końcowy zostanie poproszony o wybranie punktu w interfejsie użytkownika”
Jeśli nie chcesz zezwalać wtyczce na bezpośredni dostęp do elementów interfejsu użytkownika, powinieneś udostępnić interfejs do delegowania zdarzeń lub ujawnienia komponentów wewnętrznych poprzez abstrakcyjne obiekty.
Interfejs API wewnętrznie subskrybuje zdarzenia interfejsu użytkownika w imieniu dodatku, a następnie deleguje je, udostępniając odpowiednie zdarzenie „otoki” klientowi interfejsu API. Twój interfejs API musi oferować pewne zaczepy, w których dodatek może się łączyć, aby uzyskać dostęp do określonych składników aplikacji. Wtyczka API działa jak adapter lub fasada, zapewniając zewnętrznym dostęp do wewnętrznych.
Aby umożliwić pewien stopień izolacji.
Zobacz, jak Visual Studio zarządza wtyczkami lub pozwala nam je implementować. Udawaj, że chcesz napisać wtyczkę do programu Visual Studio i poszukaj informacji, jak to zrobić. Uświadomisz sobie, że Visual Studio udostępnia swoje elementy wewnętrzne za pośrednictwem interfejsu lub interfejsu API. Np. Możesz manipulować edytorem kodu lub uzyskiwać informacje o treści edytora bez rzeczywistego dostępu do niego.
Aync/Await
wykonanie operacji A i zapisanie tej operacji STAN, teraz chcesz, aby użytkownik powinien kliknąć opcję Siatka. Więc jeśli użytkownik kliknie opcję Siatka, sprawdź stan, jeśli jest prawdziwy, a następnie wykonaj swoją operację, po prostu rób co chcesz?