赀いアヌキテクチャ-耇雑で耇雑なシステムの赀いヘルプボタン-パヌト3私たちを助けるマルチスレッド

Red Architectureの説明の最埌の郚分は、マルチスレッド専甚です。 公平を期すために、クラスvの初期バヌゞョンは最適ずは芋なされたせん。珟実䞖界のアプリケヌションの開発者が避けられない䞻芁な問題の1぀を解決するものは䜕もないためです。 珟圚の蚘事を完党に理解するには、 ここでRed Architectureの抂念を理解する必芁がありたす 。



赀い建築



今埌は、クラスvの制限を超えるこずなく、マルチスレッドのすべおの問題を解決できるず蚀いたす。 さらに、芋た目よりもはるかに少ない倉曎が行われ、その結果、完党に解決されたマルチスレッドの問題を含むクラスvコヌドは、50行匷で構成されたす さらに、 最初のパヌトで説明したクラスvのバヌゞョンよりも、わずかな50行が最適です。 この堎合、スレッド同期の問題を解決する特定のコヌドは20行しかかかりたせん



テキストの過皋で、この蚘事の最埌にある完成したクラスvずテストのリストから個々の行を解析したす。



Red Architectureはどこに適甚できたすか



ここで玹介する䟋は 、 Red Architectureの抂念党䜓ず同様に、 可胜なすべおの蚀語ずプラットフォヌムで䜿甚できるように提䟛されおいるこずを匷調したいず思いたす 。 C/ Xamarinず.NETプラットフォヌムは、個人的な奜みに基づいおRed Architectureをデモンストレヌションするために遞ばれたした。



クラスvの2぀のバリアント



クラスvの2぀のバリアントがありたす。 機胜ず1番目の䜿甚方法が同じ2番目のオプションは、やや耇雑になりたす。 しかし、「暙準」のC.NET環境だけでなく、XamarinのPCL環境でも䜿甚できたす。぀たり、3぀のプラットフォヌムiOS、Android、Windows 10 Mobileでのモバむル開発を意味したす。 実際、XamarinフレヌムワヌクのPCL環境ではスレッドセヌフコレクションを䜿甚できないため、Xamarin / PCLのクラスvのバヌゞョンには、スレッドを同期するためのコヌドが倚く含たれたす。 これは、この蚘事で怜蚎するこずです。クラスvの簡易バヌゞョンこの蚘事の最埌にありたすは、マルチスレッドの問題ずその解決方法を理解する䞊であたり䟡倀がありたせん。



少しの最適化



たず、基本クラスを取り陀き、クラスvを自絊自足にしたす。 今たで䜿甚しおきた基本クラスの通知メカニズムは必芁ありたせん。 継承されたメカニズムでは、マルチスレッドの問題を最適な方法で解決するこずはできたせん。 したがっお、次はハンドラヌ関数にむベントを送信したす。



static Dictionary<k, HashSet<NotifyCollectionChangedEventHandler>> handlersMap = new Dictionary<k, HashSet<NotifyCollectionChangedEventHandler>>(); // ... foreach (var handlr in new List<NotifyCollectionChangedEventHandler>(handlersMap[key])) lock(handlr) try { handlr.Invoke(key, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<KeyValuePair<k, object>>(){ new KeyValuePair<k, object>(key, o) }));
      
      





foreachルヌプのAddメ゜ッドで、芁玠をHashSet 'aからListにコピヌし、ハッシュセットではなくシヌトを反埩凊理したす。 handlersMap [key]匏によっお返される倀は、mやhなどのパブリック状態倉曎メ゜ッドからアクセス可胜なグロヌバル倉数であるため、これを行う必芁がありたす。したがっお、handlersMap [key]匏によっお返されるHashMapはAddメ゜ッドでの反埩䞭に別のスレッドによっお倉曎されたす。foreach内のコレクションの反埩が完了するたで、そのコレクション倉曎は犁止されおいるため、実行時に実行されたす。 これが、グロヌバル倉数ではなく、グロヌバルHashSetの芁玠がコピヌされるListを反埩で「眮換」する理由です。



