GUIプログラミングでスレッドの安全性を確保するのはなぜ呼び出し側の責任ですか?


37

多くの場所で、UIコンポーネントを更新するときにUIスレッド上にいること(具体的には、Javaスイングでは、イベントディスパッチスレッド上にいることを確認することは呼び出し側の責任であることは、標準的な知恵1です) 。

これはなぜですか?イベントディスパッチスレッドは、MVC / MVP / MVVMのビューの問題です。ビュー以外の場所で処理するには、ビューの実装と、そのビューの実装のスレッドモデルとの間に密結合を作成します。

具体的には、Swingを使用するMVC設計のアプリケーションがあるとします。呼び出し元がイベントディスパッチスレッドのコンポーネントの更新を担当している場合、JavaFX実装のSwing View実装を交換しようとすると、代わりにJavaFXアプリケーションスレッドを使用するようにすべてのプレゼンター/コントローラーコードを変更する必要があります

だから、私は2つの質問があると思う:

  1. UIコンポーネントのスレッドの安全性を確保するのは、呼び出し側の責任なのはなぜですか?上記の推論の欠陥はどこにありますか?
  2. これらのスレッド安全性の懸念を疎結合しながら、適切にスレッドセーフであるようにアプリケーションを設計するにはどうすればよいですか?

MCVE Javaコードを追加して、「呼び出し側の責任」が意味することを説明します(ここには、私がやっていませんが、できる限り最小限にするために他の良い習慣があります)。

責任者である発信者:

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());
  }
}

責任のあるビュー:

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:その投稿の作成者は、スタックオーバーフローのSwingで最高のタグスコアを獲得しています。彼はこれをあちこちで言っており、私もそれが他の場所での発信者の責任であることを見てきました。


1
ええ、パフォーマンス私見。これらのイベントの投稿は無料ではありません。重要なアプリでは、数を最小限に抑えたい(そして、どれも大きすぎないようにする)必要がありますが、その最小化/凝縮はプレゼンターで論理的に行う必要があります。
オーダス

1
@Ordousスレッドハンドオフをビューに配置しながら、投稿が最小限であることを確認できます。
durron597

2
私はこの問題について議論している本当に良いブログを読んでいますが、基本的に言うと、UIキットをスレッドセーフにすることは非常に危険であり、デッドロックを引き起こす可能性があり、実装方法によっては、フレームワーク。パフォーマンスに関する考慮事項もあります。それほど多くはありませんが、Swingが最初にリリースされたとき、Swingのパフォーマンスはひどく批判されていました(悪い)が、それは実際にはSwingのせいではなく、それを使用する方法に関する人々の知識不足でした。
MadProgrammer

1
SWTは、違反ではない場合、例外ではなく例外をスローすることでスレッドセーフの概念を実施しますが、少なくともそのことは認識されています。SwingからJavaFXへの変更について言及しましたが、ほぼすべてのUIフレームワークでこの問題が発生するでしょう。Swingは問題を強調するもののようです。UIの呼び出しが正しく同期されるようにすることを目的とする中間層(コントローラーのコントローラー?)を設計できます。UI APIの観点からAPIの非UI部分を設計する方法を正確に知ることは不可能です
-MadProgrammer

1
また、ほとんどの開発者は、UI APIに実装されたスレッド保護が制限的であるか、ニーズを満たしていないと不満を言うでしょう。ニーズに基づいて、この問題を解決する方法を決定できるようにする方が良い
MadProgrammer

回答:


22

失敗した夢のエッセイの終わりに向かって、Graham Hamilton(主要なJavaアーキテクト)は、開発者が「イベントキューモデルと同等性を維持するためには、さまざまな非自明なルールに従う必要があり、明示的かつ明示的にイベントキューモデルは、「人々がより確実にモデルに従うことができ、したがって、確実に動作するGUIプログラムを構築するのに役立つようです。」

言い換えると、イベントキューモデルの上にマルチスレッドファサードを配置しようとすると、デバッグが非常に困難な非自明な方法で抽象化が漏れることがあります。それは紙の上で動作するように思えますが、生産ではバラバラになってしまいます。

単一のコンポーネントの周りに小さなラッパーを追加しても、ワーカースレッドからプログレスバーを更新する場合のように、おそらく問題にはなりません。複数のロックを必要とするより複雑なことを行おうとすると、マルチスレッドレイヤーとイベントキューレイヤーがどのように相互作用するかを推論するのが非常に難しくなります。

これらの種類の問題は、すべてのGUIツールキットに共通していることに注意してください。プレゼンター/コントローラーのイベントディスパッチモデルは、特定のGUIツールキットの同時実行モデルにのみ密接に結合しているのではなく、すべてのモデルに結合しています。イベントキューインターフェイスは、抽象化するのが難しいものであってはなりません。


25

GUI libをスレッドセーフにすることは、大きな頭痛とボトルネックであるためです。

GUIの制御フローは、多くの場合、イベントキューからルートウィンドウ、GUIウィジェット、アプリケーションコードからルートウィンドウまで伝播されるウィジェットの2方向に進みます。

