Tại sao trách nhiệm của người gọi là đảm bảo an toàn luồng trong lập trình GUI?


37

Tôi đã thấy, ở nhiều nơi, đó là sự khôn ngoan kinh điển 1 , đó là trách nhiệm của người gọi để đảm bảo bạn đang ở trên luồng UI khi cập nhật các thành phần UI (cụ thể, trong Java Swing, rằng bạn đang ở trong Chuỗi xử lý sự kiện ) .

Tại sao cái này rất? Chủ đề công văn sự kiện là mối quan tâm của khung nhìn trong MVC / MVP / MVVM; để xử lý nó ở bất cứ đâu nhưng chế độ xem tạo ra sự kết hợp chặt chẽ giữa triển khai của chế độ xem và mô hình luồng của triển khai của chế độ xem đó.

Cụ thể, giả sử tôi có một ứng dụng kiến ​​trúc MVC sử dụng Swing. Nếu người gọi chịu trách nhiệm cập nhật các thành phần trên Chuỗi xử lý sự kiện, thì nếu tôi cố gắng trao đổi triển khai Swing View của mình để triển khai JavaFX, tôi phải thay đổi tất cả mã Trình dẫn / Trình điều khiển để sử dụng luồng Ứng dụng JavaFX .

Vì vậy, tôi cho rằng tôi có hai câu hỏi:

  1. Tại sao trách nhiệm của người gọi là đảm bảo an toàn luồng thành phần UI? Đâu là lỗ hổng trong lý luận của tôi ở trên?
  2. Làm cách nào tôi có thể thiết kế ứng dụng của mình để có sự ghép lỏng lẻo các mối quan tâm an toàn luồng này, nhưng vẫn an toàn cho luồng?

Hãy để tôi thêm một số mã Java MCVE để minh họa ý nghĩa của "người gọi chịu trách nhiệm" (có một số thực tiễn tốt khác ở đây mà tôi không làm nhưng tôi đang cố gắng tối thiểu nhất có thể):

Người gọi chịu trách nhiệm:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        view.setData(data);
      }
    });
  }
}
public class View {
  void setData(Data data) {
    component.setText(data.getMessage());
  }
}

Xem có trách nhiệm:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    view.setData(data);
  }
}
public class View {
  void setData(Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        component.setText(data.getMessage());
      }
    });
  }
}

1: Tác giả của bài đăng đó có số điểm thẻ cao nhất trong Swing on Stack Overflow. Anh ấy nói điều này ở khắp mọi nơi và tôi cũng đã thấy đó là trách nhiệm của người gọi ở những nơi khác.


1
Eh, hiệu suất IMHO. Những bài đăng sự kiện đó không được cung cấp miễn phí và trong các ứng dụng không tầm thường mà bạn muốn giảm thiểu số lượng của chúng (và đảm bảo không quá lớn), nhưng việc giảm thiểu / ngưng tụ nên được thực hiện một cách hợp lý trong người trình bày.
Thông thường

1
@Ordous Bạn vẫn có thể đảm bảo rằng các bài đăng là tối thiểu trong khi đưa chủ đề bàn giao vào chế độ xem.
durron597

2
Tôi đã đọc một blog thực sự tốt cách đây một thời gian để thảo luận về vấn đề này, về cơ bản nó nói rằng rất nguy hiểm khi thử và làm cho chuỗi bộ công cụ UI an toàn, vì nó đưa ra các bế tắc có thể xảy ra và tùy thuộc vào cách nó được triển khai, điều kiện cuộc đua vào khuôn khổ. Ngoài ra còn có một xem xét hiệu suất. Bây giờ không nhiều lắm, nhưng khi Swing được phát hành lần đầu tiên, nó đã bị chỉ trích nặng nề vì hiệu suất của nó (rất tệ), nhưng đó không thực sự là lỗi của Swing, đó là sự thiếu hiểu biết của mọi người về cách sử dụng nó.
MadProgrammer

