"Как заблокировать поток кода, пока не сработает событие?"
Ваш подход неверен. Управляемый событиями не означает блокирование и ожидание события. Вы никогда не ждете, по крайней мере, вы всегда стараетесь избежать этого. Ожидание - это бесполезная трата ресурсов, блокировка потоков и, возможно, введение риска тупика или потока зомби (в случае, если сигнал освобождения никогда не поднимается).
Должно быть ясно, что блокировка потока для ожидания события является анти-паттерном, поскольку противоречит идее события.
Обычно у вас есть два (современных) варианта: реализовать асинхронный 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 реализовать это поведение и ограничения, предоставив два открытых метода и два открытых события.
Реализация / рефакторинг Utility 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
как элемент пользовательского интерфейса от обработчика, который выполняется в фоновом потоке, требуется критическая операция должна быть поставлена в очередь к Dispatcher
используя либо Dispatcher.Invoke
или , Dispatcher.InvokeAsync
чтобы избежать исключений кросса-нити.
Прочитайте замечания о том, DispatcherObject
чтобы узнать больше об этом явлении, называемом сходством с диспетчером или сродством потока
Некоторые мысли - ответьте на ваши комментарии
Поскольку вы подходили ко мне, чтобы найти «лучшее» решение для блокировки, на примере консольных приложений, я убедил вас, что ваше восприятие или точка зрения совершенно неверны.
«Рассмотрим консольное приложение с этими двумя строками кода.
var str = Console.ReadLine();
Console.WriteLine(str);
Что происходит, когда вы запускаете приложение в режиме отладки. Он остановится на первой строке кода и заставит вас ввести значение в пользовательском интерфейсе консоли, а затем, после того как вы введете что-то и нажмете Enter, выполнит следующую строку и фактически напечатает то, что вы ввели. Я думал о том же поведении, но в приложении WPF. "
Консольное приложение - это нечто совершенно другое. Концепция потоков немного отличается. Консольные приложения не имеют графического интерфейса. Просто вход / выход / потоки ошибок. Вы не можете сравнить архитектуру консольного приложения с многофункциональным графическим приложением. Это не сработает. Вы действительно должны понять и принять это.
Также не обманывайтесь внешностью . Вы знаете, что происходит внутри 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 / await. Вы явно ошиблись. И забыть об этом аргументе консольного приложения - это нонсенс;)
API, связанный с пользовательским интерфейсом, ДОЛЖЕН использовать async / await всякий раз, когда это возможно. В противном случае вы оставляете всю работу по написанию неблокирующего кода клиенту вашего API. Вы бы заставили меня обернуть каждый вызов вашего API в фоновый поток. Или использовать менее удобную обработку событий. Поверьте мне - каждый разработчик скорее украшает своих участников async
, чем занимается обработкой событий. Каждый раз, когда вы используете события, вы можете рисковать потенциальной утечкой памяти - это зависит от некоторых обстоятельств, но риск реален и не редок при небрежном программировании.
Я действительно надеюсь, что вы понимаете, почему блокировка это плохо. Я действительно надеюсь, что вы решите использовать async / await для написания современного асинхронного API. Тем не менее, я показал вам очень распространенный способ ожидания неблокирования, используя события, хотя я призываю вас использовать async / await.
«API позволит программисту иметь доступ к пользовательскому интерфейсу и т. Д. Теперь предположим, что программист хочет разработать надстройку, чтобы при нажатии кнопки конечному пользователю предлагалось выбрать точку в пользовательском интерфейсе».
Если вы не хотите, чтобы плагин имел прямой доступ к элементам пользовательского интерфейса, вы должны предоставить интерфейс для делегирования событий или предоставления внутренних компонентов через абстрагированные объекты.
Интерфейс API будет подписываться на события пользовательского интерфейса от имени надстройки, а затем делегирует это событие, предоставляя соответствующее событие «оболочки» клиенту API. Ваш API должен предлагать несколько ловушек, с помощью которых надстройка может подключаться для доступа к определенным компонентам приложения. API плагинов действует как адаптер или фасад, предоставляя внешним доступам к внутренним объектам.
Чтобы позволить степень изоляции.
Посмотрите, как Visual Studio управляет плагинами или позволяет нам их реализовывать. Представьте, что вы хотите написать плагин для Visual Studio, и поинтересуйтесь, как это сделать. Вы поймете, что Visual Studio предоставляет свои внутренние возможности через интерфейс или API. Например, вы можете манипулировать редактором кода или получать информацию о содержимом редактора без реального доступа к нему.
Aync/Await
как выполнить операцию А и сохранить эту операцию СОСТОЯНИЕ. Теперь вы хотите, чтобы пользователь щелкнул «Сетка». Поэтому, если пользователь щелкает «Сетка», вы проверяете состояние, если оно истинно, а затем выполняете свою операцию, просто делая то, что вы хотите ??