WorkItemsQueue.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Threading;
  4. using System.Runtime.CompilerServices;
  5. namespace Amib.Threading.Internal
  6. {
  7. #region WorkItemsQueue class
  8. /// <summary>
  9. /// WorkItemsQueue class.
  10. /// </summary>
  11. public class WorkItemsQueue : IDisposable
  12. {
  13. #region Member variables
  14. /// <summary>
  15. /// Waiters queue (implemented as stack).
  16. /// </summary>
  17. private readonly WaiterEntry _headWaiterEntry = new();
  18. /// <summary>
  19. /// Waiters count
  20. /// </summary>
  21. private int _waitersCount = 0;
  22. /// <summary>
  23. /// Work items queue
  24. /// </summary>
  25. private readonly Queue<WorkItem> _workItems = new();
  26. /// <summary>
  27. /// Indicate that work items are allowed to be queued
  28. /// </summary>
  29. private bool _isWorkItemsQueueActive = true;
  30. [ThreadStatic]
  31. private static WaiterEntry _waiterEntry;
  32. /// <summary>
  33. /// A flag that indicates if the WorkItemsQueue has been disposed.
  34. /// </summary>
  35. private bool _isDisposed = false;
  36. #endregion
  37. #region Public properties
  38. /// <summary>
  39. /// Returns the current number of work items in the queue
  40. /// </summary>
  41. public int Count
  42. {
  43. get
  44. {
  45. return _workItems.Count;
  46. }
  47. }
  48. /// <summary>
  49. /// Returns the current number of waiters
  50. /// </summary>
  51. public int WaitersCount
  52. {
  53. get
  54. {
  55. return _waitersCount;
  56. }
  57. }
  58. #endregion
  59. #region Public methods
  60. /// <summary>
  61. /// Enqueue a work item to the queue.
  62. /// </summary>
  63. public bool EnqueueWorkItem(WorkItem workItem)
  64. {
  65. // A work item cannot be null, since null is used in the
  66. // WaitForWorkItem() method to indicate timeout or cancel
  67. if (workItem is null)
  68. {
  69. throw new ArgumentNullException("workItem", "workItem cannot be null");
  70. }
  71. // First check if there is a waiter waiting for work item. During
  72. // the check, timed out waiters are ignored. If there is no
  73. // waiter then the work item is queued.
  74. lock (this)
  75. {
  76. ValidateNotDisposed();
  77. if (!_isWorkItemsQueueActive)
  78. return false;
  79. while (_waitersCount > 0)
  80. {
  81. // Dequeue a waiter.
  82. WaiterEntry waiterEntry = PopWaiter();
  83. // Signal the waiter. On success break the loop
  84. if (waiterEntry.Signal(workItem))
  85. return true;
  86. }
  87. // Enqueue the work item
  88. _workItems.Enqueue(workItem);
  89. }
  90. return true;
  91. }
  92. public void CloseThreadWaiter()
  93. {
  94. if(_waiterEntry is not null)
  95. {
  96. _waiterEntry.Close();
  97. _waiterEntry = null;
  98. }
  99. }
  100. /// <summary>
  101. /// Waits for a work item or exits on timeout or cancel
  102. /// </summary>
  103. /// <param name="millisecondsTimeout">Timeout in milliseconds</param>
  104. /// <param name="cancelEvent">Cancel wait handle</param>
  105. /// <returns>Returns true if the resource was granted</returns>
  106. public WorkItem DequeueWorkItem( int millisecondsTimeout, WaitHandle cancelEvent)
  107. {
  108. // This method cause the caller to wait for a work item.
  109. // If there is at least one waiting work item then the
  110. // method returns immidiately with it.
  111. //
  112. // If there are no waiting work items then the caller
  113. // is queued between other waiters for a work item to arrive.
  114. //
  115. // If a work item didn't come within millisecondsTimeout or
  116. // the user canceled the wait by signaling the cancelEvent
  117. // then the method returns null to indicate that the caller
  118. // didn't get a work item.
  119. WaiterEntry waiterEntry;
  120. lock (this)
  121. {
  122. ValidateNotDisposed();
  123. // If there are waiting work items then take one and return.
  124. if (_workItems.Count > 0)
  125. return _workItems.Dequeue();
  126. // No waiting work items ...
  127. // Get the waiter entry for the waiters queue
  128. waiterEntry = GetThreadWaiterEntry();
  129. // Put the waiter with the other waiters
  130. PushWaiter(waiterEntry);
  131. }
  132. // Prepare array of wait handle for the WaitHandle.WaitAny()
  133. WaitHandle[] waitHandles = new WaitHandle[] { waiterEntry.WaitHandle, cancelEvent };
  134. // Wait for an available resource, cancel event, or timeout.
  135. // During the wait we are supposes to exit the synchronization
  136. // domain. (Placing true as the third argument of the WaitAny())
  137. // It just doesn't work, I don't know why, so I have two lock(this)
  138. // statments instead of one.
  139. int index = STPEventWaitHandle.WaitAny( waitHandles, millisecondsTimeout, true);
  140. lock (this)
  141. {
  142. // On timeout update the waiterEntry that it is timed out
  143. if (index != 0)
  144. {
  145. // The Timeout() fails if the waiter has already been signaled
  146. // On timeout remove the waiter from the queue.
  147. // Note that the complexity is O(1).
  148. if (waiterEntry.Timeout())
  149. {
  150. RemoveWaiter(waiterEntry, false);
  151. return null;
  152. }
  153. }
  154. // On success return the work item
  155. WorkItem workItem = waiterEntry.WorkItem;
  156. workItem ??= _workItems.Dequeue();
  157. return workItem;
  158. }
  159. }
  160. /// <summary>
  161. /// Cleanup the work items queue, hence no more work
  162. /// items are allowed to be queue
  163. /// </summary>
  164. private void Cleanup()
  165. {
  166. lock (this)
  167. {
  168. // Deactivate only once
  169. if (!_isWorkItemsQueueActive)
  170. {
  171. return;
  172. }
  173. // Don't queue more work items
  174. _isWorkItemsQueueActive = false;
  175. foreach (WorkItem workItem in _workItems)
  176. {
  177. workItem.DisposeOfState();
  178. }
  179. // Clear the work items that are already queued
  180. _workItems.Clear();
  181. // Note:
  182. // I don't iterate over the queue and dispose of work items's states,
  183. // since if a work item has a state object that is still in use in the
  184. // application then I must not dispose it.
  185. // Tell the waiters that they were timed out.
  186. // It won't signal them to exit, but to ignore their
  187. // next work item.
  188. while (_waitersCount > 0)
  189. {
  190. WaiterEntry waiterEntry = PopWaiter();
  191. waiterEntry.Timeout();
  192. }
  193. }
  194. }
  195. public object[] GetStates()
  196. {
  197. lock (this)
  198. {
  199. object[] states = new object[_workItems.Count];
  200. int i = 0;
  201. foreach (WorkItem workItem in _workItems)
  202. {
  203. states[i] = workItem.GetWorkItemResult().State;
  204. ++i;
  205. }
  206. return states;
  207. }
  208. }
  209. #endregion
  210. #region Private methods
  211. /// <summary>
  212. /// Returns the WaiterEntry of the current thread
  213. /// </summary>
  214. /// <returns></returns>
  215. /// In order to avoid creation and destuction of WaiterEntry
  216. /// objects each thread has its own WaiterEntry object.
  217. private static WaiterEntry GetThreadWaiterEntry()
  218. {
  219. if (_waiterEntry is null)
  220. {
  221. _waiterEntry = new WaiterEntry();
  222. }
  223. else
  224. _waiterEntry.Reset();
  225. return _waiterEntry;
  226. }
  227. #region Waiters stack methods
  228. /// <summary>
  229. /// Push a new waiter into the waiter's stack
  230. /// </summary>
  231. /// <param name="newWaiterEntry">A waiter to put in the stack</param>
  232. public void PushWaiter(WaiterEntry newWaiterEntry)
  233. {
  234. // Remove the waiter if it is already in the stack and
  235. // update waiter's count as needed
  236. RemoveWaiter(newWaiterEntry, false);
  237. // If the stack is empty then newWaiterEntry is the new head of the stack
  238. if (_headWaiterEntry._nextWaiterEntry is null)
  239. {
  240. _headWaiterEntry._nextWaiterEntry = newWaiterEntry;
  241. newWaiterEntry._prevWaiterEntry = _headWaiterEntry;
  242. }
  243. // If the stack is not empty then put newWaiterEntry as the new head
  244. // of the stack.
  245. else
  246. {
  247. // Save the old first waiter entry
  248. WaiterEntry oldFirstWaiterEntry = _headWaiterEntry._nextWaiterEntry;
  249. // Update the links
  250. _headWaiterEntry._nextWaiterEntry = newWaiterEntry;
  251. newWaiterEntry._nextWaiterEntry = oldFirstWaiterEntry;
  252. newWaiterEntry._prevWaiterEntry = _headWaiterEntry;
  253. oldFirstWaiterEntry._prevWaiterEntry = newWaiterEntry;
  254. }
  255. // Increment the number of waiters
  256. ++_waitersCount;
  257. }
  258. /// <summary>
  259. /// Pop a waiter from the waiter's stack
  260. /// </summary>
  261. /// <returns>Returns the first waiter in the stack</returns>
  262. private WaiterEntry PopWaiter()
  263. {
  264. // Store the current stack head
  265. WaiterEntry oldFirstWaiterEntry = _headWaiterEntry._nextWaiterEntry;
  266. // Store the new stack head
  267. WaiterEntry newHeadWaiterEntry = oldFirstWaiterEntry._nextWaiterEntry;
  268. // Update the old stack head list links and decrement the number
  269. // waiters.
  270. RemoveWaiter(oldFirstWaiterEntry, true);
  271. // Update the new stack head
  272. _headWaiterEntry._nextWaiterEntry = newHeadWaiterEntry;
  273. if (newHeadWaiterEntry is not null)
  274. {
  275. newHeadWaiterEntry._prevWaiterEntry = _headWaiterEntry;
  276. }
  277. // Return the old stack head
  278. return oldFirstWaiterEntry;
  279. }
  280. /// <summary>
  281. /// Remove a waiter from the stack
  282. /// </summary>
  283. /// <param name="waiterEntry">A waiter entry to remove</param>
  284. /// <param name="popDecrement">If true the waiter count is always decremented</param>
  285. private void RemoveWaiter(WaiterEntry waiterEntry, bool popDecrement)
  286. {
  287. // Store the prev entry in the list
  288. WaiterEntry prevWaiterEntry = waiterEntry._prevWaiterEntry;
  289. waiterEntry._prevWaiterEntry = null;
  290. // Store the next entry in the list
  291. WaiterEntry nextWaiterEntry = waiterEntry._nextWaiterEntry;
  292. waiterEntry._nextWaiterEntry = null;
  293. // popDecrement indicate if we need to decrement the waiters count.
  294. // If we got here from PopWaiter then we must decrement.
  295. // If we got here from PushWaiter then we decrement only if
  296. // the waiter was already in the stack.
  297. // If the waiter entry had a prev link then update it.
  298. // It also means that the waiter is already in the list and we
  299. // need to decrement the waiters count.
  300. if (prevWaiterEntry is not null)
  301. {
  302. prevWaiterEntry._nextWaiterEntry = nextWaiterEntry;
  303. popDecrement = true;
  304. }
  305. // If the waiter entry had a next link then update it.
  306. // It also means that the waiter is already in the list and we
  307. // need to decrement the waiters count.
  308. if (nextWaiterEntry is not null)
  309. {
  310. nextWaiterEntry._prevWaiterEntry = prevWaiterEntry;
  311. popDecrement = true;
  312. }
  313. // Decrement the waiters count if needed
  314. if (popDecrement)
  315. --_waitersCount;
  316. }
  317. #endregion
  318. #endregion
  319. #region WaiterEntry class
  320. // A waiter entry in the _waiters queue.
  321. public sealed class WaiterEntry : IDisposable
  322. {
  323. #region Member variables
  324. /// <summary>
  325. /// Event to signal the waiter that it got the work item.
  326. /// </summary>
  327. private AutoResetEvent _waitHandle = new(false);
  328. /// <summary>
  329. /// Flag to know if this waiter already quited from the queue
  330. /// because of a timeout.
  331. /// </summary>
  332. private bool _isTimedout = false;
  333. /// <summary>
  334. /// Flag to know if the waiter was signaled and got a work item.
  335. /// </summary>
  336. private bool _isSignaled = false;
  337. /// <summary>
  338. /// A work item that passed directly to the waiter withou going
  339. /// through the queue
  340. /// </summary>
  341. private WorkItem _workItem = null;
  342. private bool _isDisposed = false;
  343. // Linked list members
  344. internal WaiterEntry _nextWaiterEntry = null;
  345. internal WaiterEntry _prevWaiterEntry = null;
  346. #endregion
  347. #region Construction
  348. public WaiterEntry()
  349. {
  350. }
  351. #endregion
  352. #region Public methods
  353. public WaitHandle WaitHandle
  354. {
  355. get { return _waitHandle; }
  356. }
  357. public WorkItem WorkItem
  358. {
  359. get
  360. {
  361. return _workItem;
  362. }
  363. }
  364. /// <summary>
  365. /// Signal the waiter that it got a work item.
  366. /// </summary>
  367. /// <returns>Return true on success</returns>
  368. /// The method fails if Timeout() preceded its call
  369. public bool Signal(WorkItem workItem)
  370. {
  371. lock (this)
  372. {
  373. if (_isTimedout)
  374. return false;
  375. _workItem = workItem;
  376. _isSignaled = true;
  377. _waitHandle.Set();
  378. return true;
  379. }
  380. }
  381. /// <summary>
  382. /// Mark the wait entry that it has been timed out
  383. /// </summary>
  384. /// <returns>Return true on success</returns>
  385. /// The method fails if Signal() preceded its call
  386. public bool Timeout()
  387. {
  388. lock (this)
  389. {
  390. // Time out can happen only if the waiter wasn't marked as
  391. // signaled
  392. if (_isSignaled)
  393. return false;
  394. // We don't remove the waiter from the queue, the DequeueWorkItem
  395. // method skips _waiters that were timed out.
  396. _isTimedout = true;
  397. return true;
  398. }
  399. }
  400. /// <summary>
  401. /// Reset the wait entry so it can be used again
  402. /// </summary>
  403. public void Reset()
  404. {
  405. _workItem = null;
  406. _isTimedout = false;
  407. _isSignaled = false;
  408. _waitHandle.Reset();
  409. }
  410. /// <summary>
  411. /// Free resources
  412. /// </summary>
  413. public void Close()
  414. {
  415. _workItem = null;
  416. if (_waitHandle is not null)
  417. {
  418. _waitHandle.Close();
  419. _waitHandle = null;
  420. }
  421. }
  422. #endregion
  423. #region IDisposable Members
  424. public void Dispose()
  425. {
  426. lock (this)
  427. {
  428. if (!_isDisposed)
  429. {
  430. Close();
  431. _isDisposed = true;
  432. }
  433. }
  434. }
  435. #endregion
  436. }
  437. #endregion
  438. #region IDisposable Members
  439. public void Dispose()
  440. {
  441. Dispose(true);
  442. GC.SuppressFinalize(this);
  443. }
  444. protected virtual void Dispose(bool disposing)
  445. {
  446. if (!_isDisposed)
  447. {
  448. _isDisposed = true;
  449. Cleanup();
  450. _headWaiterEntry.Close();
  451. }
  452. }
  453. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  454. private void ValidateNotDisposed()
  455. {
  456. if (_isDisposed)
  457. {
  458. throw new ObjectDisposedException(GetType().ToString(), "The SmartThreadPool has been shutdown");
  459. }
  460. }
  461. #endregion
  462. }
  463. #endregion
  464. }