"Làm thế nào để chặn dòng mã cho đến khi một sự kiện được kích hoạt?"
Bạn tiếp cận là sai. Hướng sự kiện không có nghĩa là chặn và chờ đợi một sự kiện. Bạn không bao giờ chờ đợi, ít nhất bạn luôn cố gắng hết sức để tránh nó. Chờ đợi là lãng phí tài nguyên, chặn các luồng và có thể gây ra nguy cơ bế tắc hoặc luồng zombie (trong trường hợp tín hiệu không bao giờ được đưa ra).
Cần phải rõ ràng rằng việc chặn một chuỗi để chờ đợi một sự kiện là một mô hình chống vì nó mâu thuẫn với ý tưởng của một sự kiện.
Bạn thường có hai tùy chọn (hiện đại): triển khai API không đồng bộ hoặc API hướng sự kiện. Vì bạn không muốn triển khai API không đồng bộ, nên bạn chỉ còn lại API theo hướng sự kiện.
Chìa khóa của API hướng sự kiện là, thay vì buộc người gọi phải chờ đồng bộ kết quả hoặc thăm dò kết quả, bạn hãy để người gọi tiếp tục và gửi thông báo cho anh ta khi kết quả đã sẵn sàng hoặc hoạt động đã hoàn tất. Trong khi đó, người gọi có thể tiếp tục thực hiện các hoạt động khác.
Khi xem xét vấn đề từ góc độ luồng, thì API hướng sự kiện cho phép luồng gọi, ví dụ, luồng UI, thực thi trình xử lý sự kiện của nút, được tự do tiếp tục xử lý các hoạt động liên quan đến giao diện người dùng, như kết xuất các thành phần UI hoặc xử lý đầu vào của người dùng như di chuyển chuột và nhấn phím. Hiệu ứng tương tự như API không đồng bộ, mặc dù ít thuận tiện hơn.
Vì bạn không cung cấp đủ chi tiết về những gì bạn đang thực sự cố gắng, những gì Utility.PickPoint()
thực sự đang làm và kết quả của nhiệm vụ là gì hoặc tại sao người dùng phải nhấp vào `Grid, tôi không thể cung cấp cho bạn một giải pháp tốt hơn . Tôi chỉ có thể cung cấp một mô hình chung về cách thực hiện yêu cầu của bạn.
Luồng hoặc mục tiêu của bạn rõ ràng được chia thành ít nhất hai bước để biến nó thành một chuỗi các hoạt động:
- Thực hiện thao tác 1, khi người dùng nhấp vào nút
- Thực hiện thao tác 2 (tiếp tục / hoàn thành thao tác 1), khi người dùng nhấp vào
Grid
với ít nhất hai ràng buộc:
- Tùy chọn: chuỗi phải được hoàn thành trước khi ứng dụng khách API được phép lặp lại. Một chuỗi được hoàn thành sau khi hoạt động 2 đã chạy đến hoàn thành.
- Hoạt động 1 luôn được thực hiện trước khi hoạt động 2. Hoạt động 1 bắt đầu chuỗi.
- Hoạt động 1 phải hoàn thành trước khi máy khách API được phép thực hiện thao tác 2
Điều này yêu cầu hai thông báo cho ứng dụng khách API để cho phép tương tác không chặn:
- Thao tác 1 đã hoàn thành (hoặc yêu cầu tương tác)
- Hoạt động 2 (hoặc mục tiêu) đã hoàn thành
Bạn nên để API của mình thực hiện hành vi và các ràng buộc này bằng cách hiển thị hai phương thức chung và hai sự kiện chung.
Triển khai / tái cấu trúc API tiện ích
Tiện ích.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));
}
}
PickPointCompletedEventArss.cs
class PickPointCompletedEventArgs : EventArgs
{
public Point Result { get; }
public PickPointCompletedEventArgs(Point result)
{
this.Result = result;
}
}
Sử dụng 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>
Nhận xét
Các sự kiện được nêu trên một luồng nền sẽ thực thi các trình xử lý của chúng trên cùng một luồng. Truy cập một DispatcherObject
phần tử UI giống như một trình xử lý, được thực thi trên luồng nền, đòi hỏi thao tác quan trọng phải được Dispatcher
sử dụng để sử dụng Dispatcher.Invoke
hoặc Dispatcher.InvokeAsync
để tránh các ngoại lệ xuyên luồng.
Một số suy nghĩ - trả lời ý kiến của bạn
Bởi vì bạn đang tiếp cận tôi để tìm giải pháp chặn "tốt hơn", cho tôi ví dụ về các ứng dụng bảng điều khiển, tôi cảm thấy thuyết phục bạn rằng nhận thức hoặc quan điểm của bạn là hoàn toàn sai.
"Hãy xem xét một ứng dụng Console có hai dòng mã này.
var str = Console.ReadLine();
Console.WriteLine(str);
Điều gì xảy ra khi bạn thực thi ứng dụng trong chế độ gỡ lỗi. Nó sẽ dừng ở dòng mã đầu tiên và buộc bạn nhập một giá trị trong Bảng điều khiển UI và sau đó sau khi bạn nhập một cái gì đó và nhấn Enter, nó sẽ thực thi dòng tiếp theo và thực sự in những gì bạn đã nhập. Tôi đã suy nghĩ về chính xác hành vi tương tự nhưng trong ứng dụng WPF. "
Một ứng dụng giao diện điều khiển là một cái gì đó hoàn toàn khác nhau. Khái niệm luồng là khác nhau. Các ứng dụng bảng điều khiển không có GUI. Chỉ cần luồng đầu vào / đầu ra / lỗi. Bạn không thể so sánh kiến trúc của ứng dụng bảng điều khiển với ứng dụng GUI phong phú. Điều này sẽ không làm việc. Bạn thực sự phải hiểu và chấp nhận điều này.
WPF được xây dựng xung quanh một luồng kết xuất và một luồng UI. Các luồng đó luôn luôn quay để giao tiếp với HĐH như xử lý đầu vào của người dùng - giữ cho ứng dụng phản hồi nhanh . Bạn không bao giờ muốn tạm dừng / chặn luồng này vì nó sẽ ngăn khung làm việc nền thiết yếu (như phản hồi các sự kiện chuột - bạn không muốn chuột đóng băng):
chờ đợi = chặn chủ đề = không phản hồi = xấu UX = người dùng / khách hàng khó chịu = rắc rối trong văn phòng.
Đôi khi, luồng ứng dụng yêu cầu đợi đầu vào hoặc một thói quen hoàn thành. Nhưng chúng tôi không muốn chặn luồng chính.
Đó là lý do tại sao mọi người phát minh ra các mô hình lập trình không đồng bộ phức tạp, để cho phép chờ mà không chặn luồng chính và không buộc nhà phát triển phải viết mã đa luồng phức tạp và sai lầm.
Mỗi khung ứng dụng hiện đại cung cấp các hoạt động không đồng bộ hoặc mô hình lập trình không đồng bộ, để cho phép phát triển mã đơn giản và hiệu quả.
Thực tế là bạn đang cố gắng hết sức để chống lại mô hình lập trình không đồng bộ, cho thấy sự thiếu hiểu biết đối với tôi. Mọi nhà phát triển hiện đại đều thích API không đồng bộ hơn API đồng bộ. Không có nhà phát triển nghiêm túc quan tâm đến việc sử dụng await
từ khóa hoặc để tuyên bố phương pháp của mình async
. Không ai. Bạn là người đầu tiên tôi gặp phải phàn nàn về API không đồng bộ và là người thấy chúng bất tiện khi sử dụng.
Nếu tôi sẽ kiểm tra khuôn khổ của bạn, mục tiêu để giải quyết các vấn đề liên quan đến UI hoặc làm cho các tác vụ liên quan đến UI dễ dàng hơn, tôi sẽ mong đợi nó không đồng bộ - tất cả các cách.
API liên quan đến giao diện người dùng không đồng bộ là lãng phí, vì nó sẽ làm phức tạp phong cách lập trình của tôi, do đó mã của tôi do đó dễ bị lỗi hơn và khó bảo trì.
Một viễn cảnh khác: khi bạn thừa nhận rằng việc chờ đợi sẽ chặn luồng UI, tạo ra trải nghiệm người dùng rất tệ và không mong muốn vì UI sẽ đóng băng cho đến khi chờ đợi kết thúc, bây giờ bạn nhận ra điều này, tại sao bạn lại cung cấp mô hình API hoặc plugin khuyến khích một nhà phát triển để làm chính xác điều này - thực hiện chờ đợi?
Bạn không biết plugin của bên thứ 3 sẽ làm gì và một thói quen sẽ mất bao lâu cho đến khi hoàn thành. Đây đơn giản là một thiết kế API tồi. Khi API của bạn hoạt động trên luồng UI thì người gọi API của bạn phải có thể thực hiện các cuộc gọi không chặn đến nó.
Nếu bạn từ chối giải pháp rẻ tiền hoặc duyên dáng hơn là sử dụng cách tiếp cận theo hướng sự kiện như trong ví dụ của tôi.
Nó thực hiện những gì bạn muốn: bắt đầu một thói quen - chờ người dùng nhập vào - tiếp tục thực hiện - hoàn thành mục tiêu.
Tôi thực sự đã cố gắng nhiều lần để giải thích tại sao chờ đợi / chặn là một thiết kế ứng dụng tồi. Một lần nữa, bạn không thể so sánh giao diện người dùng bảng điều khiển với giao diện người dùng đồ họa phong phú, trong đó, ví dụ xử lý đầu vào đơn giản là vô cùng phức tạp hơn là chỉ nghe luồng đầu vào. Tôi thực sự không biết mức độ kinh nghiệm của bạn và nơi bạn bắt đầu, nhưng bạn nên bắt đầu chấp nhận mô hình lập trình không đồng bộ. Tôi không biết lý do tại sao bạn cố gắng tránh nó. Nhưng nó không khôn ngoan chút nào.
Ngày nay các mô hình lập trình không đồng bộ được triển khai ở mọi nơi, trên mọi nền tảng, trình biên dịch, mọi môi trường, trình duyệt, máy chủ, máy tính để bàn, cơ sở dữ liệu - ở mọi nơi. Mô hình hướng sự kiện cho phép đạt được cùng một mục tiêu, nhưng nó ít thuận tiện hơn để sử dụng (đăng ký / hủy đăng ký / từ các sự kiện), dựa trên các chủ đề nền. Hướng sự kiện là kiểu cũ và chỉ nên được sử dụng khi thư viện không đồng bộ không có sẵn hoặc không áp dụng.
"Tôi đã thấy hành vi chính xác trong Autodesk Revit."
Hành vi (những gì bạn trải nghiệm hoặc quan sát) khác nhiều so với cách thực hiện trải nghiệm này. Hai điều khác nhau. Autodesk của bạn rất có thể sử dụng các thư viện hoặc tính năng ngôn ngữ không đồng bộ hoặc một số cơ chế phân luồng khác. Và nó cũng là bối cảnh liên quan. Khi phương thức trong tâm trí bạn đang thực thi trên một luồng nền thì nhà phát triển có thể chọn chặn luồng này. Anh ta có một lý do rất tốt để làm điều này hoặc chỉ đưa ra một lựa chọn thiết kế tồi. Bạn đang hoàn toàn đi sai đường;) Chặn là không tốt.
(Mã nguồn Autodesk có phải là nguồn mở không? Hoặc làm thế nào để bạn biết cách triển khai?)
Tôi không muốn xúc phạm bạn, xin hãy tin tôi. Nhưng vui lòng xem xét lại để triển khai API không đồng bộ. Chỉ trong đầu bạn, các nhà phát triển không muốn sử dụng async / await. Bạn rõ ràng đã có suy nghĩ sai lầm. Và quên đi đối số ứng dụng bảng điều khiển đó - nó vô nghĩa;)
API liên quan đến giao diện người dùng PHẢI sử dụng async / chờ đợi bất cứ khi nào có thể. Mặt khác, bạn để lại tất cả công việc để viết mã không chặn cho máy khách API của bạn. Bạn sẽ buộc tôi bọc mọi cuộc gọi đến API của bạn thành một chuỗi nền. Hoặc để sử dụng xử lý sự kiện ít thoải mái. Hãy tin tôi - mọi nhà phát triển thay vì trang trí cho các thành viên của mình async
, hơn là xử lý sự kiện. Mỗi khi bạn sử dụng các sự kiện bạn có thể gặp rủi ro rò rỉ bộ nhớ - tùy thuộc vào một số trường hợp, nhưng rủi ro là có thật và không hiếm khi lập trình bất cẩn.
Tôi thực sự hy vọng bạn hiểu tại sao chặn là xấu. Tôi thực sự hy vọng bạn quyết định sử dụng async / await để viết API không đồng bộ hiện đại. Tuy nhiên, tôi đã chỉ cho bạn một cách rất phổ biến để chờ đợi không chặn, sử dụng các sự kiện, mặc dù tôi khuyên bạn nên sử dụng async / await.
"API sẽ cho phép lập trình viên có quyền truy cập vào UI và v.v. Bây giờ giả sử lập trình viên muốn phát triển một bổ trợ mà khi nhấn nút, người dùng cuối được yêu cầu chọn một điểm trong UI"
Nếu bạn không muốn cho phép plugin có quyền truy cập trực tiếp vào các thành phần UI, bạn nên cung cấp giao diện để ủy quyền các sự kiện hoặc hiển thị các thành phần bên trong thông qua các đối tượng được trừu tượng hóa.
API bên trong sẽ đăng ký các sự kiện UI thay mặt cho Bổ trợ và sau đó ủy thác sự kiện bằng cách hiển thị một sự kiện "trình bao bọc" tương ứng cho máy khách API. API của bạn phải cung cấp một số móc nối trong đó Bổ trợ có thể kết nối để truy cập các thành phần ứng dụng cụ thể. API plugin hoạt động như một bộ chuyển đổi hoặc mặt tiền để cung cấp cho bên ngoài quyền truy cập vào bên trong.
Để cho phép một mức độ cô lập.
Hãy xem cách Visual Studio quản lý các plugin hoặc cho phép chúng tôi triển khai chúng. Giả sử bạn muốn viết một plugin cho Visual Studio và thực hiện một số nghiên cứu về cách thực hiện việc này. Bạn sẽ nhận ra rằng Visual Studio trưng bày các phần bên trong của nó thông qua một giao diện hoặc API. Vì bạn có thể thao tác với trình soạn thảo mã hoặc lấy thông tin về nội dung của trình soạn thảo mà không cần truy cập thực sự vào nó.
Aync/Await
cách thực hiện Thao tác A và lưu hoạt động đó STATE ngay bây giờ bạn muốn người dùng đó nên nhấp vào Lưới .. vì vậy nếu người dùng Nhấp vào Lưới bạn kiểm tra trạng thái nếu đúng thì thao tác của bạn có làm gì khác không?