CompositeDisposable.cs 16 KB

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