1
SWT thực thi khái niệm an toàn luồng bằng cách đưa ra các ngoại lệ nếu bạn vi phạm nó, không đẹp, nhưng ít nhất bạn đã nhận thức được về nó. Bạn đề cập đến việc thay đổi từ Swing sang JavaFX, nhưng bạn gặp phải vấn đề này với bất kỳ khung UI nào, Swing dường như là vấn đề làm nổi bật vấn đề. Bạn có thể thiết kế một lớp trung gian (bộ điều khiển của bộ điều khiển?) Có nhiệm vụ đảm bảo rằng các cuộc gọi đến UI được đồng bộ hóa chính xác. Không thể biết chính xác làm thế nào bạn có thể thiết kế các phần không phải UI của API theo quan điểm của API UI
MadProgrammer

1
và hầu hết các nhà phát triển sẽ phàn nàn rằng bất kỳ sự bảo vệ luồng nào được triển khai trong API UI là hạn chế hoặc không đáp ứng nhu cầu của họ. Tốt hơn là cho phép bạn quyết định cách bạn muốn giải quyết vấn đề này, dựa trên nhu cầu của bạn
MadProgrammer

Câu trả lời:


22

Đến cuối bài luận về giấc mơ thất bại của mình , Graham Hamilton (một kiến ​​trúc sư Java lớn) đề cập đến việc nếu các nhà phát triển "giữ gìn sự tương đương với một mô hình hàng đợi sự kiện, họ sẽ cần tuân theo các quy tắc không rõ ràng khác nhau" và có thể nhìn thấy rõ ràng và rõ ràng mô hình hàng đợi sự kiện "dường như giúp mọi người theo dõi mô hình một cách đáng tin cậy hơn và do đó xây dựng các chương trình GUI hoạt động đáng tin cậy."

Nói cách khác, nếu bạn cố gắng đặt một mặt tiền đa luồng lên trên một mô hình hàng đợi sự kiện, sự trừu tượng đôi khi sẽ bị rò rỉ theo những cách không rõ ràng rất khó gỡ lỗi. Có vẻ như nó sẽ hoạt động trên giấy, nhưng cuối cùng lại sụp đổ trong sản xuất.

Thêm các trình bao bọc nhỏ xung quanh các thành phần đơn lẻ có lẽ sẽ không có vấn đề gì, như cập nhật một thanh tiến trình từ một luồng công nhân. Nếu bạn cố gắng làm một cái gì đó phức tạp hơn đòi hỏi nhiều khóa, nó bắt đầu trở nên rất khó để suy luận về cách lớp đa luồng và lớp hàng đợi sự kiện tương tác.

Lưu ý rằng các loại vấn đề này là phổ biến cho tất cả các bộ công cụ GUI. Giả sử một mô hình điều phối sự kiện trong người thuyết trình / bộ điều khiển của bạn không kết hợp chặt chẽ bạn với chỉ một mô hình đồng thời của bộ công cụ GUI cụ thể, nó sẽ ghép bạn với tất cả chúng . Giao diện xếp hàng sự kiện không quá khó để trừu tượng.


25

Bởi vì làm cho chủ đề lib của GUI an toàn là một vấn đề đau đầu và tắc nghẽn.

Luồng điều khiển trong GUI thường đi theo 2 hướng từ hàng đợi sự kiện đến cửa sổ gốc đến các widget gui và từ mã ứng dụng đến widget được truyền lên tới cửa sổ gốc.

Việc đưa ra một chiến lược khóa không khóa cửa sổ gốc (sẽ gây ra nhiều tranh cãi) là điều khó khăn . Khóa từ dưới lên trong khi một luồng khác đang khóa từ trên xuống là một cách tuyệt vời để đạt được bế tắc ngay lập tức.

Và kiểm tra xem luồng hiện tại có phải là luồng gui mỗi lần có tốn kém không và có thể gây nhầm lẫn về những gì đang thực sự xảy ra với gui, đặc biệt là khi bạn đang thực hiện trình tự ghi cập nhật đọc. Điều đó cần phải có một khóa trên dữ liệu để tránh các cuộc đua.


1
Thật thú vị, tôi giải quyết vấn đề này bằng cách có một khung xếp hàng cho các bản cập nhật mà tôi đã viết để quản lý chuyển giao luồng.
durron597

2
@ durron597: Và bạn không bao giờ có bất kỳ cập nhật nào tùy thuộc vào trạng thái hiện tại của giao diện người dùng mà chủ đề khác có thể ảnh hưởng? Sau đó, nó có thể làm việc.
Ded repeatator

