5
5
using System ;
6
6
using System . Collections . Generic ;
7
7
using System . Runtime . CompilerServices ;
8
+ using System . Threading ;
8
9
using Microsoft . Collections . Extensions ;
9
10
using Microsoft . Toolkit . Mvvm . Messaging . Internals ;
10
11
#if NETSTANDARD2_0
13
14
using RecipientsTable = System . Runtime . CompilerServices . ConditionalWeakTable < object , Microsoft . Collections . Extensions . IDictionarySlim > ;
14
15
#endif
15
16
17
+ #pragma warning disable SA1204
18
+
16
19
namespace Microsoft . Toolkit . Mvvm . Messaging
17
20
{
18
21
/// <summary>
19
22
/// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
20
23
/// </summary>
21
24
/// <remarks>
25
+ /// <para>
22
26
/// This <see cref="IMessenger"/> implementation uses weak references to track the registered
23
27
/// 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>
24
34
/// </remarks>
25
35
public sealed class WeakReferenceMessenger : IMessenger
26
36
{
@@ -46,6 +56,22 @@ public sealed class WeakReferenceMessenger : IMessenger
46
56
/// </summary>
47
57
private readonly DictionarySlim < Type2 , RecipientsTable > recipientsMap = new ( ) ;
48
58
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
+
49
75
/// <summary>
50
76
/// Gets the default <see cref="WeakReferenceMessenger"/> instance.
51
77
/// </summary>
@@ -224,61 +250,97 @@ public void Cleanup()
224
250
{
225
251
lock ( this . recipientsMap )
226
252
{
227
- using ArrayPoolBufferWriter < Type2 > type2s = ArrayPoolBufferWriter < Type2 > . Create ( ) ;
228
- using ArrayPoolBufferWriter < object > emptyRecipients = ArrayPoolBufferWriter < object > . Create ( ) ;
253
+ CleanupWithoutLock ( ) ;
254
+ }
255
+ }
229
256
230
- var enumerator = this . recipientsMap . GetEnumerator ( ) ;
257
+ /// <inheritdoc/>
258
+ public void Reset ( )
259
+ {
260
+ lock ( this . recipientsMap )
261
+ {
262
+ this . recipientsMap . Clear ( ) ;
263
+ }
264
+ }
231
265
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 )
236
280
{
237
- emptyRecipients . Reset ( ) ;
281
+ CleanupWithoutLock ( ) ;
282
+ }
283
+ }
284
+ finally
285
+ {
286
+ if ( lockTaken )
287
+ {
288
+ Monitor . Exit ( lockObject ) ;
289
+ }
290
+ }
291
+ }
238
292
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 ( ) ;
240
301
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 ( ) ;
254
310
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 )
257
318
{
258
- enumerator . Value . Remove ( recipient ) ;
319
+ emptyRecipients . Add ( pair . Key ) ;
259
320
}
260
-
261
- // Track the type combinations with no recipients or handlers left
262
- if ( ! hasAtLeastOneHandler )
321
+ else
263
322
{
264
- type2s . Add ( enumerator . Key ) ;
323
+ hasAtLeastOneHandler = true ;
265
324
}
266
325
}
267
326
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 )
270
335
{
271
- this . recipientsMap . TryRemove ( key ) ;
336
+ type2s . Add ( enumerator . Key ) ;
272
337
}
273
338
}
274
- }
275
339
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 )
280
342
{
281
- this . recipientsMap . Clear ( ) ;
343
+ this . recipientsMap . TryRemove ( key ) ;
282
344
}
283
345
}
284
346
0 commit comments