ルートウィンドウをロックしない(多くの競合を引き起こす)ロック戦略を考案することは困難です。別のスレッドがトップダウンでロックしているときにボトムアップでロックすると、すぐにデッドロックを達成できます。

また、現在のスレッドが毎回guiスレッドであるかどうかを毎回確認するのはコストがかかり、特に読み取り更新書き込みシーケンスを実行している場合、実際にguiで何が起こっているかについて混乱を招く可能性があります。競合を避けるために、データをロックする必要があります。


1
興味深いことに、スレッドハンドオフを管理するこれらの更新用のキューフレームワークを作成して、この問題を解決しています。
durron597

2
@ durron597:UIの現在の状態に応じて、他のスレッドが影響を与える可能性のある更新はありませんか?その後、動作する可能性があります。

ネストされたロックが必要なのはなぜですか?子ウィンドウの詳細を操作するときにルートウィンドウ全体をロックする必要があるのはなぜですか?ロック順序は、複数のロックを正しい順序でロックする単一の公開メソッドを提供することで解決可能な問題です(トップダウンまたはボトムアップのいずれかですが、呼び出し元に選択を任せないでください)
-MSalters

1
@MSalters with root私は現在のウィンドウを意味しました。取得する必要があるすべてのロックを取得するには、階層をたどる必要があります。これは、コンテナを見つけて親を取得し、ロックを解除して(トップダウンのみロックすることを保証するために)各コンテナをロックする必要があり、変更されていないことを願っていますルートウィンドウを取得し、トップダウンでロックを実行した後。
ラチェットフリーク

@ratchetfreak:ロックしようとしている子が、ロック中に別のスレッドによって削除された場合、それは少し残念ですが、ロックには関係ありません。別のスレッドが削除したオブジェクトを操作することはできません。しかし、なぜあなたのスレッドがまだ使用しているオブジェクト/ウィンドウを削除する他のスレッドですか?UIだけでなく、どのシナリオでもそれは良くありません。
–MSalters

17

(共有メモリモデルの)スレッド化は、抽象化の努力に逆らう傾向があるプロパティです。単純な例はSet-typeです。and Contains(..)およびAdd(...)and Update(...)は、シングルスレッドシナリオでは完全に有効なAPIですが、マルチスレッドシナリオではが必要AddOrUpdateです。

UIにも同じことが当てはまります。リストの一番上にあるアイテムの数を含むアイテムのリストを表示する場合は、変更するたびに両方を更新する必要があります。

  1. ロックでは操作の順序が正しいことを確認できないため、ツールキットではこの問題を解決できません。
  2. ビューは問題を解決できますが、リストの一番上の数字がリスト内のアイテムの数と一致する必要があるというビジネスルールを許可し、ビューを介してのみリストを更新する場合のみです。MVCが何であるかは正確ではありません。
  3. プレゼンターはそれを解決できますが、ビューにはスレッド化に関して特別なニーズがあることを認識する必要があります。
  4. マルチスレッド対応モデルへのデータバインディングも別のオプションです。しかし、それはモデルをUIの懸念事項で複雑にします。

それらのどれも本当に魅力的ではありません。プレゼンターにスレッド処理の責任を負わせることは、それが優れているからではなく、うまく機能し、代替案がより悪いために推奨されます。


ビューとプレゼンターの間にレイヤーを導入して、スワップアウトすることもできます。
durron597

2
.NETは、System.Windows.Threading.DispatcherWPFとWinForms UIスレッドの両方へのディスパッチを処理する必要があります。プレゼンターとビューの間のこの種のレイヤーは間違いなく便利です。ただし、スレッドの独立性ではなく、ツールキットの独立性のみを提供します。
パトリック

9

私はこの問題を議論する非常に良いブログを読んでいます(カール・ビーレフェルトが言及)、基本的に言っているのは、UIキットをスレッドセーフにすることは非常に危険であり、デッドロックを引き起こし、その方法によって異なりますフレームワークへの競合状態の実装。

パフォーマンスに関する考慮事項もあります。それほど多くはありませんが、Swingが最初にリリースされたとき、そのパフォーマンスはひどく批判されていました(悪い)が、それは実際にはSwingのせいではありませんでした。

SWTは、違反ではない場合に例外をスローすることにより、スレッドセーフの概念を実施します。

たとえば、ペイントプロセスを検討する場合、要素がペイントされる順序は非常に重要です。1つのコンポーネントのペイントが画面の他の部分に副作用を与えないようにする必要があります。ラベルのテキストプロパティを更新できても、2つの異なるスレッドでペイントされた場合、出力が破損する可能性があると想像してください。したがって、すべてのペイントは通常、要件/要求の順序に基づいて単一のスレッド内で行われます(ただし、実際の物理的なペイントサイクルの数を減らすために凝縮される場合があります)

SwingからJavaFXへの変更について言及しましたが、ほぼすべてのUIフレームワーク(シッククライアントだけでなくWebも)でこの問題が発生しますが、Swingは問題を強調するもののようです。