Tại sao bạn cần khóa lồng nhau? Tại sao bạn cần khóa toàn bộ cửa sổ gốc khi làm việc với các chi tiết trong cửa sổ con? Thứ tự khóa là một vấn đề có thể giải quyết được bằng cách cung cấp một phương thức tiếp xúc duy nhất để khóa các khóa đột biến theo đúng thứ tự (từ trên xuống hoặc từ dưới lên, nhưng không để lại sự lựa chọn cho người gọi)
MSalters

1
@MSalters với root Tôi có nghĩa là cửa sổ hiện tại. Để có được tất cả các khóa bạn cần có, bạn cần phải đi lên cấu trúc phân cấp yêu cầu khóa từng container khi bạn gặp nó và mở khóa (để đảm bảo bạn chỉ khóa từ trên xuống) và sau đó hy vọng nó không bị thay đổi bạn sau khi bạn nhận được cửa sổ gốc và bạn thực hiện khóa từ trên xuống.
ratchet freak

@ratchetfreak: Nếu đứa trẻ bạn cố khóa bị xóa bởi một luồng khác trong khi bạn đang khóa nó, điều đó hơi đáng tiếc nhưng không liên quan đến khóa. Bạn không thể hoạt động trên một đối tượng mà một luồng khác vừa xóa. Nhưng tại sao luồng đó lại loại bỏ các đối tượng / cửa sổ mà luồng của bạn vẫn đang sử dụng? Điều đó không tốt trong bất kỳ kịch bản nào, không chỉ UI.
MSalters

17

Threadedness (trong mô hình bộ nhớ dùng chung) là một thuộc tính có xu hướng thách thức các nỗ lực trừu tượng hóa. Một ví dụ đơn giản là một Setkiểu: while Contains(..)Add(...)Update(...)là một API hoàn toàn hợp lệ trong một kịch bản đơn luồng, kịch bản đa luồng cần một AddOrUpdate.

Điều tương tự cũng áp dụng cho UI - nếu bạn muốn hiển thị danh sách các mục với số lượng mục ở đầu danh sách, bạn cần cập nhật cả hai trên mỗi thay đổi.

  1. Bộ công cụ không thể giải quyết vấn đề đó vì khóa không đảm bảo thứ tự các thao tác được chính xác.
  2. Chế độ xem có thể giải quyết vấn đề, nhưng chỉ khi bạn cho phép quy tắc kinh doanh thì số trên đầu danh sách phải khớp với số mục trong danh sách và chỉ cập nhật danh sách qua chế độ xem. Không chính xác những gì MVC được cho là.
  3. Người trình bày có thể giải quyết nó, nhưng cần phải biết rằng chế độ xem có các nhu cầu đặc biệt liên quan đến luồng.
  4. Databinding đến một mô hình có khả năng đa luồng là một lựa chọn khác. Nhưng điều đó làm phức tạp mô hình với những thứ đáng quan tâm về UI.

Không ai trong số họ trông thực sự hấp dẫn. Làm cho người trình bày chịu trách nhiệm xử lý luồng được khuyến nghị không phải vì nó tốt, mà bởi vì nó hoạt động và các lựa chọn thay thế tồi tệ hơn.


Có lẽ một lớp có thể được giới thiệu ở giữa Chế độ xem và Người thuyết trình cũng có thể được hoán đổi.
durron597

2
.NET có khả System.Windows.Threading.Dispatchernăng xử lý việc gửi đến cả WPF và WinForms UI-Themes. Loại lớp giữa người trình bày và chế độ xem chắc chắn là hữu ích. Nhưng nó chỉ cung cấp độc lập bộ công cụ, không độc lập luồng.
Patrick

9

Tôi đã đọc một blog thực sự tốt cách đây một thời gian để thảo luận về vấn đề này (được đề cập bởi Karl Bielefeldt), về cơ bản nó nói rằng nó rất nguy hiểm để thử và làm cho chuỗi bộ công cụ UI an toàn, vì nó đưa ra những bế tắc có thể xảy ra và tùy thuộc vào cách nó thực hiện, điều kiện cuộc đua vào khuôn khổ.

