Processes and threads are two topics that programmers can’t get around, and these two abstractions provided by operating systems are too important.

One of the classic questions about processes and threads is, what is the difference between a process and a thread? I’m sure many of you know the answer.

Remember not really understand

Some students may have “memorized” thoroughly: “process is the operating system allocation of resources unit, thread is the basic unit of scheduling, thread sharing process resources”.

But do you really understand the above sentence? Exactly what process resources are shared between threads, and what does sharing resources mean? How does sharing resources work? If you don’t have an answer to this, it means it’s almost impossible to write a multithreaded program that works correctly, and it means this article is for you.

Reverse thinking

Charlie Munger often says, “Think the other way around, always the other way around.” If you’re wondering what process resources are shared between threads, you can also think the other way around: what resources are private to threads.

Thread private resources

The essence of thread running is actually the execution of function, the execution of function always has a source, this source is the so-called entry function, CPU starts from the entry function to form a flow of execution, but we artificially give a name to the execution flow, this name is called thread.

Since the essence of thread execution is function execution, what information does function execution have?

In what does a function look like in memory when it runs? In this article, we said that the information at the time A function is run is stored in the stack frame. The stack frame holds the return value of the function, the parameters to call other functions, the local variables used by the function, and the register information used by the function, as shown in the figure. Suppose function A calls function B:

In addition, CPU execution information is stored in a register called the program counter, which tells us which instruction to execute next. Since the operating system can suspend a thread at any time, we can save and restore the value in the program counter to know where the thread stopped and where to resume running.

Since the essence of thread running is function running, function runtime information is stored in the stack frame, so each thread has its own independent, private stack area.

At the same time, the function needs additional registers to hold some information, such as some local variables. These registers are thread private, and it is impossible for one thread to access the register information of another thread.

As we know from the above discussion, the stack area of the owning thread, the program counter, the stack pointer, and the registers with which the function runs are, until now, thread-private.

All of this information has a common name, thread context.

We also said that the operating system needs to interrupt the running of the thread at any time and need the thread to resume running after being suspended. The operating system can achieve this by relying on thread context information.

Now you should know which threads are private.

Beyond that, the rest is shared resources between threads.

So what’s left? And then there’s this.

This is exactly what the process address space looks like, meaning that threads share everything in the process address space except for the context information in the thread, meaning that threads can read it directly.

So let’s look at each of these regions.

Code section

The code area in the process address space, what is stored here? As some of you might have guessed from the name, yes, this is the code we wrote, or more precisely the compiled executable machine instructions.

So where do these machine instructions come from? The answer is loaded into memory from the executable, the code area in the executable that initializes the code area in the process’s address space.

Threads share code areas, which means that any function in a program can be executed in a thread. There is no such thing as a function being executed by a particular thread.

The heap area

The heap area is familiar to programmers. We use malloc or new in C/C++ to store data in this area. Obviously, as long as we know the address of the variable, that is, the pointer, any thread can access the data pointed to by the pointer, so the heap area is shared by threads and belongs to the process.

The stack area

Oh, wait! I just said that the stack area is a thread private resource, why now talk about the stack area?

It is true that the stack area is thread-private in the abstract sense of the thread, but in practice the stack area is thread-private rule is not strictly observed.

In general, notice that the word here is usually, in general the stack area is thread private, and since there are usually there are not usually.

Not usually because unlike strict isolation between the process’s address space, the thread stack area no strict isolation mechanism to protect, so if one thread can get from another thread stack frame pointer, then the thread can change another thread stack area, that is to say, the threads can be edited in this belongs to another thread stack area variables.

This is a great convenience to the programmer in some ways, but it can also lead to bugs that are extremely difficult to detect.

Imagine your program is running well, as a result, some time suddenly out of the question, out of the question to locate lines of code simply screening after less than reason, of course, you are not problems, because there is no any questions, your program already is a function of other people’s problems cause you stack frame data is written to produce bad bugs, such problems are often difficult to troubleshoot the reason, You need to be very familiar with the overall project code, and some of the usual debugging tools may not be useful at this point.

So with that said, you might ask, how can one thread modify data that belongs to another thread?

Let’s look at this with a code example.

file

Finally, if a program opens files during execution, the process address space contains information about the opened files, which can be used by all threads as shared resources. For file IO operations, you can refer to “What happens when a program reads a file?”

One More Thing: TLS

Is that all for this article?

In fact, there is one item on thread-private data that we didn’t cover in detail at the beginning of this article, because it would have been too much to cover.

Another technique for Thread private data is Thread Local Storage, OR TLS.

What does that mean?

Thread-local storage: thread-local storage: thread-local storage: thread-local storage

  • Variables stored in this area are global variables and are accessible to all threads
  • Although it appears that all threads are accessing the same variable, the global variable is owned by a single thread, and changes made to the variable by one thread are not visible to other threads.

Have you said so much and still not understood? It doesn’t matter. You can hit me if you don’t understand these two pieces of code.

Let’s start with the first piece of code. Don’t worry, it’s very, very simple:

int a = 1; Void print_a() {cout<<a<<endl; } void run() { ++a; print_a(); } void main() { thread t1(run); t1.join(); thread t2(run); t2.join(); }Copy the code

Okay, so this code is simple enough, and this code is written in C++11, so let me show you what this code means.

  • First we create a global variable, a, with an initial value of 1
  • Next, we create two threads, each incrementing variable A by 1
  • The join function of a thread means that the thread is finished before continuing to run the following code

So what does this code print when it runs?

The initial value of the global variable a is 1, and a becomes 2 after the first thread increments by 1, so 2 is printed; The second thread increments by 1 again and a becomes 3, so it prints 3. Let’s look at the result:

2, 3,Copy the code

It appears that our analysis is correct, and the global variable eventually becomes 3 after each thread increments by 1.

Next we change the definition of variable A slightly, leaving the rest of the code unchanged:

__thread int a = 1; // Thread local storageCopy the code

If we tell the compiler to place the global variable a in thread-local storage, how does this change the program?

A simple run will tell:

2
2
Copy the code

Is that what you might think? Some of you might be surprised, why is it that we increment variable A twice, but the second time we run it, we print 2 instead of 3?

Think about why.

So, that’s what thread-local storage is all about, so changes made by thread T1 to variable A don’t affect thread T2, which becomes 2 by adding 1 to variable A, but for thread T2, variable A is still 1, so it’s still 2 by adding 1.

Therefore, thread-local storage allows you to use a thread-unique global variable. That is, although the variable can be accessed by all threads, there is a copy of the variable in each thread, and changes made by one thread to the amount of change do not affect other threads.

conclusion

How, did not expect the textbook on a simple sentence “thread sharing process resources” behind will actually have so much knowledge, knowledge on the textbook is really boring, but, not simple.

Hopefully this article will help you understand how processes and threads work.