CompositeDisposable.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the Apache 2.0 License.
  3. // See the LICENSE file in the project root for more information.
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using System.Threading;
  7. namespace System.Reactive.Disposables
  8. {
  9. /// <summary>
  10. /// Represents a group of disposable resources that are disposed together.
  11. /// </summary>
  12. [Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Backward compat + ideally want to get rid of the ICollection nature of the type.")]
  13. public sealed class CompositeDisposable : ICollection<IDisposable>, ICancelable
  14. {
  15. private readonly object _gate = new object();
  16. private bool _disposed;
  17. private List<IDisposable> _disposables;
  18. private int _count;
  19. private const int SHRINK_THRESHOLD = 64;
  20. // Default initial capacity of the _disposables list in case
  21. // The number of items is not known upfront
  22. private const int DEFAULT_CAPACITY = 16;
  23. /// <summary>
  24. /// Initializes a new instance of the <see cref="CompositeDisposable"/> class with no disposables contained by it initially.
  25. /// </summary>
  26. public CompositeDisposable()
  27. {
  28. _disposables = new List<IDisposable>();
  29. }
  30. /// <summary>
  31. /// Initializes a new instance of the <see cref="CompositeDisposable"/> class with the specified number of disposables.
  32. /// </summary>
  33. /// <param name="capacity">The number of disposables that the new CompositeDisposable can initially store.</param>
  34. /// <exception cref="ArgumentOutOfRangeException"><paramref name="capacity"/> is less than zero.</exception>
  35. public CompositeDisposable(int capacity)
  36. {
  37. if (capacity < 0)
  38. {
  39. throw new ArgumentOutOfRangeException(nameof(capacity));
  40. }
  41. _disposables = new List<IDisposable>(capacity);
  42. }
  43. /// <summary>
  44. /// Initializes a new instance of the <see cref="CompositeDisposable"/> class from a group of disposables.
  45. /// </summary>
  46. /// <param name="disposables">Disposables that will be disposed together.</param>
  47. /// <exception cref="ArgumentNullException"><paramref name="disposables"/> is <c>null</c>.</exception>
  48. /// <exception cref="ArgumentException">Any of the disposables in the <paramref name="disposables"/> collection is <c>null</c>.</exception>
  49. public CompositeDisposable(params IDisposable[] disposables)
  50. {
  51. if (disposables == null)
  52. {
  53. throw new ArgumentNullException(nameof(disposables));
  54. }
  55. Init(disposables, disposables.Length);
  56. }
  57. /// <summary>
  58. /// Initializes a new instance of the <see cref="CompositeDisposable"/> class from a group of disposables.
  59. /// </summary>
  60. /// <param name="disposables">Disposables that will be disposed together.</param>
  61. /// <exception cref="ArgumentNullException"><paramref name="disposables"/> is <c>null</c>.</exception>
  62. /// <exception cref="ArgumentException">Any of the disposables in the <paramref name="disposables"/> collection is <c>null</c>.</exception>
  63. public CompositeDisposable(IEnumerable<IDisposable> disposables)
  64. {
  65. if (disposables == null)
  66. {
  67. throw new ArgumentNullException(nameof(disposables));
  68. }
  69. // If the disposables is a collection, get its size
  70. // and use it as a capacity hint for the copy.
  71. if (disposables is ICollection<IDisposable> c)
  72. {
  73. Init(disposables, c.Count);
  74. }
  75. else
  76. {
  77. // Unknown sized disposables, use the default capacity hint
  78. Init(disposables, DEFAULT_CAPACITY);
  79. }
  80. }
  81. /// <summary>
  82. /// Initialize the inner disposable list and count fields.
  83. /// </summary>
  84. /// <param name="disposables">The enumerable sequence of disposables.</param>
  85. /// <param name="capacityHint">The number of items expected from <paramref name="disposables"/></param>
  86. private void Init(IEnumerable<IDisposable> disposables, int capacityHint)
  87. {
  88. var list = new List<IDisposable>(capacityHint);
  89. // do the copy and null-check in one step to avoid a
  90. // second loop for just checking for null items
  91. foreach (var d in disposables)
  92. {
  93. if (d == null)
  94. {
  95. throw new ArgumentException(Strings_Core.DISPOSABLES_CANT_CONTAIN_NULL, nameof(disposables));
  96. }
  97. list.Add(d);
  98. }
  99. _disposables = list;
  100. // _count can be read by other threads and thus should be properly visible
  101. // also releases the _disposables contents so it becomes thread-safe
  102. Volatile.Write(ref _count, _disposables.Count);
  103. }
  104. /// <summary>
  105. /// Gets the number of disposables contained in the <see cref="CompositeDisposable"/>.
  106. /// </summary>
  107. public int Count => Volatile.Read(ref _count);
  108. /// <summary>
  109. /// Adds a disposable to the <see cref="CompositeDisposable"/> or disposes the disposable if the <see cref="CompositeDisposable"/> is disposed.
  110. /// </summary>
  111. /// <param name="item">Disposable to add.</param>
  112. /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
  113. public void Add(IDisposable item)
  114. {
  115. if (item == null)
  116. {
  117. throw new ArgumentNullException(nameof(item));
  118. }
  119. lock (_gate)
  120. {
  121. if (!_disposed)
  122. {
  123. _disposables.Add(item);
  124. // If read atomically outside the lock, it should be written atomically inside
  125. // the plain read on _count is fine here because manipulation always happens
  126. // from inside a lock.
  127. Volatile.Write(ref _count, _count + 1);
  128. return;
  129. }
  130. }
  131. item.Dispose();
  132. }
  133. /// <summary>
  134. /// Removes and disposes the first occurrence of a disposable from the <see cref="CompositeDisposable"/>.
  135. /// </summary>
  136. /// <param name="item">Disposable to remove.</param>
  137. /// <returns>true if found; false otherwise.</returns>
  138. /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
  139. public bool Remove(IDisposable item)
  140. {
  141. if (item == null)
  142. {
  143. throw new ArgumentNullException(nameof(item));
  144. }
  145. lock (_gate)
  146. {
  147. // this composite was already disposed and if the item was in there
  148. // it has been already removed/disposed
  149. if (_disposed)
  150. {
  151. return false;
  152. }
  153. //
  154. // List<T> doesn't shrink the size of the underlying array but does collapse the array
  155. // by copying the tail one position to the left of the removal index. We don't need
  156. // index-based lookup but only ordering for sequential disposal. So, instead of spending
  157. // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also
  158. // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it.
  159. //
  160. // read fields as infrequently as possible
  161. var current = _disposables;
  162. var i = current.IndexOf(item);
  163. if (i < 0)
  164. {
  165. // not found, just return
  166. return false;
  167. }
  168. current[i] = null;
  169. if (current.Capacity > SHRINK_THRESHOLD && _count < current.Capacity / 2)
  170. {
  171. var fresh = new List<IDisposable>(current.Capacity / 2);
  172. foreach (var d in current)
  173. {
  174. if (d != null)
  175. {
  176. fresh.Add(d);
  177. }
  178. }
  179. _disposables = fresh;
  180. }
  181. // make sure the Count property sees an atomic update
  182. Volatile.Write(ref _count, _count - 1);
  183. }
  184. // if we get here, the item was found and removed from the list
  185. // just dispose it and report success
  186. item.Dispose();
  187. return true;
  188. }
  189. /// <summary>
  190. /// Disposes all disposables in the group and removes them from the group.
  191. /// </summary>
  192. public void Dispose()
  193. {
  194. var currentDisposables = default(List<IDisposable>);
  195. lock (_gate)
  196. {
  197. if (!_disposed)
  198. {
  199. currentDisposables = _disposables;
  200. // nulling out the reference is faster no risk to
  201. // future Add/Remove because _disposed will be true
  202. // and thus _disposables won't be touched again.
  203. _disposables = null;
  204. Volatile.Write(ref _count, 0);
  205. Volatile.Write(ref _disposed, true);
  206. }
  207. }
  208. if (currentDisposables != null)
  209. {
  210. foreach (var d in currentDisposables)
  211. {
  212. d?.Dispose();
  213. }
  214. }
  215. }
  216. /// <summary>
  217. /// Removes and disposes all disposables from the <see cref="CompositeDisposable"/>, but does not dispose the <see cref="CompositeDisposable"/>.
  218. /// </summary>
  219. public void Clear()
  220. {
  221. var previousDisposables = default(IDisposable[]);
  222. lock (_gate)
  223. {
  224. // disposed composites are always clear
  225. if (_disposed)
  226. {
  227. return;
  228. }
  229. var current = _disposables;
  230. previousDisposables = current.ToArray();
  231. current.Clear();
  232. Volatile.Write(ref _count, 0);
  233. }
  234. foreach (var d in previousDisposables)
  235. {
  236. d?.Dispose();
  237. }
  238. }
  239. /// <summary>
  240. /// Determines whether the <see cref="CompositeDisposable"/> contains a specific disposable.
  241. /// </summary>
  242. /// <param name="item">Disposable to search for.</param>
  243. /// <returns>true if the disposable was found; otherwise, false.</returns>
  244. /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception>
  245. public bool Contains(IDisposable item)
  246. {
  247. if (item == null)
  248. {
  249. throw new ArgumentNullException(nameof(item));
  250. }
  251. lock (_gate)
  252. {
  253. if (_disposed)
  254. {
  255. return false;
  256. }
  257. return _disposables.Contains(item);
  258. }
  259. }
  260. /// <summary>
  261. /// Copies the disposables contained in the <see cref="CompositeDisposable"/> to an array, starting at a particular array index.
  262. /// </summary>
  263. /// <param name="array">Array to copy the contained disposables to.</param>
  264. /// <param name="arrayIndex">Target index at which to copy the first disposable of the group.</param>
  265. /// <exception cref="ArgumentNullException"><paramref name="array"/> is <c>null</c>.</exception>
  266. /// <exception cref="ArgumentOutOfRangeException"><paramref name="arrayIndex"/> is less than zero. -or - <paramref name="arrayIndex"/> is larger than or equal to the array length.</exception>
  267. public void CopyTo(IDisposable[] array, int arrayIndex)
  268. {
  269. if (array == null)
  270. {
  271. throw new ArgumentNullException(nameof(array));
  272. }
  273. if (arrayIndex < 0 || arrayIndex >= array.Length)
  274. {
  275. throw new ArgumentOutOfRangeException(nameof(arrayIndex));
  276. }
  277. lock (_gate)
  278. {
  279. // disposed composites are always empty
  280. if (_disposed)
  281. {
  282. return;
  283. }
  284. if (arrayIndex + _count > array.Length)
  285. {
  286. // there is not enough space beyond arrayIndex
  287. // to accommodate all _count disposables in this composite
  288. throw new ArgumentOutOfRangeException(nameof(arrayIndex));
  289. }
  290. var i = arrayIndex;
  291. foreach (var d in _disposables)
  292. {
  293. if (d != null)
  294. {
  295. array[i++] = d;
  296. }
  297. }
  298. }
  299. }
  300. /// <summary>
  301. /// Always returns false.
  302. /// </summary>
  303. public bool IsReadOnly => false;
  304. /// <summary>
  305. /// Returns an enumerator that iterates through the <see cref="CompositeDisposable"/>.
  306. /// </summary>
  307. /// <returns>An enumerator to iterate over the disposables.</returns>
  308. public IEnumerator<IDisposable> GetEnumerator()
  309. {
  310. lock (_gate)
  311. {
  312. if (_disposed || _count == 0)
  313. {
  314. return EMPTY_ENUMERATOR;
  315. }
  316. // the copy is unavoidable but the creation
  317. // of an outer IEnumerable is avoidable
  318. return new CompositeEnumerator(_disposables.ToArray());
  319. }
  320. }
  321. /// <summary>
  322. /// Returns an enumerator that iterates through the <see cref="CompositeDisposable"/>.
  323. /// </summary>
  324. /// <returns>An enumerator to iterate over the disposables.</returns>
  325. IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
  326. /// <summary>
  327. /// Gets a value that indicates whether the object is disposed.
  328. /// </summary>
  329. public bool IsDisposed => Volatile.Read(ref _disposed);
  330. /// <summary>
  331. /// An empty enumerator for the <see cref="GetEnumerator"/>
  332. /// method to avoid allocation on disposed or empty composites.
  333. /// </summary>
  334. private static readonly CompositeEnumerator EMPTY_ENUMERATOR =
  335. new CompositeEnumerator(new IDisposable[0]);
  336. /// <summary>
  337. /// An enumerator for an array of disposables.
  338. /// </summary>
  339. private sealed class CompositeEnumerator : IEnumerator<IDisposable>
  340. {
  341. private readonly IDisposable[] _disposables;
  342. private int _index;
  343. public CompositeEnumerator(IDisposable[] disposables)
  344. {
  345. _disposables = disposables;
  346. _index = -1;
  347. }
  348. public IDisposable Current => _disposables[_index];
  349. object IEnumerator.Current => _disposables[_index];
  350. public void Dispose()
  351. {
  352. // Avoid retention of the referenced disposables
  353. // beyond the lifecycle of the enumerator.
  354. // Not sure if this happens by default to
  355. // generic array enumerators though.
  356. var disposables = _disposables;
  357. Array.Clear(disposables, 0, disposables.Length);
  358. }
  359. public bool MoveNext()
  360. {
  361. var disposables = _disposables;
  362. for (; ; )
  363. {
  364. var idx = ++_index;
  365. if (idx >= disposables.Length)
  366. {
  367. return false;
  368. }
  369. // inlined that filter for null elements
  370. if (disposables[idx] != null)
  371. {
  372. return true;
  373. }
  374. }
  375. }
  376. public void Reset()
  377. {
  378. _index = -1;
  379. }
  380. }
  381. }
  382. }