PageRenderTime 65ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/code-samples/threading/README.md

https://bitbucket.org/BanksySan/banksysan.workshops.advancedcsharp
Markdown | 1255 lines | 1016 code | 239 blank | 0 comment | 0 complexity | 114fb45557367d300ed37508e1768528 MD5 | raw file
  1. # Threading
  2. ## Defining a thread
  3. This isn't an easy thing to do, as I discovered when I did a presentation on async JavaScript. The term _thread_ is used very liberally for similar things. It seems best to define it on a per-use basis.
  4. 1. A process will consist of one or more threads.
  5. 1. A thread is the basic unit through which the operating system can assign processor time.
  6. 1. A processor can execute one thread at any given moment.
  7. 1. A thread runs to completion and is then disposed.
  8. If we had a single processor and no concept of threads then each process would block all the other operations until it had finished.
  9. Threads are an answer to this problem. The OS will share time on the processor between the treads requesting it. This means that even if a thread does have an infinite loop the OS will still swap it out so the other threads keep working.
  10. Nothing is ever free though, the management system required to perform threading used resources itself and the algorithm used to decide what thread should get precious processor time isn't perfect. This is the reason that .NET has friendlier classes available to abstract away some of the ugliness.
  11. ## Creating a Thread
  12. > If you create threads directly, then you're code is already obsolete.
  13. Whilst this is true for almost all new development, it's still useful to understand them is only to understand what the problems with them are.
  14. > The managed threads created in C# map one-to-one with Windows threads. Originally there was an idea that a managed thread would be a thing, it didn't happen though. This is why you have a thread ID and a managed thread ID.
  15. Creating and running a thread is trivial. All we need to do is create a method that's assignable to a `ThreadStart` delegate and pass it to the constructor of the `Thread` type.
  16. ``` csharp
  17. using static System.Console;
  18. using System.Threading;
  19. static class Program
  20. {
  21. private static void Main()
  22. {
  23. WriteLine($"Main managed thread ID: {Thread.CurrentThread.ManagedThreadId}.");
  24. var thread = new Thread(Counter);
  25. WriteLine($"Created thread. Managed thread ID: {thread.ManagedThreadId}.");
  26. thread.Start();
  27. WriteLine($"Thread started.");
  28. }
  29. private static void Counter()
  30. {
  31. for (var i = 0; i < 10; i++)
  32. {
  33. WriteLine($"{i}: Managed thread ID: {Thread.CurrentThread.ManagedThreadId}.");
  34. Thread.Sleep(100);
  35. }
  36. }
  37. }
  38. ```
  39. Compile and run this code.
  40. Main managed thread ID: 1.
  41. Created thread. Managed thread ID: 3.
  42. Thread started.
  43. 0: Managed thread ID: 3.
  44. 1: Managed thread ID: 3.
  45. 2: Managed thread ID: 3.
  46. 3: Managed thread ID: 3.
  47. 4: Managed thread ID: 3.
  48. 5: Managed thread ID: 3.
  49. 6: Managed thread ID: 3.
  50. 7: Managed thread ID: 3.
  51. 8: Managed thread ID: 3.
  52. 9: Managed thread ID: 3.
  53. This is no different than what we'd see if you hadn't used threads at all, in order to see threads interplaying we need another one:
  54. ``` csharp
  55. using static System.Console;
  56. using System.Threading;
  57. static class Program
  58. {
  59. private static void Main()
  60. {
  61. WriteLine($"Main managed thread ID: {Thread.CurrentThread.ManagedThreadId}.");
  62. var thread1 = new Thread(Counter);
  63. WriteLine($"Created thread 1. Managed thread ID: {thread1.ManagedThreadId}.");
  64. thread1.Start();
  65. WriteLine($"Thread 1 started.");
  66. var thread2 = new Thread(Counter);
  67. WriteLine($"Created thread 2. Managed thread ID: {thread2.ManagedThreadId}.");
  68. thread2.Start();
  69. WriteLine($"Thread 2 started.");
  70. }
  71. private static void Counter()
  72. {
  73. for (var i = 0; i < 10; i++)
  74. {
  75. WriteLine($"{i}: Managed thread ID: {Thread.CurrentThread.ManagedThreadId}.")Thread.Sleep(100);
  76. }
  77. }
  78. }
  79. ```
  80. Now we can see the threads operating in parallel:
  81. Main managed thread ID: 1.
  82. Created thread 1. Managed thread ID: 3.
  83. Thread 1 started.
  84. Created thread 2. Managed thread ID: 4.
  85. 0: Managed thread ID: 3.
  86. Thread 2 started.
  87. 0: Managed thread ID: 4.
  88. 1: Managed thread ID: 4.
  89. 1: Managed thread ID: 3.
  90. 2: Managed thread ID: 3.
  91. 2: Managed thread ID: 4.
  92. 3: Managed thread ID: 4.
  93. 3: Managed thread ID: 3.
  94. 4: Managed thread ID: 4.
  95. 4: Managed thread ID: 3.
  96. 5: Managed thread ID: 4.
  97. 5: Managed thread ID: 3.
  98. 6: Managed thread ID: 4.
  99. 6: Managed thread ID: 3.
  100. 7: Managed thread ID: 4.
  101. 7: Managed thread ID: 3.
  102. 8: Managed thread ID: 4.
  103. 8: Managed thread ID: 3.
  104. 9: Managed thread ID: 4.
  105. 9: Managed thread ID: 3.
  106. In this example the threads happened to run in perfect parallel, however the OS scheduler is free to manage them however it sees fit. It could even run one to completion and then the next if it deemed that more optimal for the wider system.
  107. ## Foreground v Background Treads
  108. You probably noticed in the previous examples that the application didn't exit until all the threads had finished. This might be unexpected behaviour to you, it was to me initially. The reason we get this behavior is because the CLR has a concept of threads being either _background_ or _foreground_. The threads are identical with one behavioral difference. An application will terminate when all foreground threads have returned and will terminate any remaining background threads.
  109. ``` csharp
  110. using System.Threading;
  111. using static System.Console;
  112. static class BackgroundAndForegroundThreads
  113. {
  114. private static void Main()
  115. {
  116. var backgroundThread = new Thread(Counter)
  117. {
  118. IsBackground = true
  119. };
  120. var foregroundThread = new Thread(Counter);
  121. WriteLine($"Starting both threads.");
  122. backgroundThread.Start();
  123. foregroundThread.Start();
  124. Thread.Sleep(20);
  125. WriteLine("We'll kill the foreground thread and the application will exit...");
  126. foregroundThread.Abort();
  127. }
  128. private static void Counter()
  129. {
  130. while (true)
  131. {
  132. WriteLine($"Thread ID: {Thread.CurrentThread.ManagedThreadId}, Is Background: {Thread.CurrentThread.IsBackground}.");
  133. Thread.Sleep(5);
  134. }
  135. }
  136. }
  137. ```
  138. This will produce something like the following:
  139. Starting both threads.
  140. Thread ID: 3, Is Background: True.
  141. Thread ID: 4, Is Background: False.
  142. Thread ID: 3, Is Background: True.
  143. Thread ID: 4, Is Background: False.
  144. Thread ID: 3, Is Background: True.
  145. Thread ID: 4, Is Background: False.
  146. Thread ID: 4, Is Background: False.
  147. Thread ID: 3, Is Background: True.
  148. We'll kill the foreground thread and the application will exit...
  149. Thread ID: 3, Is Background: True.
  150. Thread ID: 3, Is Background: True.
  151. Try killing the background thread instead, you'll see that the application never finishes because `Counter()` never returns.
  152. ## Synchronising Threads
  153. Almost always, at some point, we need to synchronise threads in some way. Normally synchronisation is so they can manipulate some shared data or prevent a race condition error. We have a number of requirements for synchronisation which we'll go through next.
  154. ### Waiting for a Thread to Complete
  155. The simplest synchronisation is just waiting for completion of some other thread. This is done my joining the other thread back.
  156. ``` csharp
  157. using System.Threading;
  158. using static System.Console;
  159. static class ThreadJoining
  160. {
  161. private static void Main()
  162. {
  163. var thread = new Thread(Counter) { IsBackground = true };
  164. thread.Start();
  165. for(var i = 0; i < 5; i++)
  166. {
  167. WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Working on iteration {i}.");
  168. Thread.Sleep(20);
  169. }
  170. WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Finished! waiting...");
  171. thread.Join();
  172. WriteLine($"{Thread.CurrentThread.ManagedThreadId}: All done.");
  173. }
  174. private static void Counter()
  175. {
  176. for (var i = 0; i < 10; i++)
  177. {
  178. WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Working on iteration {i}.");
  179. Thread.Sleep(20);
  180. }
  181. WriteLine($"{Thread.CurrentThread.ManagedThreadId}: Finished!");
  182. }
  183. }
  184. ```
  185. When you run this you'll see that the execution of the `Main()` pauses at the `Join()` method.
  186. 1: Working on iteration 0.
  187. 3: Working on iteration 0.
  188. 1: Working on iteration 1.
  189. 3: Working on iteration 1.
  190. 1: Working on iteration 2.
  191. 3: Working on iteration 2.
  192. 1: Working on iteration 3.
  193. 3: Working on iteration 3.
  194. 1: Working on iteration 4.
  195. 3: Working on iteration 4.
  196. 1: Finished! waiting...
  197. 3: Working on iteration 5.
  198. 3: Working on iteration 6.
  199. 3: Working on iteration 7.
  200. 3: Working on iteration 8.
  201. 3: Working on iteration 9.
  202. 3: Finished!
  203. 1: All done.
  204. ## Exceptions
  205. It's important to note that though background threads don't contribute to an application's lifetime when things are going well; they will still cause the application to terminate when an unhandled exception occurs.
  206. ``` csharp
  207. using System;
  208. using System.Threading;
  209. using static System.Console;
  210. internal static class ExceptionHandling
  211. {
  212. private static void Main()
  213. {
  214. var thread = new Thread(ThrowsDummyException);
  215. thread.Start();
  216. while (true)
  217. {
  218. WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId} is working hard...");
  219. Thread.Sleep(10);
  220. }
  221. }
  222. private static void ThrowsDummyException()
  223. {
  224. var timeSpan = TimeSpan.FromMilliseconds(100);
  225. WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}: Waiting {timeSpan.TotalSeconds} seconds to throw.");
  226. Thread.Sleep(timeSpan);
  227. throw new Exception("BOOM!");
  228. }
  229. }
  230. ```
  231. You might be tempted to solve this problem by adding a `try-catch` in the `Main` method like this:
  232. ``` csharp
  233. using System;
  234. using System.Threading;
  235. using static System.Console;
  236. internal static class ExceptionHandling
  237. {
  238. private static void Main()
  239. {
  240. try
  241. {
  242. var thread = new Thread(ThrowsDummyException);
  243. thread.Start();
  244. while (true)
  245. {
  246. WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId} is working hard...");
  247. Thread.Sleep(10);
  248. }
  249. }
  250. catch (System.Exception)
  251. {
  252. System.Console.WriteLine($"I caught the exception.");
  253. }
  254. }
  255. private static void ThrowsDummyException()
  256. {
  257. var timeSpan = TimeSpan.FromMilliseconds(100);
  258. WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}: Waiting {timeSpan.TotalSeconds} seconds to throw.");
  259. Thread.Sleep(timeSpan);
  260. throw new Exception("BOOM!");
  261. }
  262. }
  263. ```
  264. This doesn't actually catch the exception though, it can't because `thread` is running asynchronously to the `Main` method. Exception handling needs to be done in the thread that's throwing the exception. In the example above, a `try-catch` would be used in the `ThrowsDummyException` method.
  265. ## Aborting a Thread
  266. Once a thread is started another thread can't just stop it. A thread is unloaded when it either finishes, the app domain it's in is unloaded or an exception is thrown in the thread.
  267. This third option is common and the CLR has a special exception class for doing this. Calling `Abort` on a thread tells the CLR to raise a `ThreadAbortedException` in that thread.
  268. 1. The `ThreadAbortedException` is sealed.
  269. 1. The `ThreadAbortedException` has no public constructor.
  270. 1. The CLR will not terminate an application if a `ThreadAbortedException` is raised.
  271. 1. You can catch a `ThreadAbortedException` **however**, after handling it the `ThreadAbortedException` will continue to bubble up **unless** `ResetAbort` is called.
  272. The last point here means that whilst you can try to abort a thread, you can't guarantee when or if is will actually happen.
  273. ``` csharp
  274. using static System.Console;
  275. using System.Threading;
  276. static class AbortingAThread
  277. {
  278. private static void Main()
  279. {
  280. var thread = new Thread(Counter);
  281. thread.Start();
  282. Thread.Sleep(50);
  283. thread.Abort("I'm aborting you.");
  284. }
  285. private static void Counter()
  286. {
  287. try
  288. {
  289. var i = 0;
  290. while(true)
  291. {
  292. WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
  293. Thread.Sleep(10);
  294. }
  295. }
  296. catch(ThreadAbortException e)
  297. {
  298. WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: I caught the `ThreadAbortedException` exception: {e.Message};");
  299. WriteLine($"The object data is: {e.ExceptionState}");
  300. }
  301. }
  302. }
  303. ```
  304. Whilst we can't catch the exception to stop it from being re-thrown, we can cancel it completely by calling `ResetAbort()`.
  305. ``` csharp
  306. using static System.Console;
  307. using System.Threading;
  308. static class ResettingAnAbortingThread
  309. {
  310. private static void Main()
  311. {
  312. var thread = new Thread(Counter);
  313. thread.Start();
  314. Thread.Sleep(50);
  315. thread.Abort("I'm aborting you.");
  316. }
  317. private static void Counter()
  318. {
  319. try
  320. {
  321. var i = 0;
  322. while(true)
  323. {
  324. WriteLine($"{Thread.CurrentThread.ManagedThreadId}: {i}");
  325. Thread.Sleep(10);
  326. }
  327. }
  328. catch(ThreadAbortException e)
  329. {
  330. WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: I caught the `ThreadAbortedException` exception: {e.Message};");
  331. WriteLine($"The object data is: {e.ExceptionState}");
  332. Thread.ResetAbort();
  333. }
  334. WriteLine("Wee! I wasn't aborted!");
  335. }
  336. }
  337. ```
  338. ## Parameterized Threads
  339. The `Thread` constructor is overloaded to accept either a `ThreadStart` delegate or a `ParameterizedThreadStart`. The `ParameterizedThreadStart` delegate accepts a single `object` as an argument.
  340. ``` csharp
  341. using System.Threading;
  342. using static System.Console;
  343. static class ParameterizedThreads
  344. {
  345. private static void Main()
  346. {
  347. var thread = new Thread(CountInterval);
  348. thread.Start("Hello World!");
  349. thread.Join();
  350. }
  351. private static void CountInterval(object message)
  352. {
  353. WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: Message: {(string) message}.");
  354. }
  355. }
  356. ```
  357. ## Thread Priority Levels
  358. Windows uses a threads priority (relative to other threads competing for processor time) to decide what thread gets processor time. It will share the processor time between the highest priority threads that are ready at the exclusion of all lower priority threads. This means that you must exercise care in setting a thread's priority - in fact, there are some priorities that can only be set by a user with kernel-mode permissions.
  359. > The absolute priority is a number from 0 to 31 (inclusive). 0 is the lowest priority and 31 is the highest. 0 however is reserved for the sole use of the _zero page thread_ that is created by the OS.
  360. There are two values that affect the calculation of the absolute priority of a thread:
  361. 1. The thread's `ThreadPriority` value.
  362. 1. The process's priority class.
  363. They are calculated as:
  364. ![Thread priorities](https://i.imgur.com/15Z4Pr9.png)
  365. What this means is that you could create a high priority, long running thread which would block other threads from executing. This is called _thread starvation_.
  366. We can see this happening with the following code:
  367. ``` csharp
  368. using System;
  369. using System.Collections.Generic;
  370. using System.Linq;
  371. using System.Threading;
  372. using static System.Console;
  373. static class ThreadStarvation
  374. {
  375. private static void Main()
  376. {
  377. var processorCount = Environment.ProcessorCount;
  378. WriteLine($"There are {processorCount} processors.");
  379. WriteLine($"There can be {processorCount} threads running at the same time.");
  380. var aboveNormalPriorityThreads = new HashSet<Thread>();
  381. for (var i = 0; i < processorCount; i ++)
  382. {
  383. aboveNormalPriorityThreads.Add(new Thread(Counter) { Priority = ThreadPriority.AboveNormal });
  384. }
  385. var normalPriorityThread = new Thread(Counter);
  386. normalPriorityThread.Start();
  387. WriteLine($"Letting the normal priority thread run for a bit");
  388. Thread.Sleep(1000);
  389. foreach(var thread in aboveNormalPriorityThreads)
  390. {
  391. WriteLine($"Starting {thread.ManagedThreadId} with priority {thread.Priority}.");
  392. thread.Start();
  393. }
  394. }
  395. private static void Counter()
  396. {
  397. var currentThread = Thread.CurrentThread;
  398. var priority = currentThread.Priority;
  399. var id = currentThread.ManagedThreadId;
  400. for (var i = 0; i < 5; i++)
  401. {
  402. var then = DateTime.Now;
  403. var number = then.GetHashCode();
  404. while (DateTime.Now < then.AddSeconds(1))
  405. {
  406. number ^= number.GetHashCode();
  407. }
  408. WriteLine($"Thread ID: {id}, Priority: {priority, -15}{i, 10}{number}");
  409. }
  410. WriteLine($"Thread ID: {id}, Priority: {priority, -15}COMPLETED.");
  411. }
  412. }
  413. ```
  414. Here we create a number of high priority threads, enough to saturate the processors we have. We also create low priority thread. Even though the low priority thread is started before the higher priority ones it's starved of resources until one of the higher priority threads complete.
  415. There are 4 processors.
  416. There can be 4 threads running at the same time.
  417. Letting the normal priority thread run for a bit
  418. Starting 3 with priority AboveNormal.
  419. Thread ID: 7, Priority: Normal 00
  420. Starting 4 with priority AboveNormal.
  421. Starting 5 with priority AboveNormal.
  422. Starting 6 with priority AboveNormal.
  423. Thread ID: 4, Priority: AboveNormal 00
  424. Thread ID: 3, Priority: AboveNormal 00
  425. Thread ID: 5, Priority: AboveNormal 00
  426. Thread ID: 6, Priority: AboveNormal 00
  427. Thread ID: 4, Priority: AboveNormal 10
  428. Thread ID: 5, Priority: AboveNormal 10
  429. Thread ID: 3, Priority: AboveNormal 10
  430. Thread ID: 6, Priority: AboveNormal 10
  431. Thread ID: 3, Priority: AboveNormal 20
  432. Thread ID: 4, Priority: AboveNormal 20
  433. Thread ID: 5, Priority: AboveNormal 20
  434. Thread ID: 6, Priority: AboveNormal 20
  435. Thread ID: 3, Priority: AboveNormal 30
  436. Thread ID: 5, Priority: AboveNormal 30
  437. Thread ID: 4, Priority: AboveNormal 30
  438. Thread ID: 6, Priority: AboveNormal 30
  439. Thread ID: 3, Priority: AboveNormal 40
  440. Thread ID: 3, Priority: AboveNormal COMPLETED.
  441. Thread ID: 5, Priority: AboveNormal 40
  442. Thread ID: 5, Priority: AboveNormal COMPLETED.
  443. Thread ID: 7, Priority: Normal 10
  444. Thread ID: 4, Priority: AboveNormal 40
  445. Thread ID: 4, Priority: AboveNormal COMPLETED.
  446. Thread ID: 6, Priority: AboveNormal 40
  447. Thread ID: 6, Priority: AboveNormal COMPLETED.
  448. Thread ID: 7, Priority: Normal 20
  449. Thread ID: 7, Priority: Normal 30
  450. Thread ID: 7, Priority: Normal 40
  451. Thread ID: 7, Priority: Normal COMPLETED.
  452. Notice as well that the lower priority thread is actually stopped mid-execution to make room for the higher priority threads. When you run this you probably notice that the performance of the system degrades for the ten-ish seconds it's running for.
  453. ## The Cost of Threads
  454. Aside from the problems demonstrated above and the need for you as the developer to optimise for an unknown number of CPUs you will also have to conciser the other substantial overheads associated with creating and managing threads.
  455. ### The Upfront Cost
  456. Each thread created has is initialized with some data structures. A kernel object, a thread environment block, user stack and kernel stack and DLL thread-attach and thread detach notifications.
  457. The kernel object itself it used by the OS kernel to reference the thread.
  458. The stacks contain the processing information for user and kernel mode. Kernel mode and user mode can't pass by reference, so any state that needs to be passed between them needs to be copied.
  459. The DLL thread attach and detach list the `DllMain` method for every unmanaged DLL loaded. With some exceptions, it will call this method when it loads and unloads with different flags (i.e. `DLL_THREAD_ATTACH` and `DLL_THREAD_DETACH`);
  460. ### The Ongoing Costs
  461. The hardware processor contains many optimisations, one of which is the cache. When a thread is processing the cache hugely speeds up data access for recently accessed data items. Swapping unrelated threads in and out of the processor makes the cache useless and slower then if it weren't there at all.
  462. In order to solve this problem, each thread stores it's cache in the kernel object and has to reload the cache in the processor each time it is granted processor time.
  463. ## Possible Solutions
  464. I use the term _solution_ rather flippantly here. There are many best practices that help us avoid the problems described above to some extent or another, but the have drawbacks as well. That being acknowledged, for most scenarios these are better options than the manual thread manipulation we've looked at so far.
  465. ### Thread Pools
  466. We can avoid the overhead of thread creation by reusing threads that have already been created. The `ThreadPool` class handles this for us.
  467. ``` csharp
  468. using System.Collections.Generic;
  469. using System.Collections.Concurrent;
  470. using System.Linq;
  471. using System.Threading;
  472. using static System.Console;
  473. static class ThreadPoolClass
  474. {
  475. private const int THREAD_COUNT = 100;
  476. private static readonly bool[] COMPLETION = new bool[THREAD_COUNT];
  477. private static readonly ConcurrentStack<int> USAGE = new ConcurrentStack<int>();
  478. private static void Main()
  479. {
  480. for (var i = 0; i < THREAD_COUNT; i++)
  481. {
  482. WriteLine($"Queueing {i}.");
  483. ThreadPool.QueueUserWorkItem(Counter, i);
  484. }
  485. while (!COMPLETION.All(x => x))
  486. {
  487. Thread.Sleep(0);
  488. }
  489. var threadUsages = new SortedDictionary<int, int>();
  490. foreach (var usage in USAGE)
  491. {
  492. if (threadUsages.ContainsKey(usage))
  493. {
  494. threadUsages[usage] = threadUsages[usage] + 1;
  495. }
  496. else
  497. {
  498. threadUsages.Add(usage, 1);
  499. }
  500. }
  501. WriteLine($"Thread reuse:");
  502. foreach (var usage in threadUsages)
  503. {
  504. WriteLine($"Thread {usage.Key} used {usage.Value} times.");
  505. }
  506. }
  507. private static void Counter(object state)
  508. {
  509. var index = (int)state;
  510. USAGE.Push(Thread.CurrentThread.ManagedThreadId);
  511. for (var i = 0; i < 10; i++)
  512. {
  513. WriteLine($"Thread ID: {Thread.CurrentThread.ManagedThreadId}: {i}");
  514. Thread.Sleep(1);
  515. }
  516. COMPLETION[index] = true;
  517. }
  518. }
  519. ```
  520. When you run this, you'll see something like the following at the end of the output:
  521. Thread reuse:
  522. Thread 3 used 22 times.
  523. Thread 4 used 22 times.
  524. Thread 5 used 23 times.
  525. Thread 6 used 22 times.
  526. Thread 7 used 11 times.
  527. This might seem like the silver bullet, however we lose a lot of benefits from the manual creation.
  528. 1. No way to chose a thread's priority.
  529. 1. No way to wait for a thread to finish.
  530. 1. The thread is in an unknown state. This means that there could be unexpected values in the TLS, maybe even secret stuff from whatever it was doing last.
  531. 1. Thread pool threads are always background threads.
  532. ## The Task Type
  533. The `Task` and `Task<T>` types address the problem of waiting until completion. If we change the `ThreadPool` example above to use tasks then we get exactly the same output, but we don't need the `while` loop and boolean array to wait for tasks to all finish. We can just use the `Task.WaitAll` method.
  534. Internally the task is using the `ThreadPool`, which is why our output is identical.
  535. ## Cancelling Threads
  536. Cancellation has two _modes_ that can be used for cancelling.
  537. 1. Marking to the subject thread that it's creator wants it to cancel.
  538. 1. The subject thread throwing a `OperationCancelledException` if cancellation has been requested.
  539. Cancelling threads is a cooperative pattern. We tell the thread that we want to cancel it, it is up to the executing code to then decide what action to take. The thread is under no obligation to cancel at all. A _well written_ thread will cancel if it is safe to do so. This isn't dissimilar to the disposal of objects, in that the object will perform any cleaning up actions it deems necessary before being collected.
  540. Both these methods are shown here:
  541. ``` csharp
  542. using System;
  543. using System.Threading;
  544. using static System.Threading.Thread;
  545. using static System.Console;
  546. internal static class ThreadCancellation
  547. {
  548. private static void Main()
  549. {
  550. var cancellationTokenSource = new CancellationTokenSource();
  551. var cancellationToken = cancellationTokenSource.Token;
  552. ThreadPool.QueueUserWorkItem(x => Counter(cancellationToken));
  553. ThreadPool.QueueUserWorkItem(x => CounterWithThrow(cancellationToken));
  554. Sleep(10);
  555. cancellationTokenSource.Cancel();
  556. Sleep(10);
  557. }
  558. private static void Counter(CancellationToken cancellationToken)
  559. {
  560. WriteLine($"Counter running on {CurrentThread.ManagedThreadId}.");
  561. while (!cancellationToken.IsCancellationRequested)
  562. {
  563. WriteLine($"Thread {CurrentThread.ManagedThreadId}: {DateTime.Now.Ticks}");
  564. Sleep(1);
  565. }
  566. WriteLine("Counter() was canceled");
  567. }
  568. private static void CounterWithThrow(CancellationToken cancellationToken)
  569. {
  570. WriteLine($"CounterWithThrow running on {CurrentThread.ManagedThreadId}.");
  571. try
  572. {
  573. while (true)
  574. {
  575. WriteLine($"Thread {CurrentThread.ManagedThreadId}: {DateTime.Now.Ticks}");
  576. cancellationToken.ThrowIfCancellationRequested();
  577. Sleep(1);
  578. }
  579. }
  580. catch (OperationCanceledException e)
  581. {
  582. WriteLine($"Thread {CurrentThread.ManagedThreadId}: Caught OperationCanceledException: {e.Message}");
  583. }
  584. }
  585. }
  586. ```
  587. Both `Counter` and `CancellationToken` accept a `CancellationToken` token (created by the same `CancellationTokenSource`) and both keep processing until `Cancel()` is called on the token's source; however `Counter()` just exits gracefully by querying the `IsCancellationRequest` property whereas `CounterWithThrow()` catches the exception that's thrown. If we didn't catch this exception then the whole application would fail.
  588. We can also catch the exception in the parent thread. If an exception is thrown then `Cancel()`.
  589. ## Registering a Callback
  590. We can also register callbacks to be invoked when a cancellation is called:
  591. ``` csharp
  592. using System;
  593. using System.Threading;
  594. using static System.Console;
  595. using static System.Threading.Thread;
  596. internal class RegisterCancellationCallback
  597. {
  598. private static void Main()
  599. {
  600. var cancellationTokenSource = new CancellationTokenSource();
  601. var cancellationToken = cancellationTokenSource.Token;
  602. ThreadPool.QueueUserWorkItem(x => Counter(cancellationToken));
  603. ThreadPool.QueueUserWorkItem(x => CounterWithThrow(cancellationToken));
  604. cancellationToken.Register(LogCanceled);
  605. Sleep(10);
  606. cancellationTokenSource.Cancel();
  607. Sleep(10);
  608. }
  609. private static void LogCanceled()
  610. {
  611. WriteLine($"Thread {CurrentThread.ManagedThreadId}: Registered callback invoked.");
  612. }
  613. private static void Counter(CancellationToken cancellationToken)
  614. {
  615. WriteLine($"Counter running on {CurrentThread.ManagedThreadId}.");
  616. while (!cancellationToken.IsCancellationRequested)
  617. {
  618. WriteLine($"Thread {CurrentThread.ManagedThreadId}: {DateTime.Now.Ticks}");
  619. Sleep(1);
  620. }
  621. WriteLine("Counter() was canceled");
  622. }
  623. private static void CounterWithThrow(CancellationToken cancellationToken)
  624. {
  625. try
  626. {
  627. WriteLine($"CounterWithThrow running on {CurrentThread.ManagedThreadId}.");
  628. while (true)
  629. {
  630. WriteLine($"Thread {CurrentThread.ManagedThreadId}: {DateTime.Now.Ticks}");
  631. cancellationToken.ThrowIfCancellationRequested();
  632. Sleep(1);
  633. }
  634. }
  635. catch (OperationCanceledException e)
  636. {
  637. WriteLine($"Thread {CurrentThread.ManagedThreadId}: Caught OperationCanceledException: {e.Message}");
  638. }
  639. }
  640. }
  641. ```
  642. ## Locking Primitives
  643. Let's assume we have many threads performing tasks, periodically they want to adjust some value to reflect their progress.
  644. ``` csharp
  645. using System;
  646. using System.Threading;
  647. internal static class AggregatingFromManyThreadsIncorrect
  648. {
  649. private static float _BALANCE;
  650. private static void Main()
  651. {
  652. var threadCount = 10;
  653. var threads = new Thread[threadCount];
  654. for (var i = 0; i < threadCount; i++)
  655. {
  656. var thread = new Thread(PerformTransactions);
  657. thread.Start();
  658. threads[i] = thread;
  659. }
  660. for (var i = 0; i < threadCount; i++)
  661. {
  662. threads[i].Join();
  663. }
  664. Console.WriteLine($"Balance = {_BALANCE}.");
  665. }
  666. private static void PerformTransactions(object state)
  667. {
  668. for (var i = 0; i < 10000; i++)
  669. {
  670. _BALANCE += 1;
  671. Thread.Sleep(0);
  672. _BALANCE -= 1;
  673. Thread.Sleep(0);
  674. }
  675. }
  676. }
  677. ```
  678. The above code adds and removed `1` from the balance. This should mean that the final balance should be zero. Run this a few times though and you'll see that the final value is regularly non-zero.
  679. The reason for this is that the `+=` operator isn't atomic. In fact the lines `+=` and `-=` are expanded into this:
  680. ```csharp
  681. private static void PerformTransactions(object state)
  682. {
  683. for (var i = 0; i < 10000; i++)
  684. {
  685. var b1 = _BALANCE;
  686. var result1 = b1 + 1;
  687. _BALANCE = result1;
  688. Thread.Sleep(0);
  689. var b2 = _BALANCE;
  690. var result2 = b2 - 1;
  691. _BALANCE = result2;
  692. Thread.Sleep(0);
  693. }
  694. }
  695. ```
  696. With many threads running it's quite probable that several threads will be in the code between assigning the current value of `_BALANCE` to the temporary variable and the code setting `_BALANCE` to the newly calculated result.
  697. The simplest was the make this thread safe is with the `lock` keyword. The `lock` is a keyword that tells the compiler to wrap the locked block with a `Monitor.Enter()` and `Monitor.Exit()`.
  698. Rewrite the code above with the `lock` keyword.
  699. ``` csharp
  700. using System;
  701. using System.Threading;
  702. internal static class LockKeyword
  703. {
  704. private static int _BALANCE;
  705. private static readonly object LOCK = new object();
  706. private static void Main()
  707. {
  708. var threadCount = 10;
  709. var threads = new Thread[threadCount];
  710. for (var i = 0; i < threadCount; i++)
  711. {
  712. var thread = new Thread(PerformTransactions);
  713. thread.Start();
  714. threads[i] = thread;
  715. }
  716. for (var i = 0; i < threadCount; i++) threads[i].Join();
  717. Console.WriteLine($"Balance = {_BALANCE}.");
  718. }
  719. private static void PerformTransactions(object state)
  720. {
  721. for (var i = 0; i < 10000; i++)
  722. {
  723. lock(LOCK)
  724. {
  725. _BALANCE += 1;
  726. }
  727. Thread.Sleep(0);
  728. lock(LOCK)
  729. {
  730. _BALANCE -= 1;
  731. }
  732. Thread.Sleep(0);
  733. }
  734. }
  735. }
  736. ```
  737. Running this again will prove that the result is now what we expect. The `lock` block keyword is actually a shortcut for calling the `Monitor.Enter()` and `Monitor.Exit()` methods in a `try-finally` pattern (similar to how the `using` statement works).
  738. We can see this in the IL:
  739. ``` il
  740. .method private hidebysig static void PerformTransactions(object state) cil managed
  741. {
  742. // Code size 109 (0x6d)
  743. .maxstack 2
  744. .locals init (int32 V_0,
  745. object V_1,
  746. bool V_2)
  747. IL_0000: ldc.i4.0
  748. IL_0001: stloc.0
  749. IL_0002: br.s IL_0064
  750. IL_0004: ldsfld object BanksySan.Workshops.AdvancedCSharp.ThreadingExamples.LockKeyword::LOCK
  751. IL_0009: stloc.1
  752. IL_000a: ldc.i4.0
  753. IL_000b: stloc.2
  754. .try
  755. {
  756. IL_000c: ldloc.1
  757. IL_000d: ldloca.s V_2
  758. IL_000f: call void [mscorlib]System.Threading.Monitor::Enter(object,
  759. bool&)
  760. IL_0014: ldsfld int32 BanksySan.Workshops.AdvancedCSharp.ThreadingExamples.LockKeyword::_BALANCE
  761. IL_0019: ldc.i4.1
  762. IL_001a: add
  763. IL_001b: stsfld int32 BanksySan.Workshops.AdvancedCSharp.ThreadingExamples.LockKeyword::_BALANCE
  764. IL_0020: leave.s IL_002c
  765. } // end .try
  766. finally
  767. {
  768. IL_0022: ldloc.2
  769. IL_0023: brfalse.s IL_002b
  770. IL_0025: ldloc.1
  771. IL_0026: call void [mscorlib]System.Threading.Monitor::Exit(object)
  772. IL_002b: endfinally
  773. } // end handler
  774. IL_002c: ldc.i4.0
  775. IL_002d: call void [mscorlib]System.Threading.Thread::Sleep(int32)
  776. IL_0032: ldsfld object BanksySan.Workshops.AdvancedCSharp.ThreadingExamples.LockKeyword::LOCK
  777. IL_0037: stloc.1
  778. IL_0038: ldc.i4.0
  779. IL_0039: stloc.2
  780. .try
  781. {
  782. IL_003a: ldloc.1
  783. IL_003b: ldloca.s V_2
  784. IL_003d: call void [mscorlib]System.Threading.Monitor::Enter(object,
  785. bool&)
  786. IL_0042: ldsfld int32 BanksySan.Workshops.AdvancedCSharp.ThreadingExamples.LockKeyword::_BALANCE
  787. IL_0047: ldc.i4.1
  788. IL_0048: sub
  789. IL_0049: stsfld int32 BanksySan.Workshops.AdvancedCSharp.ThreadingExamples.LockKeyword::_BALANCE
  790. IL_004e: leave.s IL_005a
  791. } // end .try
  792. finally
  793. {
  794. IL_0050: ldloc.2
  795. IL_0051: brfalse.s IL_0059
  796. IL_0053: ldloc.1
  797. IL_0054: call void [mscorlib]System.Threading.Monitor::Exit(object)
  798. IL_0059: endfinally
  799. } // end handler
  800. IL_005a: ldc.i4.0
  801. IL_005b: call void [mscorlib]System.Threading.Thread::Sleep(int32)
  802. IL_0060: ldloc.0
  803. IL_0061: ldc.i4.1
  804. IL_0062: add
  805. IL_0063: stloc.0
  806. IL_0064: ldloc.0
  807. IL_0065: ldc.i4 0x2710
  808. IL_006a: blt.s IL_0004
  809. IL_006c: ret
  810. } // end of method LockKeyword::PerformTransactions
  811. ```
  812. We can see the `try` and `finally` and the calls to `Monitor.Enter()` and `Monitor.Exit()` within them.
  813. ## Performance
  814. Locking a code block, by necessity, causes a bottleneck because only one thread can get past that point at a time. The performance hit, even for a very simple example like this a significant amount. To combat this you need to limit the amount of code in a synchronised context to a minimum, or none at all.
  815. In the example above the nature of the calculation means that we don't actually need to be updating the shared value constantly. Each thread can calculate it's total and just lock the shared value once at the end to update it.
  816. ``` csharp
  817. private static void PerformTransactions(object state)
  818. {
  819. var balance = 0;
  820. for (var i = 0; i < 10000; i++)
  821. {
  822. balance += 1;
  823. Thread.Sleep(0);
  824. balance-= 1;
  825. Thread.Sleep(0);
  826. }
  827. lock (LOCK)
  828. {
  829. _BALANCE += balance;
  830. }
  831. }
  832. ```
  833. Now the lock only occurs once per thread; a big improvement which is actually faster than the unsynchronised (and erroneous) example because theres no need for enforced atomic red/writes from the shared value.
  834. We can achieve this by using tasks.
  835. ``` csharp
  836. using System;
  837. using System.Threading;
  838. using System.Threading.Tasks;
  839. internal static class NoLockAtAll
  840. {
  841. private static void Main()
  842. {
  843. var threadCount = 10;
  844. var tasks = new Task<int>[threadCount];
  845. for (var i = 0; i < threadCount; i++) tasks[i] = new Task<int>(PerformTransactions);
  846. for (var i = 0; i < tasks.Length; i++) tasks[i].Start();
  847. var results = Task.WhenAll(tasks);
  848. var balance = 0;
  849. foreach (var result in results.Result) balance += result;
  850. Console.WriteLine($"Balance = {balance}.");
  851. }
  852. private static int PerformTransactions()
  853. {
  854. var balance = 0;
  855. for (var i = 0; i < 10000; i++)
  856. {
  857. balance += 1;
  858. Thread.Sleep(0);
  859. balance -= 1;
  860. Thread.Sleep(0);
  861. }
  862. return balance;
  863. }
  864. }
  865. ```
  866. By re-writing the `PerformTransactions()` method so it returns a value and doesn't have any side-affects (i.e. changing any state external to it) we know have a method that is guaranteed thread safe.
  867. > This method also always returns the same value when given the same arguments (in this case no arguments). When a method has all three of these properties we call it a _pure_ method. Pure methods are one of the fundamental building blocks of functional programming.
  868. ## Torn Reads
  869. The error we got in the initial multi-threaded balance calculation was due to the read, calculate and write not being atomic, however the read and write _independently_ are atomic. There isn't any way that a read can happen _whilst_ a write is happening. As per the ECMA specification:
  870. > I.12.6.5 Locks and threads
  871. > > Built-in atomic reads and writes. All reads and writes of certain properly aligned data types are guaranteed to occur atomically
  872. The CLI guarantees that reads and writes to the following data types are atomic:
  873. * `bool`
  874. * `char`
  875. * `byte`
  876. * `sbyte`
  877. * `short`
  878. * `ushort`
  879. * `int`
  880. * `uint`
  881. * `float`
  882. * Object pointer
  883. This doesn't include `double`, `decimal`, `long` or `ulong`. This is because these types are all larger than 32-bits.
  884. The following code has one thread writing to a `ulong` and another reading from it.
  885. ``` csharp
  886. using System;
  887. using System.Threading;
  888. internal static class TornReads
  889. {
  890. private const ulong NUMBER_1 = 0xFFFFFFFFFFFFFFFF;
  891. private const ulong NUMBER_2 = 0x0000000000000000;
  892. private static ulong _NUMBER = NUMBER_1;
  893. private static bool @continue = true;
  894. private static void Main()
  895. {
  896. var writerThread = new Thread(Writer);
  897. var readerThread = new Thread(Reader);
  898. writerThread.Start();
  899. readerThread.Start();
  900. readerThread.Join();
  901. writerThread.Abort();
  902. @continue = true;
  903. }
  904. private static void Reader()
  905. {
  906. for (var i = 0; i < 100; i++)
  907. {
  908. var number = _NUMBER;
  909. if (number != NUMBER_1 && number != NUMBER_2)
  910. Console.WriteLine($"{i,3}: Read: {number:X16} TornRead!");
  911. else
  912. Console.WriteLine($"{i,3}: Read: {number:X16}");
  913. }
  914. }
  915. private static void Writer()
  916. {
  917. while (@continue)
  918. {
  919. _NUMBER = NUMBER_2;
  920. _NUMBER = NUMBER_1;
  921. }
  922. }
  923. }
  924. ```
  925. A _good_ read would be either `0x0000000000000000` or `0xFFFFFFFFFFFFFFFF` (i.e. setting all bits to `0` or setting all bits to `1`). A _bad_ read would be when some of the bits have been changed, but not all.
  926. Compile this code, **targeting x32** and you will see that some values are `0x00000000FFFFFFFF` and some are `0xFFFFFFFF00000000`. This is called a _torn read_. We only see these two torn values because 32 bits are atomic, this it takes two atomic 32 bit writes to the full 62 bit value.
  927. Try targeting x64 now, you'll see that there aren't any torn reads at all. This is because a x64 CPU can read and write 64 bits atomically.
  928. > NB The atomic reading of the values larger than 32 bits is because of the CPU architecture. It's not guaranteed by the CLI.
  929. ## Interlocked operations
  930. Interlocking is the term used for performing read _and_ write operations atomically. C# has a static class `Interlocked` which has several methods to achieve this behaviour. For example, the `Increment` and `Decrement` methods perform the `+=` and `-=` operations we used in the `lock` examples.
  931. Torn reads could be fixed with the `lock` keyword again, but `Interlocked` provides us with a better option using the `Read` method.
  932. ## Volatile
  933. When compiling C# you have the option of optimising the code. Effectively, optimising rewrites the code you've written so that it runs faster. Production code should always be optimised.
  934. > Because optimised code is different version it should have a different version number.
  935. Look at this code:
  936. ``` csharp
  937. using System;
  938. using System.Threading;
  939. internal static class OptimisationBugs
  940. {
  941. private const int TERMINATING_COUNT = 10;
  942. private static int _COUNT;
  943. private static void Main()
  944. {
  945. var checker = new Thread(Checker);
  946. var stopper = new Thread(Counter);
  947. checker.Start();
  948. Thread.Sleep(10);
  949. stopper.Start();
  950. var timeout = TimeSpan.FromSeconds(1);
  951. _COUNT = 1;
  952. Console.WriteLine($"Waiting for {timeout} worker to stop.");
  953. checker.Join(timeout);
  954. if (checker.IsAlive)
  955. {
  956. Console.Error.WriteLine($"Thread failed to stop. Aborting instead. ");
  957. checker.Abort();
  958. }
  959. Console.WriteLine("Done");
  960. }
  961. private static void Counter()
  962. {
  963. for (; _COUNT < 21; _COUNT++)
  964. {
  965. Console.WriteLine($"Count: {_COUNT}");
  966. if (_COUNT == TERMINATING_COUNT)
  967. Console.WriteLine($"Terminator {TERMINATING_COUNT} reached.");
  968. }
  969. }
  970. private static void Checker()
  971. {
  972. var x = 0;
  973. while (_COUNT < TERMINATING_COUNT) x++;
  974. Console.WriteLine($"{nameof(Checker)} stopped at {x}.");
  975. }
  976. }
  977. ```
  978. If we compile this code without any optimisations then things happen as we intend; that being that the `Checker` counts as high as it can before the `_COUNT` variable exceeds `9`.
  979. Waiting for 00:00:01 worker to stop.
  980. Count: 1
  981. Count: 2
  982. Count: 3
  983. Count: 4
  984. Count: 5
  985. Count: 6
  986. Count: 7
  987. Count: 8
  988. Count: 9
  989. Count: 10
  990. Terminator 10 reached.
  991. Count: 11
  992. Count: 12
  993. Count: 13
  994. Count: 14
  995. Count: 15
  996. Count: 16
  997. Count: 17
  998. Count: 18
  999. Count: 19
  1000. Count: 20
  1001. Checker stopped at 9101073.
  1002. Done
  1003. Now compile this again, this time optimised. Now the output is different, the timeout on the `Join` is breached. If fact, if we didn't have the timeout there the application would never exit.
  1004. To fix this problem we need to signal to the compiler that it need to fetch the value in `_COUNT` each time. We need to mark the read as volatile. We have two options here, we could put the `volatile` keyword in from of the declaration or we can use the static methods off the `Volatile` class. The latter of these options is preferred for two reasons:
  1005. 1. Using the `volatile` keyword causes every read and write to be volatile.
  1006. 1. Volatility is an operation of individual reads and writes, not of declaration.
  1007. In this case it's the read that should be volatile, so we can correct the `Checker` method.
  1008. ``` csharp
  1009. private static void Checker()
  1010. {
  1011. var x = 0;
  1012. while (Volatile.Read(ref _COUNT) < TERMINATING_COUNT) x++;
  1013. Console.WriteLine($"{nameof(Checker)} stopped at {x}.");
  1014. }
  1015. ```
  1016. Notice that the target of the read is passed by reference.