Ngoài ra còn có một xem xét hiệu suất. Bây giờ không nhiều lắm, nhưng khi Swing được phát hành lần đầu tiên, nó đã bị chỉ trích nặng nề vì hiệu suất của nó (rất tệ), nhưng đó không thực sự là lỗi của Swing, đó là sự thiếu hiểu biết của mọi người về cách sử dụng nó.

SWT thực thi khái niệm an toàn luồng bằng cách đưa ra các ngoại lệ nếu bạn vi phạm nó, không đẹp, nhưng ít nhất bạn đã nhận thức được về nó.

Nếu bạn nhìn vào quá trình vẽ tranh, ví dụ, thứ tự các yếu tố được vẽ là rất quan trọng. Bạn không muốn bức tranh của một thành phần có tác dụng phụ trên bất kỳ phần nào khác của màn hình. Hãy tưởng tượng nếu bạn có thể cập nhật thuộc tính văn bản của nhãn, nhưng nó được vẽ bởi hai luồng khác nhau, bạn có thể kết thúc bằng một đầu ra bị hỏng. Vì vậy, tất cả các bức tranh được thực hiện trong một chủ đề duy nhất, thông thường dựa trên thứ tự yêu cầu / yêu cầu (nhưng đôi khi cô đọng để giảm số lượng chu kỳ sơn vật lý thực tế)

Bạn đề cập đến việc thay đổi từ Swing sang JavaFX, nhưng bạn sẽ gặp vấn đề này với bất kỳ khung UI nào (không chỉ các máy khách dày, mà cả web), Swing dường như là vấn đề làm nổi bật vấn đề.

Bạn có thể thiết kế một lớp trung gian (bộ điều khiển của bộ điều khiển?) Có nhiệm vụ đảm bảo rằng các cuộc gọi đến UI được đồng bộ hóa chính xác. Không thể biết chính xác làm thế nào bạn có thể thiết kế các phần không phải UI của API theo quan điểm của API UI và hầu hết các nhà phát triển sẽ phàn nàn rằng mọi bảo vệ luồng được triển khai trong API UI là để hạn chế hoặc không đáp ứng nhu cầu của họ. Tốt hơn là cho phép bạn quyết định cách bạn muốn giải quyết vấn đề này, dựa trên nhu cầu của bạn

Một trong những vấn đề lớn nhất bạn cần xem xét là khả năng biện minh cho một thứ tự các sự kiện nhất định, dựa trên các đầu vào đã biết. Ví dụ: nếu người dùng thay đổi kích thước cửa sổ, mô hình Hàng đợi Sự kiện đảm bảo rằng một thứ tự các sự kiện đã cho sẽ xảy ra, điều này có vẻ đơn giản, nhưng nếu hàng đợi cho phép các sự kiện được kích hoạt bởi các luồng khác, thì bạn không còn có thể đảm bảo thứ tự trong mà các sự kiện có thể xảy ra (một điều kiện cuộc đua) và đột nhiên bạn phải bắt đầu lo lắng về các trạng thái khác nhau và không làm một điều gì cho đến khi có điều gì khác xảy ra và bạn bắt đầu phải chia sẻ cờ trạng thái xung quanh và bạn kết thúc với spaghetti.

Được rồi, bạn có thể giải quyết điều này bằng cách có một số loại hàng đợi đã đặt hàng các sự kiện dựa trên thời gian chúng được phát hành, nhưng đó không phải là những gì chúng ta đã có? Ngoài ra, bạn vẫn không thể đảm bảo rằng luồng B sẽ tạo ra các sự kiện SAU KHI A

Lý do chính khiến mọi người khó chịu về việc phải suy nghĩ về mã của họ, là vì họ được tạo ra để suy nghĩ về mã / thiết kế của họ. "Tại sao nó không thể đơn giản hơn?" Nó không thể đơn giản hơn, vì đó không phải là vấn đề đơn giản.

Tôi nhớ khi PS3 được phát hành và Sony đã nói về bộ xử lý Cell và khả năng thực hiện các dòng logic riêng biệt, giải mã âm thanh, video, tải và giải quyết dữ liệu mô hình. Một nhà phát triển trò chơi đã hỏi, "Điều đó thật tuyệt vời, nhưng làm thế nào để bạn đồng bộ hóa các luồng?"

