Multi-Threading I 2020
When we start a program, the system (OS) creates a new process.
In other words, for each *.exe loaded into memory, the OS creates a separate and isolated process for use during its lifetime. The process is the set of resources that comprise a running program. Processes are fully isolated from each other while threads have just a limited degree of isolation. In particular, threads share memory with other threads running in the same application. A common use for multithreading is to maintain a responsive user interface while a time-consuming task run on a worker thread, the main thread is free to continue processing other events.
The basic things to know about threads:
- Be default, a process contains only a single thread, which executes from the beginning of the program to the end.
- A thread can spawn other threads, so that at any time, a process might have multiple threads in various states, executing different parts of the program.
- If there are multiple threads in a process, they all share the process's resources.
- Threads are the units that are scheduled by the system for executing on the processor not processes.
More exactly it is Thread of Execution which is the smallest unit of processing.
- It is scheduled by an OS.
- In general it is contained in a process. So, multiple threads can exist within the same process.
- It shares the resources with the process. The memory, code (instructions), and global variable (context - the values that its variables reference at any given moment ).
- An analogy: Multiple Threads is multiple cooks from a recipe of a cook book. It follows the instructions of the recipe.
- On a single processor, each thread has its turn by multiplexing based on time. On a multiple processor, each thread is running at the same time with each processor/core running a particular thread.
Processes and threads are related to each other but are fundamentally different.
A process can be thought of as an instance of a program in execution. Each process is an independent entity to which system resources such as CPU time, memory, etc. are allocated and each process is executed in a separate address space. If we want to access another process; resources, inter-process communications have to be used such as pipes, files, sockets etc.
A thread uses the same stack space of a process. A process can have multiple threads. A key difference between processes and threads is that multiple threads share parts of their state. Typically, one allows multiple threads to read and write the same memory (no process can directly access the memory of another process). However, each thread still has its own registers and its own stack, but the other thread can read and write the stack memory.
A thread is a particular execution path of a process. When one thread modifies a process resource, the change is immediately visible to sibling threads.
- Processes are independent while thread is within a process.
- Processes have separate address spaces while threads share their address spaces.
- Processes communicate each other through inter-process communication.
- Faster on a multi-CPU system.
- Even in a single CPU system, application can remain responsive by using worker thread runs concurrently with the main thread.
Multithreading creates program overhead and additional complexity.
- There are time and resource costs in both creating and destroying threads.
- The time required for scheduling threads, loading them onto the process, and storing their states after each time slice is pure overhead.
- Since the threads in a process all share the same resources and heap, it adds additional programming complexity to ensure that they are not ruining each other's work. This might be the biggest cost we pay for doing multithreading.
- Debugging multithreaded programs can be quite difficult, since the timing on each run of the program can be different, reproducing the same results is touch. And the act of running the program in debugger blows the timing out of the water.
The System.Threading provides a number of types that enable the direct construction of multithread applications. It also allows us to interact with a CLR thread and CLR maintained thread pool, Timer class and so on. Here is the list of Types of the Ssystem.Threading Namespace:
Interlocked | This type provides atomic operations for types that are shared by multiple thread. |
Monitor | This type provides the synchronization of threading object using locks and wait/signals. The C# lock keyword is using a Monitor type under the hood. |
Mutex | This synchronization primitive can be used for synchronization between application domain boundaries. |
ParameterizedThreadStart | This delegate allows a thread to call methods that take any number of arguments. |
Semaphore | This type allows us to limit the number of threads that can access a resource, or a particular type of resource, concurrently. |
Thread | This type represents a thread that executes within the CLR. Using this type, we're able to spawn additional threads in the originating AppDomain. |
ThreadPool | This type allows us to interact with the CLR-maintained thread pool within a given process. |
ThreadPriority | This enum represents a thread's priority level. |
ThreadStart | This delegate is used to specify the method to call for a given thread. Unlike ParameterizedThreadStart, targets of ThreadStart must match a fixed prototype. |
ThreadState | This enum specified the valid state a thread make take such as Running, Aborted etc. |
Timer | This type provides a mechanism for executing a method at specified intervals. |
TimerCallbask | This delegate type is used in conjunction with Timer types. |
The most primitive of all types in System.Threading namespace is Thread. This type defines a number of methods that allow us to create new threads within the current AppDomain. It also allows us to suspend, stop, and destroy a particular thread.
Here is the list of Key Static Member of the Thread Type:
CurrentContext | This read-only property returns the context in which the thread is currently running. |
CurrentThread | This read-only property returns a reference to the currently running thread. |
GetDomain()/GetDomainID() | These methods return a reference to the current AppDomain or the ID of this domain in which the current thread is running. |
Sleep() | This method suspends the current thread for a specified time. |
Here is the list of Instance-Level Member of the Thread Type:
IsAlive | Returns a Boolean that indicates whether this thread has been started. |
IsBackGround | Gets or sets a value indicating whether or not this thread is a background thread. |
Name | This allows us to establish a friendly text name of the thread. |
Priority | Gets or sets the priority of a thread, which may be assigned a value from the ThreadPriority enumeration. |
ThreadState | Gets the state of this thread, which may be assigned a value from the ThreadState enumeration. |
Abort() | Instructs the CLR to terminate the thread as soon as possible. |
Interrupt() | Interrupts (wakes) the current thread from a suitable wait period. |
Join() | Blocks the calling thread until the specified thread (the one on which Join() is called) exits. |
Resume() | Resumes a thread that has been previously suspended. |
Start() | Instructs the CLR to execute the thread as soon as possible. |
Suspend() | Suspends the thread. If the thread is already suspended, a call to Suspend() has no effect. |
using System; using System.Threading; namespace MultiThreadI { class FirstThread { static void Main(string[] args) { Thread t1 = new Thread(threadJobA); Thread t2 = new Thread(threadJobB); t1.Start(); t2.Start(); threadJobMain(); //t1.Start(); //t2.Start(); } static void threadJobA() { for (int i = 0; i < 101; i++) { Console.Write("A{0} ", i); } } static void threadJobB() { for (int i = 0; i < 101; i++) { Console.Write("B{0} ", i); } } static void threadJobMain() { for (int i = 0; i < 101; i++) { Console.Write("M{0} ", i); } } } }
Output from the run is:
A0 B0 B1 B2 B3 B4 B5 B6 A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12 A13 A14 A15 A16 A17 A18 A19 A20 A21 A22 A23 A24 A25 A26 A27 A28 A29 A30 A31 A32 A33 A34 A35 A36 A37 A38 A39 A40 A41 A42 A43 A44 A45 A46 A47 A48 A49 A50 A51 A52 M0 M1 B7 B8 B9 B10 B11 B12 B13 B14 B15 B16 B17 B18 B19 B20 B21 B22 B23 B24 B25 B26 M2 M3 M4 M5 M6 M7 M8 A53 A54 A55 A56 A57 A58 A59 A60 A61 A62 A63 A64 A65 A66 A67 A68 A69 A70 A71 A72 A73 A74 A75 A76 A77 A78 A79 A80 A81 A82 A83 A84 A85 A86 A87 A88 A89 A90 A91 A92 A93 A94 A95 A96 A97 B27 B28 M9 M10 M11 M12 M13 M14 M15 M16 B29 B30 B31 B32 M17 M18 M19 M20 M21 M22 M23 M24 M25 M26 M27 M28 M29 M30 M31 M32 M33 M34 M35 M36 M37 M38 M39 M40 M41 M42 M43 M44 M45 M46 M47 M48 M49 M50 M51 M52 M53 M54 M55 M56 M57 M58 M59 M60 M61 M62 M63 M64 M65 M66 M67 M68 M69 M70 M71 M72 M73 M74 M75 M76 M77 B33 B34 B35 B36 B37 B38 B39 B40 B41 B42 B43 B44 B45 B46 B47 B48 B49 B50 B51 B52 B53 B54 B55 B56 B57 B58 B59 B60 B61 B62 B63 B64 B65 B66 B67 B68 B69 B70 B71 B72 B73 B74 B75 B76 B77 B78 B79 B80 B81 B82 B83 B84 B85 B86 B87 B88 B89 B90 B91 B92 B93 B94 B95 B96 B97 B98 B99 B100 M78 M79 M80 M81 M82 M83 M84 A98 A99 A100 M85 M86 M87 M88 M89 M90 M91 M92 M93 M94 M95 M96 M97 M98 M99 M100
The main thread creates a new thread t1 and t2 on which each of the thread runs its method (a delegate passed to the Thread's constructor) that repeatedly print the character "A#" or "B#". Simultaneously, the main thread repeatedly prints the character "M#". We probably may not get the same output from the next run.
Once started, a thread's IsAlive property returns true, until the point where the thread ends. A thread ends when the delegate passed to the Thread's constructor finishes executing.
Once ended, a thread cannot restart. If we try to restart a thread, we may get the error: something like this:
Unhandled Exception: System.Threading.ThreadStateException: Thread is running or terminated; it cannot restart.
We can test it if we run the code after taking off the comments:
//t1.Start(); //t2.Start();
In the following example, we define a method with a local variable, then call the method simultaneously on the main thread and a newly created thread. The CLR assigns each thread its own memory stack so that local variables are kept separate.
using System; using System.Threading; namespace MultiThreadI { class ThreadWithLocalCounter { static void Main(string[] args) { new Thread(threadJob).Start(); threadJob(); } static void threadJob() { for (int count = 0; count < 10; count++) { Console.Write("{0} ", count); } } } }
Output is:
0 0 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9
A separate copy of the cycles variable is created on each thread's memory stack, and so the output produced is, (2*count) numbers not (1*count) numbers.
In this example, the two thread will share count variable by declaring it as static.:
using System; using System.Threading; namespace MultiThreadI { class ThreadWithStaticCounter { static int count; static void Main(string[] args) { new Thread(threadJob).Start(); threadJob(); } static void threadJob() { while(count < 10) { Console.Write("{0} ", count); count++; } } } }
Now, the output only has (1*count) numbers not (2*count) numbers:
0 1 3 4 5 6 7 8 9 2
From the output, we see one of the thread had only one chance to produce "2". Probably, it didn't have a chance to write while kept the value at the time and finally had a chance to write after the other thread ended. So, we don't seem to be quite there yet.
In this example, the two threads will share count variable: the threads have a common reference to the same object instance:
using System; using System.Threading; namespace MultiThreadI { class ThreadInstanceTest { int count; static void Main(string[] args) { ThreadInstanceTest ti = new ThreadInstanceTest(); new Thread(ti.threadJob).Start(); } void threadJob() { while(count < 10) { Console.Write("{0} ", count++); } } } }
We get the output as we expected:
0 1 2 3 4 5 6 7 8 9
When a managed thread is created, the method that executes on the thread is represented by a ThreadStart delegate or a ParameterizedThreadStart delegate that is passed to the Thread constructor. The thread does not begin executing until the Thread.Start method is called. The ThreadStart or ParameterizedThreadStart delegate is invoked on the thread, and execution begins at the first line of the method represented by the delegate. In the case of the ParameterizedThreadStart delegate, the object that is passed to the Start(Object) method is passed to the delegate.
The ParameterizedThreadStart delegate and the Thread.Start(Object) method overload make it easy to pass data to a thread procedure, but this technique is not type safe because any object can be passed to Thread.Start(Object). A more robust way to pass data to a thread procedure is to put both the thread procedure and the data fields into a worker object.
The following code example shows the syntax for creating and using a ParameterizedThreadStart delegate with a static method and an instance method.
using System; using System.Threading; namespace MultiThreadI { public class ThreadTest { public static void Main() { Thread newThread = new Thread(ThreadTest.threadJob); newThread.Start("Shared Thread"); ThreadTest t = new ThreadTest(); newThread = new Thread(t.moreThreadJob); newThread.Start("Instance Thread"); } public static void threadJob(object data) { Console.WriteLine("{0}", data); } public void moreThreadJob(object data) { Console.WriteLine("{0}", data); } } }
The output is:
Shared Thread Instance Thread
Let's look into the code:
Thread newThread = new Thread(ThreadTest.threadJob);
To start a thread using a shared thread procedure, we use the class name and method name
when we create the ParameterizedThreadStart delegate.
C# infers the appropriate delegate creation syntax:
new ParameterizedThreadStart(ThreadTest.threadJob)
newThread.Start("Shared Thread");
We used the overload of the Start method that has a parameter of type Object. We can create an object that contains several pieces of data, or we can pass any reference type or value type. In the code above, we pass the string.
ThreadTest t = new ThreadTest(); newThread = new Thread(t.moreThreadJob);
To start a thread using an instance method for the thread procedure, we used the instance variable and method name when we create the ParameterizedThreadStart delegate.
C# infers the appropriate delegate creation syntax: new ParameterizedThreadStart(t.moreThreadJob)
newThread.Start("Instance Thread");
We passed an object containing data for the thread.
We could have used more compact code for the Main() as the following code:
public static void Main() { new Thread(ThreadTest.threadJob).Start("Shared Thread"); ThreadTest tt = new ThreadTest(); new Thread(tt.moreThreadJob).Start("Instance Thread"); }
Now that we can pass data to threads, we can revise the previous example which is printing count variable. In the following example, we'll print out the count variable up to 29 and we'll be able to tell which thread is printing the variable:
using System; using System.Threading; namespace MultiThreadI { public class ThreadTest { static int count = 0; public static void Main() { new Thread(ThreadTest.staticJob).Start("s:"); ThreadTest tt = new ThreadTest(); new Thread(tt.instanceJob).Start("i:"); } public static void staticJob(object data) { while (count < 30) { Console.Write("{0}{1} ", data, count++); } } public void instanceJob(object data) { while (count < 30) { Console.Write("{0}{1} ", data, count++); } } } }
Output is:
s:0 i:1 i:3 i:4 s:2 s:6 s:7 s:8 s:9 s:10 s:11 s:12 s:13 s:14 s:15 s:16 s:17 s:18 s:19 s:20 s:21 s:22 s:23 s:24 s:25 s:26 i:5 i:28 i:29 s:27
As we see from the example, it's not printing the count in the ordered manner (ascending order).
We'll resolve this issue in the following chapters.
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization