5 chap14
Jason Zhu edited this page 2021-07-27 16:24:46 +10:00

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

  1. Instantiate a thread object via Thread t = new Thread(ThreadFunc)
  2. 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 stay true
  • After thread end (i.e. delegate passed to Thread constructor finishes execution), t.IsAlive become false
  • 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 thread
  • Thread.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
}

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

14.7.1 Asynchronous Programming Model

14.7.2 Event-Based Asynchronous Pattern

14.7.3 BackgroundWorker