UIの呼び出しが正しく同期されるようにすることを目的とする中間層(コントローラーのコントローラー?)を設計できます。UI APIの観点からAPIの非UI部分を設計する方法を正確に知ることは不可能であり、ほとんどの開発者は、UI APIに実装されたスレッド保護が制限的であるか、ニーズを満たしていないと文句を言います。ニーズに基づいて、この問題をどのように解決したいかを決定できる方が良い

考慮する必要がある最大の問題の1つは、既知の入力に基づいて、イベントの特定の順序を正当化する機能です。たとえば、ユーザーがウィンドウのサイズを変更した場合、イベントキューモデルは特定のイベントの順序が発生することを保証しますが、これは単純に思えるかもしれませんが、キューが他のスレッドによるイベントのトリガーを許可した場合、その順序を保証できなくなりますイベントが発生する可能性があり(競合状態)、突然、異なる状態について心配し始め、何か他のことが起こるまで何もしないで、状態フラグを共有しなければならなくなり、スパゲッティになります。

OK その上、スレッドAの後でスレッドBがイベントを生成するという保証はまだありません。

人々が自分のコードについて考えなければならないことに動揺する主な理由は、彼らが自分のコード/デザインについて考えさせられるからです。「なぜもっと簡単にできないのですか?」単純な問題ではないため、これ以上単純にすることはできません。

PS3がリリースされ、SonyがCellプロセッサをアップトークしたときのことを覚えています。ロジックの個別のラインを実行し、オーディオ、ビデオをデコードし、モデルデータをロードして解決することができます。あるゲーム開発者は、「それはすべて素晴らしいですが、どのようにストリームを同期しますか?」と尋ねました。

開発者が話していた問題は、ある時点で、それらの個別のストリームをすべて出力用の単一パイプに同期する必要があることでした。貧しいプレゼンターは、彼らがよく知っている問題ではなかったので、ただ肩をすくめた。明らかに、彼らは今この問題を解決する解決策を持っていますが、それは当時面白いものです。

現代のコンピューターは多くの異なる場所から同時に多くの入力を取得していますが、そのすべての入力を処理して離れた場所にいるユーザーに配信する必要があるため、他の情報の表示を妨げる​​ことはないため、複雑な問題です。単一のシンプルなソリューション。

今、フレームワークを切り替える機能を持っているので、それを設計するのは簡単ではありませんが、MVCを少し取ってください、MVCは多層にすることができます、つまり、UIフレームワークの管理を直接扱うMVCを持つことができます次に、他の(潜在的にマルチスレッド化された)フレームワークとの相互作用を処理する上位層MVCで、下位MVC層に通知/更新する方法を決定するのはこの層の責任です。

その後、コーディングを使用して設計パターンとファクトリーまたはビルダーのパターンをインターフェースし、これらの異なるレイヤーを構築します。つまり、マルチスレッドフレームワークは、アイデアとして中間層を使用することでUI層から切り離されます。


2
Webではこの問題は発生しません。JavaScriptは意図的にスレッド化をサポートしていません-JavaScriptランタイムは事実上1つの大きなイベントキューにすぎません。(はい、厳密に言えばJSにはWebWorkersがあることを知っています-これらはナーフされたスレッドであり、他の言語のアクターのように振る舞います)。
James_pic

1
@James_picは、あなたが実際に持っている私たちは、発信者は更新がツールキットイベントキューで発生することを確実にすることを担当してき、の話をしてきたものを基本的にイベントキューのための同期器として動作するブラウザの一部である
MadProgrammer

はい、正確に。Webのコンテキストでの主な違いは、呼び出し側のコードに関係なくこの同期が発生することです。これは、ランタイムがイベントキュー外でコードを実行できるメカニズムを提供しないためです。したがって、呼び出し側はそれに対して責任を取る必要はありません。NodeJSの開発の背後にある動機の大きな部分でもあると思います-ランタイム環境がイベントループである場合、すべてのコードはデフォルトでイベントループを認識します。
James_pic

1
そうは言っても、独自のイベントループ内イベントループを持つブラウザーUIフレームワークがあります(Angularを見ています)。これらのフレームワークはランタイムによって保護されなくなったため、呼び出し元は、他のフレームワークのマルチスレッドコードと同様に、イベントループ内でコードが実行されることも保証する必要があります。
James_pic

「表示専用」コントロールがスレッドセーフを自動的に提供することに問題はありますか?あるスレッドによって書き込まれ、別のスレッドによって読み取られる編集可能なテキストコントロールがある場合、それらのアクションの少なくとも1つをコントロールの実際のUI状態に同期せずに、書き込みをスレッドに表示できる良い方法はありません。しかし、そのような問題は表示専用コントロールにとって重要ですか?私は...これらは簡単にそれらの上に実行された操作のために必要なすべてのロックまたはインターロックを提供し、呼び出し側はタイミングを無視する可能性がありますと思うだろう
supercat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.