55using System ;
66using System . Collections . Generic ;
77using System . Runtime . CompilerServices ;
8+ using System . Threading ;
89using Microsoft . Collections . Extensions ;
910using Microsoft . Toolkit . Mvvm . Messaging . Internals ;
1011#if NETSTANDARD2_0
1314using RecipientsTable = System . Runtime . CompilerServices . ConditionalWeakTable < object , Microsoft . Collections . Extensions . IDictionarySlim > ;
1415#endif
1516
17+ #pragma warning disable SA1204
18+
1619namespace Microsoft . Toolkit . Mvvm . Messaging
1720{
1821 /// <summary>
1922 /// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
2023 /// </summary>
2124 /// <remarks>
25+ /// <para>
2226 /// This <see cref="IMessenger"/> implementation uses weak references to track the registered
2327 /// recipients, so it is not necessary to manually unregister them when they're no longer needed.
28+ /// </para>
29+ /// <para>
30+ /// The <see cref="WeakReferenceMessenger"/> type will automatically perform internal trimming when
31+ /// full GC collections are invoked, so calling <see cref="Cleanup"/> manually is not necessary to
32+ /// ensure that on average the internal data structures are as trimmed and compact as possible.
33+ /// </para>
2434 /// </remarks>
2535 public sealed class WeakReferenceMessenger : IMessenger
2636 {
@@ -46,6 +56,22 @@ public sealed class WeakReferenceMessenger : IMessenger
4656 /// </summary>
4757 private readonly DictionarySlim < Type2 , RecipientsTable > recipientsMap = new ( ) ;
4858
59+ /// <summary>
60+ /// Initializes a new instance of the <see cref="WeakReferenceMessenger"/> class.
61+ /// </summary>
62+ public WeakReferenceMessenger ( )
63+ {
64+ // Register an automatic GC callback to trigger a non-blocking cleanup. This will ensure that the
65+ // current messenger instance is trimmed and without leftover recipient maps that are no longer used.
66+ // This is necessary (as in, some form of cleanup, either explicit or automatic like in this case)
67+ // because the ConditionalWeakTable<TKey, TValue> instances will just remove key-value pairs on their
68+ // own as soon as a key (ie. a recipient) is collected, causing their own keys (ie. the Type2 instances
69+ // mapping to each conditional table for a pair of message and token types) to potentially remain in the
70+ // root mapping structure but without any remaining recipients actually registered there, which just
71+ // adds unnecessary overhead when trying to enumerate recipients during broadcasting operations later on.
72+ Gen2GcCallback . Register ( static obj => ( ( WeakReferenceMessenger ) obj ) . CleanupWithNonBlockingLock ( ) , this ) ;
73+ }
74+
4975 /// <summary>
5076 /// Gets the default <see cref="WeakReferenceMessenger"/> instance.
5177 /// </summary>
@@ -224,61 +250,97 @@ public void Cleanup()
224250 {
225251 lock ( this . recipientsMap )
226252 {
227- using ArrayPoolBufferWriter < Type2 > type2s = ArrayPoolBufferWriter < Type2 > . Create ( ) ;
228- using ArrayPoolBufferWriter < object > emptyRecipients = ArrayPoolBufferWriter < object > . Create ( ) ;
253+ CleanupWithoutLock ( ) ;
254+ }
255+ }
229256
230- var enumerator = this . recipientsMap . GetEnumerator ( ) ;
257+ /// <inheritdoc/>
258+ public void Reset ( )
259+ {
260+ lock ( this . recipientsMap )
261+ {
262+ this . recipientsMap . Clear ( ) ;
263+ }
264+ }
231265
232- // First, we go through all the currently registered pairs of token and message types.
233- // These represents all the combinations of generic arguments with at least one registered
234- // handler, with the exception of those with recipients that have already been collected.
235- while ( enumerator . MoveNext ( ) )
266+ /// <summary>
267+ /// Executes a cleanup without locking the current instance. This method has to be
268+ /// invoked when a lock on <see cref="recipientsMap"/> has already been acquired.
269+ /// </summary>
270+ private void CleanupWithNonBlockingLock ( )
271+ {
272+ object lockObject = this . recipientsMap ;
273+ bool lockTaken = false ;
274+
275+ try
276+ {
277+ Monitor . TryEnter ( lockObject , ref lockTaken ) ;
278+
279+ if ( lockTaken )
236280 {
237- emptyRecipients . Reset ( ) ;
281+ CleanupWithoutLock ( ) ;
282+ }
283+ }
284+ finally
285+ {
286+ if ( lockTaken )
287+ {
288+ Monitor . Exit ( lockObject ) ;
289+ }
290+ }
291+ }
238292
239- bool hasAtLeastOneHandler = false ;
293+ /// <summary>
294+ /// Executes a cleanup without locking the current instance. This method has to be
295+ /// invoked when a lock on <see cref="recipientsMap"/> has already been acquired.
296+ /// </summary>
297+ private void CleanupWithoutLock ( )
298+ {
299+ using ArrayPoolBufferWriter < Type2 > type2s = ArrayPoolBufferWriter < Type2 > . Create ( ) ;
300+ using ArrayPoolBufferWriter < object > emptyRecipients = ArrayPoolBufferWriter < object > . Create ( ) ;
240301
241- // Go through the currently alive recipients to look for those with no handlers left. We track
242- // the ones we find to remove them outside of the loop (can't modify during enumeration).
243- foreach ( KeyValuePair < object , IDictionarySlim > pair in enumerator . Value )
244- {
245- if ( pair . Value . Count == 0 )
246- {
247- emptyRecipients . Add ( pair . Key ) ;
248- }
249- else
250- {
251- hasAtLeastOneHandler = true ;
252- }
253- }
302+ var enumerator = this . recipientsMap . GetEnumerator ( ) ;
303+
304+ // First, we go through all the currently registered pairs of token and message types.
305+ // These represents all the combinations of generic arguments with at least one registered
306+ // handler, with the exception of those with recipients that have already been collected.
307+ while ( enumerator . MoveNext ( ) )
308+ {
309+ emptyRecipients . Reset ( ) ;
254310
255- // Remove the handler maps for recipients that are still alive but with no handlers
256- foreach ( object recipient in emptyRecipients . Span )
311+ bool hasAtLeastOneHandler = false ;
312+
313+ // Go through the currently alive recipients to look for those with no handlers left. We track
314+ // the ones we find to remove them outside of the loop (can't modify during enumeration).
315+ foreach ( KeyValuePair < object , IDictionarySlim > pair in enumerator . Value )
316+ {
317+ if ( pair . Value . Count == 0 )
257318 {
258- enumerator . Value . Remove ( recipient ) ;
319+ emptyRecipients . Add ( pair . Key ) ;
259320 }
260-
261- // Track the type combinations with no recipients or handlers left
262- if ( ! hasAtLeastOneHandler )
321+ else
263322 {
264- type2s . Add ( enumerator . Key ) ;
323+ hasAtLeastOneHandler = true ;
265324 }
266325 }
267326
268- // Remove all the mappings with no handlers left
269- foreach ( Type2 key in type2s . Span )
327+ // Remove the handler maps for recipients that are still alive but with no handlers
328+ foreach ( object recipient in emptyRecipients . Span )
329+ {
330+ enumerator . Value . Remove ( recipient ) ;
331+ }
332+
333+ // Track the type combinations with no recipients or handlers left
334+ if ( ! hasAtLeastOneHandler )
270335 {
271- this . recipientsMap . TryRemove ( key ) ;
336+ type2s . Add ( enumerator . Key ) ;
272337 }
273338 }
274- }
275339
276- /// <inheritdoc/>
277- public void Reset ( )
278- {
279- lock ( this . recipientsMap )
340+ // Remove all the mappings with no handlers left
341+ foreach ( Type2 key in type2s . Span )
280342 {
281- this . recipientsMap . Clear ( ) ;
343+ this . recipientsMap . TryRemove ( key ) ;
282344 }
283345 }
284346
0 commit comments