Table of Contents
- Chapter 14. Concurrency and Asynchrony
- 14.1 Introduction
- 14.2 Threading
- 14.2.1 Creating a Thread
- 14.2.2 Join and Sleep
- 14.2.3 Blocking
- 14.2.4 Local vs Shared State
- 14.2.5 Locking and Thread Safety
- 14.2.6 Passing Data to a Thread
- 14.2.7 Exception Handling
- 14.2.8 Signaling
- 14.2.9 Threading in Rich Client Applications
- 14.2.10 Synchronization Contexts
- 14.2.11 The Thread Pool
- 14.3 Tasks
- 14.3.1 Staring a Task
- 14.3.2 Returning values
- 14.3.3 Exceptions
- 14.3.4 Continuation
- 14.3.5 TaskCompletionSource
- 14.3.6 Task.Delay
- 14.4 Principles of Asynchrony
- 14.4.1 Synchronous vs. Asynchronous Operations
- 14.4.2 What is Asynchronous Programming?
- 14.4.3 Asynchronous Programming and Continuations
- 14.4.4 Why Language Support is Important
- 14.5 Asynchronous Functions in C#
- 14.5.1 Awaiting
- 14.5.2 Writing Asynchronous Functions
- 14.5.3 Asynchronous Streams
- 14.5.4 Asynchronous Methods in WinRT
- 14.5.5 Asynchrony and Synchronization Contexts
- 14.5.6 Optimizations
- 14.6 Asynchronous Patterns
- 14.6.1 Cancellation
- 14.6.2 Progress Reporting
- 14.6.3 The Task-Based Asynchronous Pattern
- 14.6.4 Task Combinators
- 14.6.5 Asynchronous Locking
- 14.7 Obsolete Patterns
Chapter 14. Concurrency and Asynchrony
14.1 Introduction
Multithreading (多线程) = program mechanism for simultaneously execute code. It's supported by CLR and OS
Common concurrency (并发) scenarios (related with web dev):
- Allowing requests to process simultaneously: e.g. client requests arrive at server concurrently, and must be handled in parallel for scalability. (runtime in ASP.NET Core automatically manage it, but it's still be aware of shared state)
- Parallel programming: intensive calculation executed on multicore/multiprocessor computers
- Speculative execution: multicore machine predict future action and execute before hand
14.2 Threading
- Tread = the single execution path that can proceed independently from each other
- Each thread run within a OS process. That single process provides an isolated environment (mainly memory) for the program to run
- A single-threaded program has only 1 thread running, which has access to the isolated environment
- A multi-threaded program has multithreads within same process. These threads sharing same environment (e.g. memory)
Benefit of multi-threaded program: simutaneously work
- One thread can fetch/update data (shared state)
- Another thread display data (i.e. shared state)
14.2.1 Creating a Thread
Main Thread:
- Main Thread = a single thread created by OS to house the client program
- The app lives its life within that single thread, and create thread from the main thread
Syntax for creating & start a Thread
- Instantiate a thread object via
Thread t = new Thread(ThreadFunc)
- Start thread object by
t.Start()
e.g.
using System;
using System.Threading;
namespace Task1
{
class Program
{
static void Main(string[] args)
{
Thread t = new Thread(ThreadDelegate);
t.Start(); // Thread start execution
// Main thread loop
// ... // Main start execution, will interrupt thread
// Define thread func
void ThreadDelegateFunc() {}
}
}
}
Life Cycle of Thread after starting:
- After start, property
t.IsAlive
staytrue
- After thread end (i.e. delegate passed to
Thread
constructor finishes execution),t.IsAlive
becomefalse
- Thread cannot restart after it's ended
Useful propertys:
t.Name
property can be used to set (name of thread) for debugging- static property
Thread.CurrentThread
return currently executing thread (which also has name:Thread.CurrentThread.Name
)
14.2.2 Join and Sleep
Join
t.Join()
method:
- Hold other threads until the this thread object (
t
) to be finished - Optional parameter: timeout (either in milliseconds int or as
TimeSpan
)
Thread t = new Thread(ThreadDelegateMethod);
t.Start(); // Start thread execution concurrently
t.Join(); // Pause other threads until t thread finish
Console.WriteLine("Thread t has ended!"); // Main thread execute after t finish
void Go() { for (int i = 0; i < 1000; i++) Console.Write("y")};
Sleep
Few more static methods of Threads
Thread.Sleep
(a static method, not applied to thread object) pauses current executing thread for a special period.
Thread.Sleep(TimeSpan.FromHours(1)); // Sleep for 1 hour
Thread.Sleep(500); // Sleep for 500 miliseconds
Thread.Sleep(0)
hijack current thread and hand over CPU to other threadThread.Yield()
(yield = 屈服) also hijack current thread but only limited to threads on the same processor
While waiting on Sleep
or Join
, a thread is blocked
14.2.3 Blocking
- A thread is Blocked when its execution is paused for reasons like:
- Sleeping
- Waiting for another to end via
Join
- What happen to blocked thread:
- The blocked thread immediately yield its processor time slice, and consume no processor time until unblocked
- use
t.ThreadState
property to test whether thread is blocked ThreadState
property: it's a flags enum (comprising 3 layers of data in bitwise combination). We only care about 4 values:Unstarted
Running
WaitSleepJoin
Stopped
public static ThreadState Simplify (this ThreadState ts)
{
return ts & (ThreadState.Unstarted |
ThreadState.WaitSleepJoin |
THreadState.Stopped);
}
Note: Don't use ThreadState property for diagnostic: as thread state can change btw acting and testing
I/O-bound vs. Compute-bound
- I/O-bound: an operation spends most of its time waiting for something to happen (e.g. downloading a web page)
- Compute-bound: an operation spends most of its time performing CPU-intensive work
Blocking vs. Spinning
I/O-bound operation works in 2 ways:
- Wait synchronously on current thread until it's completed (e.g.
Thread.Sleep
) - Operat asynchronously, it will fire a callback when operation finished in future
When I/O-bound operation work synchronously, it spin itself in a loop periodically to wait another operation to finish:
e.g. spin in a loop
while (DateTime.Now < nextStartTime)
{
Thread.Sleep(100)
}
Hence, spinning is wasteful on processor.
Pro & Con of Blocking and Spinning:
- Spinning saves resource only if we expect a condition to be satisfied soon (i.e. few microseconds)
- Blocking occurs cost, as each thread requires 1MB of memory for its life cycle, and cause admin overhead for CLR and OS.
(TODO: May have wrong understanding of Blocking vs Spinning here. Come back later)
14.2.4 Local vs Shared State
Local State
CLR assign each threas its own memory stack so local variables are kept separate
void Go()
{
// Declare and use a local variable 'cycles'
for (int cycles = 0; cycles < 5; cycles++ )
{
Console.Write('?');
}
}
// Create a new thread and call Go() on it
new Thread(Go).Start();
// Call Go() on main thread
Go();
Shared State
Mutliple threads share data if all threads have a common reference to the same object or variable:
e.g. Both threads share a boolean and that boolean is changed.
bool _done = false;
new Thread(Go).Start();
Go();
void Go()
{
if (!_done) { _done = true; Console.WriteLine("Done"); } // Executed only once, as it's _done is changed during one of threads' execution
}