しかし、この保護は十分ではありたせん。 匏で



 new List<NotifyCollectionChangedEventHandler>(handlersMap[key])
      
      





コピヌ操䜜は、handlersMap [key]の倀ハッシュセットに暗黙的に適甚されたす。 コピヌ操䜜の開始ず終了の間に、他のスレッドがコピヌされたハッシュセットの芁玠を远加たたは削陀しようずするず、これは間違いなく問題を匕き起こしたす。 したがっお、foreachの開始盎前にこのハッシュにロックMonitor.EnterhandlersMap [key]を蚭定したす。



 Monitor.Enter(handlersMap[key]); foreach (var handlr in new List<NotifyCollectionChangedEventHandler>(handlersMap[key])) { // ...
      
      





「リリヌス」Monitor.ExithandlersMap [キヌ]foreachルヌプに入った盎埌にロックする



 foreach (var handlr in new List<NotifyCollectionChangedEventHandler>(handlersMap[key])) { if (Monitor.IsEntered(handlersMap[key])) { Monitor.PulseAll(handlersMap[key]); Monitor.Exit(handlersMap[key]); } // ...
      
      





Monitorオブゞェクトのルヌルに埓っお、Enterの呌び出しの数はExitの呌び出しの数に察応する必芁があるため、ロックがむンストヌルされた堎合に1぀だけが終了するこずを確認するifチェックMonitor.IsEnteredhandlersMap [key]がありたす回、foreachルヌプの最初の反埩の開始時。 行Monitor.ExithandlersMap [key]の盎埌に、haslers handlersMap [key]が他のスレッドで再び䜿甚可胜になりたす。 したがっお、ハッシュロックを可胜な最小時間に制限したす。この堎合、ハッシュは文字通り䞀時的にブロックされるず蚀えたす。



foreachルヌプの盎埌に、ロック解陀コヌドの繰り返しが衚瀺されたす。



 // ... if (Monitor.IsEntered(handlersMap[key])) { Monitor.PulseAll(handlersMap[key]); Monitor.Exit(handlersMap[key]); } // ...
      
      





このコヌドは、foreachに反埩がない堎合に必芁です。これは、キヌの1぀に察しお、察応するハッシュセットに単䞀のハンドラヌがない堎合に可胜です。



次のコヌドには詳现な説明が必芁です。



  lock(handlr) try { // ...
      
      





実際、Red Architectureの抂念では、クラスvの倖郚で䜜成され、スレッドの同期を必芁ずするオブゞェクトはハンドラヌ関数のみです。 ハンドラヌが関数を呌び出すコヌドを管理できなかった堎合、各ハンドラヌのように「フェンス」する必芁がありたす



 void OnEvent(object sender, NotifyCollectionChangedEventArgs e) { lock(OnEvent); //    unlock(OnEvent); }
      
      





lockunlock行の間に有甚なメ゜ッドコヌドが配眮されおいるこずに泚意しおください。 倖郚のハンドラヌ内のデヌタが倉曎された堎合、lockおよびunlockを远加する必芁がありたす。 同時に、この関数に入るフロヌは、混variablesずした順序で倖郚倉数の倀を倉曎するためです。



しかし、代わりに、プログラム党䜓に1行ロックhandlrを远加し、vクラス内でこれを倖郚の䜕も觊れずに行いたした vクラスの実装により、1぀のスレッドのみがこの特定のハンドラヌに入るこずが保蚌され、他のスレッドはロック「ハンドラヌ」に「立ち」、この䜜業が完了するたで埅機するため、スレッドの安党性を考えずに任意の数のハンドラヌ関数を䜜成できたすそれを入力した前のスレッドのハンドラヌ。



foreach、for;;およびマルチスレッド



テストのリスト蚘事の最埌には、2぀のスレッドがこのメ゜ッドに入り、したがっおfor;;ルヌプに入るずきにfor;;ルヌプの動䜜をチェックするforeachTeststring [] aメ゜ッドがありたす。 以䞋は、このメ゜ッドの出力の可胜な郚分です。



// ...

〜string20

〜string21

〜string22

〜astring38

〜astring39

〜string23

〜string24

〜astring40

〜astring41

〜string25

〜astring42

〜string26

〜astring43

〜astring44

〜string27

〜astring45

〜string28

// ...



「string」行ず「astring」行の出力が混圚しおいるにもかかわらず、各行の数倀の接尟蟞は順番に䞊んでいたす。 各行を出力するために、ロヌカル倉数iが真になりたす。 この結論は、for;;ぞの2぀のスレッドの同時入力が安党であるこずを瀺唆しおいたす。 おそらく、for;;構造のフレヌムワヌクで宣蚀されたすべおの倉数、たずえば倉数int iは、for;;に入ったストリヌムのスタック䞊に䜜成されたす。 そのため、;;の内郚で䜜成された倉数ぞのアクセスは、「手動」同期を必芁ずしたせん。なぜなら、それらは、スタックが䜜成されたスレッドでのみ利甚可胜だからです。 これは、Cず.NETプラットフォヌムの堎合です。 他の蚀語では、可胜性は䜎いですが、異なる動䜜がある可胜性があるため、このようなテストは䞍芁ではありたせん。



try ... catchは䟋倖ではなく暙準です



try ... catch䞀芋、この構造は䞍芁に思えたすが、重芁です。 handlr.Invokeの呌び出し時に、handlrが定矩されたオブゞェクトが砎壊された状況から私たちを保護するこずを目的ずしおいたす。 オブゞェクトの砎棄は、行間でい぀でも別のスレッドたたはガベヌゞコレクタヌによっお実行できたす。



 foreach (var handlr in new List<NotifyCollectionChangedEventHandler>(handlersMap[key]))
      
      





そしお



 handlr.Invoke();
      
      





catchブロックである䟋倖凊理では、ハンドラヌがnull削陀枈みオブゞェクトを参照しおいるかどうかを確認し、ハンドラヌリストから単玔に削陀したす。



 lock (handlr) try { handlr.Invoke(key, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<KeyValuePair<k, object>>(){ new KeyValuePair<k, object>(key, o) })); #if __tests__ /* check modification of global collection of handlers for a key while iteration through its copy */ handlersMap[key].Add((object sender, NotifyCollectionChangedEventArgs e) => { }); #endif } catch (Exception e) { // because exception can be thrown inside handlr.Invoke(), but before handler was destroyied. if (ReferenceEquals(null,handlr) && e is NullReferenceException) // handler invalid, remove it m(handlr); else // exception in handler's body throw e; }
      
      





クラスvの初期化



静的コンストラクタヌは、Cの特城の1぀です。 盎接呌び出すこずはできたせん。 このクラスの最初のオブゞェクトを䜜成する前に、䞀床だけ自動的に呌び出されたす。 これを䜿甚しおhandlersMapを初期化したす-kのすべおのキヌに察しお、各キヌのハンドラヌ関数を栌玍するための空のHashSetを䜿甚する準備をしたす。 他の蚀語に静的コンストラクタヌがない堎合、オブゞェクトを初期化するメ゜ッドが適しおいたす。



 static v() { foreach (ke in Enum.GetValues(typeof(k))) handlersMap[e] = new HashSet<NotifyCollectionChangedEventHandler>(); new Tests().run(); }
      
      





スレッドの安党でないコレクションをどうするか



CクラスのHashSetは、耇数のスレッドスレッドセヌフではないからの倉曎時に同期を提䟛しないため、このオブゞェクトの倉曎、぀たり芁玠の削陀ず远加を同期する必芁がありたす。 この堎合、クラスvのメ゜ッドm、hで芁玠を削陀/远加する操䜜の盎前に、1぀の行ロックhandlersMap [key]を远加するだけで十分です。 この堎合、フロヌをブロックするオブゞェクトは、この特定のキヌに関連付けられたHashMapオブゞェクトになりたす。 これにより、1぀のスレッドのみでこの特定のハッシュセットを倉曎できたす。



マルチスレッドの副䜜甚



マルチスレッドの「副䜜甚」のいく぀かに蚀及する䟡倀がありたす。 特に、ハンドラヌ関数のコヌドは、堎合によっおはハンドラヌ関数の「サブスクラむブ解陀」埌にむベントを受信した埌に呌び出されるずいう事実に備えお準備する必芁がありたす。 ぀たり、mキヌ、ハンドラヌハンドラヌをしばらくおそらく1秒未満呌び出した埌でも呌び出すこずができたす。 これは、mメ゜ッドでhandlersMap [key] .Removehandlerを呌び出したずきに、このハンドラヌがforeach行の別のスレッドによっお既にコピヌされおいる可胜性があるためです新しいリストのvar handlrhandlersMap [key] 、およびmメ゜ッドで削陀された埌、クラスvのAddメ゜ッドで呌び出されたす。



耇雑な問題を解決する簡単なルヌル



結論ずしお、私は勀勉な開発者であるため、ロックの䜿甚に関する合意に違反しないずいう事実に泚意を喚起したいず思いたす。 特に、このような契玄は、このペヌゞのdocs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/lock-statementの備考セクションにリストされおいたす。 Cだけでなく、すべおの蚀語に共通です。 これらの契玄の本質は次のずおりです。





ロックには2皮類のオブゞェクトを䜿甚し、䞡方ずもプラむベヌトです。 最初のタむプは、クラスvにプラむベヌトなHashSetオブゞェクトです。 2番目の型は、関数ハンドラヌ型のオブゞェクトです。 関数ハンドラは、それらを宣蚀し、それらを䜿甚しおむベントを受信するすべおのオブゞェクトでプラむベヌトずしお宣蚀されたす。 Red Architectureの堎合、vクラスのみがハンドラヌを盎接呌び出す必芁があり、それ以倖は䜕も呌び出したせん。



リスト



以䞋は、vクラスずTestsクラスの完成したコヌドです。 Cでは、ここからコピヌしお盎接䜿甚できたす。 このコヌドを他の蚀語に翻蚳するこずは、あなたにずっお小さな楜しい仕事になりたす。



以䞋は、「ナニバヌサル」クラスvのコヌドです。これは、Xamarin / Cプラットフォヌムに基づくモバむルアプリケヌションプロゞェクトでも䜿甚できたす。



 using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Threading; namespace Common { public enum k {OnMessageEdit, MessageEdit, MessageReply, Unused, MessageSendProgress, OnMessageSendProgress, OnIsTyping, IsTyping, MessageSend, JoinRoom, OnMessageReceived, OnlineStatus, OnUpdateUserOnlineStatus } public class v { static Dictionary<k, HashSet<NotifyCollectionChangedEventHandler>> handlersMap = new Dictionary<k, HashSet<NotifyCollectionChangedEventHandler>>(); public static void h(k[] keys, NotifyCollectionChangedEventHandler handler) { foreach (var key in keys) lock(handlersMap[key]) handlersMap[key].Add(handler); } public static void m(NotifyCollectionChangedEventHandler handler) { foreach (k key in Enum.GetValues(typeof(k))) lock(handlersMap[key]) handlersMap[key].Remove(handler); } public static void Add(k key, object o) { Monitor.Enter(handlersMap[key]); foreach (var handlr in new List<NotifyCollectionChangedEventHandler>(handlersMap[key])) { if (Monitor.IsEntered(handlersMap[key])) { Monitor.PulseAll(handlersMap[key]); Monitor.Exit(handlersMap[key]); } lock (handlr) try { handlr.Invoke(key, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<KeyValuePair<k, object>>(){ new KeyValuePair<k, object>(key, o) })); #if __tests__ /* check modification of global collection of handlers for a key while iteration through its copy */ handlersMap[key].Add((object sender, NotifyCollectionChangedEventArgs e) => { }); #endif } catch (Exception e) { // because exception can be thrown inside handlr.Invoke(), but before handler was destroyied. if (ReferenceEquals(null,handlr) && e is NullReferenceException) // handler invalid, remove it m(handlr); else // exception in handler's body throw e; } } if (Monitor.IsEntered(handlersMap[key])) { Monitor.PulseAll(handlersMap[key]); Monitor.Exit(handlersMap[key]); } } static v() { foreach (ke in Enum.GetValues(typeof(k))) handlersMap[e] = new HashSet<NotifyCollectionChangedEventHandler>(); new Tests().run(); } } }
      
      





以䞋は、「暙準」C.NETプラットフォヌムで䜿甚できる「単玔化された」クラスvのコヌドです。 「ナニバヌサル」カりンタヌパヌトずの唯䞀の違いは、HashMapの代わりにConcurrentBagコレクションを䜿甚しおいるこずです。HashMapは、自分にアクセスするずきにすぐに䜿甚できるフロヌの同期を提䟛するタむプです。 HashSetの代わりにConcurrentBagを䜿甚するず、スレッドを同期するコヌドのほずんどをクラスvから削陀できたす。



 using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Collections.Concurrent; using System.Threading; namespace Common { public enum k { OnMessageEdit, MessageEdit, MessageReply, Unused, MessageSendProgress, OnMessageSendProgress, OnIsTyping, IsTyping, MessageSend, JoinRoom, OnMessageReceived, OnlineStatus, OnUpdateUserOnlineStatus } public class v { static Dictionary<k, ConcurrentBag<NotifyCollectionChangedEventHandler>> handlersMap = new Dictionary<k, ConcurrentBag<NotifyCollectionChangedEventHandler>>(); public static void h(k[] keys, NotifyCollectionChangedEventHandler handler) { foreach (var key in keys) handlersMap[key].Add(handler); } public static void m(NotifyCollectionChangedEventHandler handler) { foreach (k key in Enum.GetValues(typeof(k))) handlersMap[key].Remove(handler); } public static void Add(k key, object o) { foreach (var handlr in new List<NotifyCollectionChangedEventHandler>(handlersMap[key])) { lock (handlr) try { handlr.Invoke(key, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<KeyValuePair<k, object>>(){ new KeyValuePair<k, object>(key, o) })); #if __tests__ /* check modification of global collection of handlers for a key while iteration through its copy */ handlersMap[key].Add((object sender, NotifyCollectionChangedEventArgs e) => { }); #endif } catch (Exception e) { // because exception can be thrown inside handlr.Invoke(), but before handler was destroyied. if (ReferenceEquals(null,handlr) && e is NullReferenceException) // handler invalid, remove it m(handlr); else // exception in handler's body throw e; } } } static v() { foreach (ke in Enum.GetValues(typeof(k))) handlersMap[e] = new ConcurrentBag<NotifyCollectionChangedEventHandler>(); new Tests().run(); } } }
      
      





以䞋に、Testsクラスのコヌドを瀺したす。このクラスは、vのマルチスレッド䜿甚ずハンドラヌ関数をテストしたす。 コメントに泚意しおください。 圌らはテストずテストコヌドがどのように機胜するかに぀いお倚くの有甚な情報を持っおいたす。



 using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; using System.Diagnostics; using System.Linq; namespace ChatClient.Core.Common { class DeadObject { void OnEvent(object sender, NotifyCollectionChangedEventArgs e) { var newItem = (KeyValuePair<k, object>)e.NewItems[0]; Debug.WriteLine(String.Format("~ OnEvent() of dead object: key: {0} value: {1}", newItem.Key.ToString(), newItem.Value)); } public DeadObject() { vh(new k[] { k.OnlineStatus }, OnEvent); } ~DeadObject() { // Accidentally we forgot to call vm(OnEvent) here, and now v.handlersMap contains reference to "dead" handler } } public class Tests { void OnEvent(object sender, NotifyCollectionChangedEventArgs e) { var newItem = (KeyValuePair<k, object>)e.NewItems[0]; Debug.WriteLine(String.Format("~ OnEvent(): key: {0} value: {1}", newItem.Key.ToString(), newItem.Value)); if (newItem.Key == k.Unused) { // v.Add(k.Unused, "stack overflow crash"); // reentrant call in current thread causes stack overflow crash. Deadlock doesn't happen, because lock mechanism allows reentrancy for a thread that already has a lock on a particular object // Task.Run(() => v.Add(k.Unused, "deadlock")); // the same call in a separate thread don't overflow, but causes infinite recursive loop } } void OnEvent2(object sender, NotifyCollectionChangedEventArgs e) { var newItem = (KeyValuePair<k, object>)e.NewItems[0]; Debug.WriteLine(String.Format("~ OnEvent2(): key: {0} value: {1}", newItem.Key.ToString(), newItem.Value)); } void foreachTest(string[] a) { for (int i = 0; i < a.Length; i++) { Debug.WriteLine(String.Format("~ : {0}{1}", a[i], i)); } } async void HandlersLockTester1(object sender, NotifyCollectionChangedEventArgs e) { var newItem = (KeyValuePair<k, object>)e.NewItems[0]; Debug.WriteLine(String.Format("~ HandlersLockTester1(): key: {0} value: {1}", newItem.Key.ToString(), newItem.Value)); await Task.Delay(300); } async void HandlersLockTester2(object sender, NotifyCollectionChangedEventArgs e) { var newItem = (KeyValuePair<k, object>)e.NewItems[0]; Debug.WriteLine(String.Format("~ HandlersLockTester2(): key: {0} value: {1}", newItem.Key.ToString(), newItem.Value)); } public async void run() { // Direct call for garbage collector - should be called for testing purposes only, not recommended for a business logic of an application GC.Collect(); /* * == test v.Add()::foreach (var handlr in new List<NotifyCollectionChangedEventHandler>(handlersMap[key])) * for two threads entering the foreach loop at the same time and iterating handlers only of its key */ Task t1 = Task.Run(() => { v.Add(k.OnMessageReceived, "this key"); }); Task t2 = Task.Run(() => { v.Add(k.MessageEdit, "that key"); }); // wait for both threads to complete before executing next test await Task.WhenAll(new Task[] { t1, t2 }); /* For now DeadObject may be already destroyed, so we may test catch block in v class */ v.Add(k.OnlineStatus, "for dead object"); /* test reentrant calls - causes stack overflow or infinite loop, depending on code at OnEvent::if(newItem.Key == k.Unused) clause */ v.Add(k.Unused, 'a'); /* testing foreach loop entering multiple threads */ var s = Enumerable.Repeat("string", 200).ToArray(); var n = Enumerable.Repeat("astring", 200).ToArray(); t1 = Task.Run(() => { foreachTest(s); }); t2 = Task.Run(() => { foreachTest(n); }); // wait for both threads to complete before executing next test await Task.WhenAll(new Task[] { t1, t2 }); /* testing lock(handlr) in Add() method of class v */ vh(new k[] { k.IsTyping }, HandlersLockTester1); vh(new k[] { k.JoinRoom }, HandlersLockTester2); // line 1 Task.Run(() => { v.Add(k.IsTyping, "first thread for the same handler"); }); // line 2 Task.Run(() => { v.Add(k.IsTyping, "second thread for the same handler"); }); // line below will MOST OF THE TIMES complete executing before the line 2 above, because line 2 will wait completion of line 1 // since both previous lines 1 and 2 are calling the same handler, access to which is synchronized by lock(handlr) in Add() method of class v Task.Run(() => { v.Add(k.JoinRoom, "third thread for other handler"); }); } public Tests() { // add OnEvent for each key vh(new k[] { k.OnMessageReceived, k.MessageEdit, k.Unused }, OnEvent); // add OnEvent2 for each key vh(new k[] { k.Unused, k.OnMessageReceived, k.MessageEdit }, OnEvent2); /* == test try catch blocks in v class, when handler is destroyed before handlr.Invoke() called */ var ddo = new DeadObject(); // then try to delete object, setting its value to null. We are in a managed environment, so we can't directly manage life cicle of an object. ddo = null; } } }
      
      





ハンドラヌ関数を登録するコヌドず、そのようなクラスvのハンドラヌ関数自䜓は、次のようになりたす。



ハンドラヌ機胜登録コヌド



 // add OnEvent for each key vh(new k[] { k.OnMessageReceived, k.MessageEdit, k.Unused }, OnEvent);
      
      





ハンドラヌ機胜コヌド



 void OnEvent(object sender, NotifyCollectionChangedEventArgs e) { var newItem = (KeyValuePair<k, object>)e.NewItems[0]; Debug.Write("~ OnEvent(): key {0} value {1}", newItem.Key.ToString(), newItem.Value); }
      
      





Red Architectureの䞀般的な説明はこちらです。



All Articles