Vấn đề mà nhà phát triển đã nói đến là, tại một số điểm, tất cả các luồng riêng biệt đó sẽ cần phải được đồng bộ hóa thành một ống dẫn cho đầu ra. Người dẫn chương trình nghèo chỉ nhún vai vì đó không phải là vấn đề mà họ quen thuộc. Rõ ràng, họ đã có giải pháp để giải quyết vấn đề này ngay bây giờ, nhưng nó thật buồn cười vào thời điểm đó.

Các máy tính hiện đại đang nhận rất nhiều đầu vào từ nhiều nơi khác nhau cùng một lúc, tất cả những đầu vào đó cần được xử lý và gửi đến người dùng mà không can thiệp vào việc trình bày thông tin khác, vì vậy đây là một vấn đề phức tạp. một giải pháp đơn giản.

Bây giờ, có khả năng chuyển đổi các khung công tác, đó không phải là một điều dễ dàng để thiết kế, NHƯNG, hãy sử dụng MVC trong giây lát, MVC có thể được nhiều lớp, nghĩa là bạn có thể có một MVC liên quan trực tiếp đến việc quản lý khung UI, bạn sau đó có thể bọc nó, một lần nữa, trong một lớp MVC cao hơn liên quan đến các tương tác với các khung công tác (có khả năng đa luồng) khác, lớp này sẽ có trách nhiệm xác định cách thức thông báo / cập nhật lớp MVC thấp hơn.

Sau đó, bạn sẽ sử dụng mã hóa để giao diện các mẫu thiết kế và các mẫu nhà máy hoặc nhà xây dựng để xây dựng các lớp khác nhau này. Điều này có nghĩa là, các khung công tác đa luồng của bạn trở nên tách rời khỏi lớp UI thông qua việc sử dụng lớp giữa, như một ý tưởng.


2
Bạn sẽ không gặp vấn đề này với web. JavaScript cố tình không có hỗ trợ luồng - thời gian chạy JavaScript thực sự chỉ là một hàng đợi sự kiện lớn. .
James_pic

1
@James_pic Những gì bạn thực sự có là một phần của trình duyệt đóng vai trò là trình đồng bộ hóa cho hàng đợi sự kiện, về cơ bản là những gì chúng ta đã nói, người gọi chịu trách nhiệm đảm bảo rằng các cập nhật xảy ra với hàng đợi sự kiện của bộ công cụ
MadProgrammer

Đúng chính xác. Sự khác biệt chính, trong ngữ cảnh của web, là sự đồng bộ hóa này xảy ra bất kể mã của người gọi, vì thời gian chạy không cung cấp cơ chế nào cho phép thực thi mã bên ngoài hàng đợi sự kiện. Vì vậy, người gọi không cần phải chịu trách nhiệm cho nó. Tôi tin rằng đó cũng là một phần lớn của động lực thúc đẩy sự phát triển của NodeJS - nếu môi trường thời gian chạy là một vòng lặp sự kiện, thì tất cả các mã đều là vòng lặp sự kiện theo mặc định.
James_pic

1
Điều đó nói rằng, có các khung giao diện người dùng trình duyệt có vòng lặp sự kiện của riêng họ (tôi đang nhìn vào bạn Angular). Vì các khung công tác này không được bảo vệ bởi thời gian chạy nữa, nên người gọi cũng phải đảm bảo mã được thực thi trong vòng lặp sự kiện, tương tự như mã đa luồng trong các khung khác.
James_pic

Sẽ có bất kỳ vấn đề cụ thể nào với việc các điều khiển "chỉ hiển thị" tự động cung cấp an toàn luồng? Nếu một người có một điều khiển văn bản có thể chỉnh sửa được viết bởi một luồng và được đọc bởi một luồng khác, thì không có cách nào tốt để người viết có thể nhìn thấy luồng mà không đồng bộ hóa ít nhất một trong những hành động đó với trạng thái UI thực tế của điều khiển, Nhưng liệu có vấn đề nào như vậy đối với các điều khiển chỉ hiển thị không? Tôi nghĩ rằng những thứ đó có thể dễ dàng cung cấp bất kỳ khóa hoặc khóa liên động cần thiết nào cho các thao tác được thực hiện trên chúng và cho phép người gọi bỏ qua thời gian khi ...
supercat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.