1, Process
In short, a process is an abstraction of resources and a container of resources. In traditional operating systems, a process is the basic unit of resource allocation and execution. Processes support concurrent execution because each process has its own data and stack space. If a program wants to execute concurrently, just start multiple processes.
Q1: how can processes execute simultaneously in a single core?
First, we should distinguish two concepts: concurrent and parallel
- Concurrency: concurrency refers to that in a small period of time, multiple program code segments are executed by the CPU, which shows that multiple programs can be executed at the same time.
- Parallel: parallel means that at a point in time, there are multiple program block codes executed by the CPU, which is the real simultaneous execution.
So it should be said that the processes are executed concurrently. For the CPU, it does not know the existence of the process. The CPU mainly deals with registers. There are some common registers, such as program counter register, which stores the address of the instruction to be executed. The CPU will go where the address of the register points to. There are also stack registers, general registers and so on. In a word, these data constitute the execution environment of a program. This execution environment is called "Context". The essence of switching between processes is to save these data to memory, the term is "save site", and then recover the execution environment of a process, that is, "recover site", the whole process The process term is called "Context switching". Specifically, it is the process Context switching, which is the essence of concurrent execution between processes - frequent process Context switching. This function is provided by the operating system. It is kernel state and transparent to application software developers.
2, Threads
Although the process supports concurrency, it is not very friendly to concurrency. Unfriendliness means that every process is started, a part of resources must be reallocated. Compared with the process, the cost of creating a thread is smaller than that of creating a process, so the introduction of a thread can better improve concurrency. In modern operating system, process becomes the basic unit of resource allocation, while thread becomes the basic unit of execution. Each thread has its own stack space, and all threads of the same process share code segment, address space and other shared resources. The corresponding context switch changes from process context switch to thread context switch.
3, Why introduce processes and threads
- In the early single channel batch processing system, if the executing code needs to depend on external conditions, the CPU will be idle, such as file reading, waiting for keyboard signal input, which will waste a lot of CPU time. The problem of low CPU utilization can be solved by introducing multiple processes and threads.
- Isolate the data between programs (each process has its own address space) to ensure the stability of system operation.
- Improve the responsiveness and interaction ability of the system.
4, Create managed thread in C ා
1. Thread class
In. NET, managed threads are divided into:
- Foreground Threads
- Background thread
In a. Net program, there must be at least one foreground thread. When all foreground threads are finished, all background threads will be forcibly destroyed by the common language runtime (CLR), and the program execution is finished.
A background thread will be created in the console program as follows
1 static void Main(string[] args) 2 { 3 var t = new Thread(() => 4 { 5 Thread.Sleep(1000); 6 Console.WriteLine("completion of enforcement"); 7 }); 8 t.IsBackground = true; 9 t.Start(); 10 }
The main thread (the default is the foreground thread) finishes executing, and the program exits directly.
But when the IsBackground property is changed to false, the console prints "execution complete.".
2. What's the problem
It is inconvenient to directly use Thread class for multithreading programming (the server side is more obvious), for example, chestnut.
If I write a Web server program and create a Thread for each request, I need to create a new Thread object every time, and then pass in the HttpRequest processing delegation. After processing, the Thread will be destroyed, which will waste a lot of CPU time and memory. In the early case of poor CPU performance and precious memory resources, this disadvantage will be magnified One disadvantage is not obvious. The reason is the hardware.
Where is the inconvenience?
- Can't get an unhandled exception in another thread directly
- Cannot get the return value of thread function directly
1 public static void ThrowException() 2 { 3 throw new Exception("exception occurred"); 4 } 5 static void Main(string[] args) 6 { 7 var t = new Thread(() => 8 { 9 Thread.Sleep(1000); 10 ThrowException(); 11 }); 12 t.IsBackground = false; 13 try 14 { 15 t.Start(); 16 } 17 catch(Exception e) 18 { 19 Console.WriteLine(e.Message); 20 } 21 }
The above code will lead to program crash, as shown below.
To get the return value directly and catch the uncapped exception in the thread function directly from the main thread, we can do so.
Create a new one MyTask.cs Document, as follows
1 using System; 2 using System.Threading; 3 namespace ConsoleApp1 4 { 5 public class MyTask 6 { 7 private Thread _thread; 8 private Action _action; 9 private Exception _innerException; 10 public MyTask() 11 { 12 } 13 public MyTask(Action action) 14 { 15 _action = action; 16 } 17 protected virtual void Excute() 18 { 19 try 20 { 21 _action(); 22 } 23 catch(Exception e) 24 { 25 _innerException = e; 26 } 27 28 } 29 public void Start() 30 { 31 if (_thread != null) throw new InvalidOperationException("Task started"); 32 _thread = new Thread(() => Excute()); 33 _thread.Start(); 34 } 35 public void Start(Action action) 36 { 37 _action = action; 38 if (_thread != null) throw new InvalidOperationException("Task started"); 39 _thread = new Thread(() => Excute()); 40 _thread.Start(); 41 } 42 public void Wait() 43 { 44 _thread.Join(); 45 if (_innerException != null) throw _innerException; 46 } 47 } 48 public class MyTask<T> : MyTask 49 { 50 private Func<T> _func { get; } 51 private T _result; 52 public T Result { 53 54 private set => _result = value; 55 get 56 { 57 base.Wait(); 58 return _result; 59 } 60 } 61 public MyTask(Func<T> func) 62 { 63 _func = func; 64 } 65 public new void Start() 66 { 67 base.Start(() => 68 { 69 Result = _func(); 70 }); 71 } 72 } 73 }
With a simple packaging (don't care about the details), we can achieve the effect we want.
The test code is as follows
1 public static void ThrowException() 2 { 3 throw new Exception("exception occurred"); 4 } 5 public static void Test3() 6 { 7 MyTask<string> myTask = new MyTask<string>(() => 8 { 9 Thread.Sleep(1000); 10 return "completion of enforcement"; 11 }); 12 myTask.Start(); 13 try 14 { 15 Console.WriteLine(myTask.Result); 16 } 17 catch (Exception e) 18 { 19 Console.WriteLine(e.Message); 20 } 21 } 22 public static void Test2() 23 { 24 MyTask<string> myTask = new MyTask<string>(() => 25 { 26 Thread.Sleep(1000); 27 ThrowException(); 28 return "completion of enforcement"; 29 }); 30 myTask.Start(); 31 try 32 { 33 Console.WriteLine(myTask.Result); 34 } 35 catch(Exception e) 36 { 37 Console.WriteLine(e.Message); 38 } 39 } 40 public static void Test1() 41 { 42 MyTask myTask = new MyTask(() => 43 { 44 Thread.Sleep(1000); 45 ThrowException(); 46 }); 47 myTask.Start(); 48 try 49 { 50 myTask.Wait(); 51 } 52 catch (Exception e) 53 { 54 Console.WriteLine(e.Message); 55 } 56 } 57 static void Main(string[] args) 58 { 59 Test1(); 60 Test2(); 61 Test3(); 62 }
As you can see, we can simply wrap the Thread object to achieve the following effects
- Read thread function return value directly
- Catch exceptions not caught by thread function directly (if Wait() function or Result property is called)
This is the basis of understanding and using task. Task function is very perfect, but using task well requires mastering many concepts. Let's talk about it next.