"Як заблокувати потік коду, поки подія не буде запущена?"
Ваш підхід невірний. Події, що керуються подіями, не означають блокування та очікування події. Ви ніколи не чекаєте, принаймні, завжди намагаєтесь цього уникнути. Очікування - це витрачання ресурсів, блокування потоків і, можливо, введення ризику виникнення тупикового або зомбі потоку (якщо сигнал звільнення ніколи не підвищується).
Повинно бути зрозуміло, що блокування потоку для очікування події є антидіаграмою, оскільки суперечить ідеї події.
Зазвичай у вас є два (сучасні) варіанти: реалізувати асинхронний API або API, керований подіями. Оскільки ви не хочете реалізовувати свій API асинхронним, вам залишається API, керований подіями.
Ключ API, керованого подіями, полягає в тому, що замість того, щоб змусити абонента синхронно чекати результату або опитувати результат, ви дозволяєте абоненту продовжуватись та надсилати йому повідомлення, як тільки результат буде готовий або операція завершена. Тим часом абонент може продовжувати виконувати інші операції.
Якщо дивитися на проблему з точки зору різьблення, тоді API, керований подією, дозволяє виклику потоку, наприклад, потоку інтерфейсу, який виконує обробник подій кнопки, бути вільним продовжувати обробляти, наприклад, інші операції, пов’язані з інтерфейсом, наприклад рендеринг елементів інтерфейсу користувача або обробляти введення користувача, як-от переміщення миші та натискання клавіш. API, керований подіями, має такий же ефект або ціль, як і асинхронний API, хоча це і є набагато менш зручним.
Оскільки ви не надали достатньо детальних відомостей про те, що ви насправді намагаєтеся робити, що Utility.PickPoint()
насправді робить і який результат завдання, або чому користувач повинен натиснути на "Сітка", я не можу запропонувати вам кращого рішення . Я просто можу запропонувати загальну схему, як реалізувати вашу вимогу.
Ваш потік або мета, очевидно, розділені принаймні на два кроки, щоб зробити послідовність операцій:
- Виконайте операцію 1, коли користувач натисне кнопку
- Виконайте операцію 2 (продовжте / завершіть операцію 1), коли користувач натисне на
Grid
щонайменше з двома обмеженнями:
- Необов’язково: послідовність повинна бути завершена до того, як клієнту API буде дозволено її повторити. Послідовність завершується після запуску операції 2 до завершення.
- Операція 1 завжди виконується перед операцією 2. Операція 1 починає послідовність.
- Операція 1 повинна завершитися, перш ніж клієнт API дозволить виконати операцію 2
Для цього потрібно, щоб клієнт API за допомогою двох повідомлень (подій) дозволяв не блокувати взаємодію:
- Операція 1 завершена (або потрібна взаємодія)
- Операція 2 (або мета) завершена
Ви повинні дозволити своєму API реалізувати цю поведінку та обмеження, викривши два публічні методи та дві публічні події.
Оскільки ця реалізація дозволяє лише один (без одночасного) виклику в API, рекомендується також розкрити IsBusy
властивість для вказівки запущеної послідовності. Це дозволяє опитувати поточний стан перед початком нової послідовності, хоча рекомендується дочекатися завершення події для виконання наступних викликів.
API впровадження / рефактор Utility
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;
}
}
Використовуйте 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
уникати винятків перехресних потоків.
Прочитайте зауваження, DispatcherObject
щоб дізнатися більше про це явище, яке називається спорідненістю диспетчера або спорідненістю потоку.
Для зручного використання API я пропоную перенести всі події в початковий контекст абонента або захоплюючи, і використовуючи абонент, SynchronizationContext
або використовуючи AsyncOperation
(або AsyncOperationManager
).
Наведений вище приклад можна легко покращити, надавши скасування (рекомендується), наприклад, шляхом викриття Cancel()
методу, наприклад, PickPointCancel()
та звітування про хід (бажано, використовуючи Progress<T>
).
Деякі думки - відповідь на ваші коментарі
Оскільки ви наближалися до мене, щоб знайти «краще» рішення щодо блокування, давши мені приклад консольних програм, я переконав вас, що ваше сприйняття чи точка зору абсолютно неправильно.
"Розгляньте додаток Console з цими двома рядками коду в ньому.
var str = Console.ReadLine();
Console.WriteLine(str);
Що відбувається при виконанні програми в режимі налагодження. Він зупиниться на першому рядку коду і змусить вас ввести значення в інтерфейсі консолі, а потім після того, як ви щось введете і натисніть Enter, він виконає наступний рядок і фактично надрукує те, що ви ввели. Я думав про таку саму поведінку, але у застосуванні WPF ".
Консольний додаток - це щось зовсім інше. Концепція різьблення дещо інша. У консольних програмах немає графічного інтерфейсу. Просто потоки введення / виводу / помилок. Ви не можете порівняти архітектуру консольного додатка з багатою програмою GUI. Це не спрацює. Ви дійсно повинні це зрозуміти і прийняти.
Також не обманюйте погляд . Чи знаєте ви, що відбувається всередині Console.ReadLine
? Як це реалізується ? Блокує вона основну нитку і паралельно вона зчитує вхід? Або це просто опитування?
Ось оригінальна реалізація 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;
}
Як бачите, це проста синхронна операція. Це опитування для введення користувачем у "нескінченному" циклі. Жодного магічного блоку і продовжуйте.
WPF побудований навколо потоку візуалізації та потоку інтерфейсу користувача. Ці потоки постійно крутяться для того, щоб спілкуватися з ОС, як обробляти введення користувачів - підтримуючи програму адаптивною . Ви ніколи не хочете призупинити / заблокувати цей потік, оскільки це не дозволить рамці виконувати основні фонові роботи, як-от реагування на події миші - ви не хочете, щоб миша застигала:
очікування = блокування потоку = невідповідальність = поганий UX = роздратовані користувачі / клієнти = проблеми в офісі.
Іноді потік додатків вимагає дочекатися введення або рутини, щоб завершити. Але ми не хочемо блокувати основну нитку.
Ось чому люди винайшли складні моделі асинхронного програмування, щоб дозволити очікувати, не блокуючи основний потік і не змушуючи розробника писати складний і помилковий багатопотоковий код.
Кожна сучасна прикладна програма пропонує асинхронні операції або асинхронну модель програмування, щоб дозволити розробку простого та ефективного коду.
Той факт, що ви намагаєтесь чинити опір асинхронній моделі програмування, свідчить про деяке нерозуміння для мене. Кожен сучасний розробник надає перевагу асинхронному API над синхронним. Жоден серйозний розробник не піклується використати await
ключове слово або оголосити його метод async
. Ніхто. Ви перший, з ким я стикаюсь, хто скаржиться на асинхронні API та хто вважає їх незручними у використанні.
Якби я перевірив вашу основу, яка спрямована на вирішення проблем, пов’язаних із користувальницьким інтерфейсом, або полегшити завдання, пов’язані з інтерфейсом, я б очікував, що це буде асинхронним - до кінця.
API, пов'язаний з інтерфейсом, який не є асинхронним - це відходи, оскільки це ускладнить мій стиль програмування, тому мій код, який, отже, стає більш схильним до помилок і важким у обслуговуванні.
Інша перспектива: коли ви визнаєте, що очікування блокує потік інтерфейсу користувача, створює дуже поганий і небажаний досвід користувача, оскільки користувальницький інтерфейс буде замерзати, поки очікування закінчиться, тепер, коли ви це усвідомлюєте, навіщо пропонувати API чи модуль плагінів, який закликає розробника зробити саме це - реалізувати очікування?
Ви не знаєте, що зробить плагін третьої сторони та скільки часу триватиме рутина, поки вона не завершиться. Це просто поганий дизайн API. Коли ваш API працює на потоці інтерфейсу, то абонент вашого API повинен мати можливість робити неблокуючі дзвінки на нього.
Якщо ви заперечуєте єдине дешеве або витончене рішення, тоді використовуйте підхід, орієнтований на події, як показано в моєму прикладі.
Це робить те, що ви хочете: запустити процедуру - чекати введення користувача - продовжити виконання - досягти мети.
Я дійсно кілька разів намагався пояснити, чому очікування / блокування - це погана конструкція програми. Знову ж таки, ви не можете порівнювати інтерфейс консолі з багатим графічним інтерфейсом, де, наприклад, обробка вводу лише є багато складнішою, ніж просто прослуховування вхідного потоку. Я дійсно не знаю рівня вашого досвіду та з чого ви почали, але ви повинні почати сприймати асинхронну модель програмування. Я не знаю причини, чому ви намагаєтесь цього уникнути. Але це зовсім не мудро.
Сьогодні асинхронні моделі програмування реалізуються скрізь, на кожній платформі, компіляторі, у кожному середовищі, браузері, сервері, настільному ПК, базі даних - скрізь. Модель, керована подіями, дозволяє досягти тієї самої мети, але менш зручна у використанні (передплатити / скасувати підписку на / з подій, читати документи (коли є документи), щоб дізнатися про події), спираючись на фонові нитки. Керовані подіями є старомодними і повинні використовуватися лише тоді, коли асинхронні бібліотеки недоступні або не застосовуються.
Як зауваження: .NET Framwork (.NET Standard) пропонує TaskCompletionSource
(серед інших цілей) запропонувати простий спосіб перетворення існуючого навіть керованого API в асинхронний API.
"Я бачив точну поведінку в Autodesk Revit."
Поведінка (те, що ви переживаєте чи спостерігаєте) сильно відрізняється від того, як реалізується цей досвід. Дві різні речі. Ваш Autodesk дуже ймовірно використовує асинхронні бібліотеки або мовні функції або якийсь інший механізм врізки. І це також пов'язане з контекстом. Коли метод, який вам сподобається, виконується на фоновому потоці, тоді розробник може вирішити заблокувати цей потік. У нього є або дуже вагомий привід для цього, або просто зробили поганий вибір дизайну. Ви зовсім на неправильному шляху;) Блокувати це не добре.
(Чи є вихідний код Autodesk з відкритим кодом? Або як ви знаєте, як він реалізований?)
Я не хочу вас ображати, будь ласка, повірте мені. Але, будь ласка, перегляньте, щоб застосувати ваш API асинхронний. Тільки в голові розробники не люблять використовувати async / очікувати. Ви, очевидно, неправильно склалися. І забудьте про той аргумент програми консолі - це нісенітниця;)
API, пов'язаний з інтерфейсом, ОБОВ'ЯЗКОВО використовувати async / очікувати, коли це можливо. В іншому випадку ви залишаєте всю роботу над написанням незаблокувального коду клієнту свого API. Ви змусите мене обернути кожен виклик вашого API у фонову нитку. Або використовувати менш зручне поводження з подіями. Повірте - кожен розробник швидше прикрашає своїх членів async
, ніж займається подіями. Кожен раз, коли ви використовуєте події, ви можете ризикувати потенційним витоком пам'яті - залежить від деяких обставин, але ризик є реальним і не рідким, коли програмування недбале.
Я дуже сподіваюся, що ви зрозуміли, чому блокування погано. Я дуже сподіваюся, що ви вирішите використовувати async / чекаєте, щоб написати сучасний асинхронний API. Тим не менш, я показав вам дуже поширений спосіб зачекати неблокування, використовуючи події, хоча я закликаю вас використовувати async / wait.
"API дозволить програмісту отримати доступ до інтерфейсу користувача і т.д.
Якщо ви не хочете дозволити плагіну мати прямий доступ до елементів інтерфейсу, вам слід надати інтерфейс для делегування подій або викриття внутрішніх компонентів через абстраговані об'єкти.
API внутрішньо підписується на події інтерфейсу від імені надбудови, а потім делегує подію, виставляючи відповідну подію "обгортки" клієнту API. Ваш API повинен запропонувати кілька гачків, на яких надбудова може підключитися, щоб отримати доступ до конкретних компонентів програми. API плагіна діє як адаптер або фасад, щоб надати зовнішнім користувачам доступ до внутрішніх приміщень.
Щоб дозволити ступінь ізоляції.
Погляньте, як Visual Studio управляє плагінами чи дозволяє нам їх реалізувати. Притворіться, що ви хочете написати плагін для Visual Studio і провести кілька досліджень, як це зробити. Ви зрозумієте, що Visual Studio розкриває внутрішню програму через інтерфейс або API. EG ви можете маніпулювати редактором коду або отримувати інформацію про вміст редактора без реального доступу до нього.
Aync/Await
те, як щодо виконання операції A та збереження цієї операції ДЕРЖАВНО зараз ви хочете, щоб користувач повинен натиснути Grid .. тому якщо користувач натискає Grid, ви перевіряєте стан, якщо це правда, то виконайте свою операцію інше, просто робіть все, що ви хочете ??