POSIX Threads (English: POSIX Threads, commonly abbreviated as Pthreads) is the POSIX thread standard that defines a set of APIs for creating and manipulating threads.
In the pthread thread library, we commonly use the following:
Prototype for the function that creates a thread in the Pthread library:
1 | int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*func)(void*),void *arg) |
Here is a simple multithreading code. I will gradually summarize the knowledge used in multithreading by analyzing several pieces of code.
1 |
|
Note: The
-lpthread
parameter must be used when compiling. For example:clang++ -o pthread.exe pthread.cc -lpthread
Using a makefile
is more convenient, here is the makefile
I used:
1 | # compile pthread.c |
The compilation result is:
Next, let’s analyze several functions used from the pthread thread library:
pthread_create | Create a new thread |
---|---|
Header File | pthread.h |
Function Prototype | int pthread_create(pthread_t thread,pthread_attr_t attr,void *(*func)(void*),void *arg) |
Parameter Analysis | thread——Pointer to a pthread_t type variable attr——Pointer to a pthread_attr_t type variable, or NULL func——Pointer to the function to be run by the new thread arg——Argument passed to func |
Return Value | 0——Successful return errcode (non-zero error code)——Error |
The pthread_create
function creates a new execution thread, which calls func(arg) within the new thread. The new thread’s attributes can be specified by the attr parameter. If attr is NULL, the thread uses default attributes. func is a function that takes a pointer as an argument and returns a pointer upon completion. The parameters and return values are defined as pointers of type void, allowing them to point to any type of value.
pthread_join | Wait for a thread to terminate |
---|---|
Header File | pthread.h |
Function Prototype | int pthread_join(pthread_t thread,void **retval) |
Parameters | thread——The thread to wait for retval——Pointer to a variable to store the thread’s return value |
Return Value | 0——Successful return errcode (non-zero error code)——Error |
pthread_join
causes the calling thread to suspend until the thread specified by the thread parameter terminates. If retval is not NULL, the return value of the thread will be stored in the variable pointed to by retval.
When the thread terminates, the pthread_join function returns 0. If an error occurs, a non-zero error code is returned. It will result in an error code if a thread tries to wait for a non-existent thread, multiple threads try to wait for the same thread, or a thread tries to wait for itself.
Inter-process communication can occur via
pipes
,sockets
,signals
,exits
, and the runtime environment. Inter-thread communication is also straightforward. Multiple threads run in a single process and share global variables; therefore, they can communicate by setting and reading those global variables. However, accessing shared memory can be both useful and dangerous for threads.
Consider the following code, where two threads operate on a global variable:
1 |
|
In the code above, two threads are used. The main thread executes a loop to increment the counter by 1 every second. Before entering the loop, the main thread creates a new thread, which runs a function to print the value of the counter. The main function and print_counter run in the same process, thus both have access to the counter.
The order of execution of the threads is unpredictable. This can be seen from the output results.
It can be observed that sometimes it executes in the expected way, but other times it does not.
This is because the scheduling between threads does not guarantee that the main thread will execute before the created thread. It is possible that we may execute the created thread t1 multiple times before the main thread starts, or after the main thread has entered its loop, hence the output order is not certain.
When not using sleep, the execution result looks like this:
Here is another piece of code that uses two threads to count the number of words in two files:
Sample statistics:
1 | // test1.txt is 14 words |
The code is as follows:
1 |
|
The running result:
You can also add data to test1.txt or test2.txt:
1 | // test2.txt is 11 words |
If you output the order in which threads read the two files, similar to the previous code, it will also be unordered, haha. Why do I keep paying attention to these things.
In the code above, several new pthread interfaces are used: pthread_mutex_lock
and pthread_mutex_unlock
, to ensure that the shared global variable can be read and written by one thread at a time.
A simple example: In an airport or bus station’s public storage locker, it remains unlocked unless someone is storing something inside. When someone stores something and takes the key, no one else can open that locker until that person returns the key and unlocks it. Similarly, if two threads need to safely share a common counter, they also need a method to lock the variable, so only one thread can read from or write to the counter at any time.
The threading system contains a variable known as a
mutex
, which allows better cooperation between threads and avoids conflicts in accessing variables, functions, and resources.
pthread_mutex_lock | Wait for the mutex to unlock and then lock the mutex |
---|---|
pthread_mutex_unlock | Unlock the mutex |
Header File | pthread.h |
Function Prototype | int pthread_mutex_lock(pthread_mutex_t *mutex) int pthread_mutex_unlock(pthread_mutex_t *mutex) |
Parameters | mutex: Pointer to the mutex object |
Return Value | 0: Successful return errcode (non-zero error code): Error |
Firstly, define a global variable of type pthread_mutex_t
called counter_lock, and assign it an initial value. Then, operations on the global counter total_words
can be wrapped between calls to pthread_mutex_lock
and pthread_mutex_unlock
.
1 | pthread_mutex_lock(&counter_lock); |
Now two threads can safely share the counter. When one thread calls pthread_mutex_lock
, if another thread has already locked this mutex, that thread will be blocked waiting for the lock to be released before it can operate on the counter. Each thread unlocks the mutex after manipulating the counter so the shared data can be operated on by other threads.
Any number of threads can be suspended waiting for the mutex to unlock. When a thread unlocks the mutex, the system grants control to one of the waiting threads.
Upgrade code:
- Can accept an arbitrary number of files
- Determines how many threads and struct objects (to store file information) to create based on the number of arguments obtained via arg
- Uses struct to record the number of words in each individual file (each file has its own separate counter)
1 |
|
Let’s test it:
Here, we use four files as parameters for the program, the contents of test1.txt and test2.txt are the same as above, and the contents of test3.txt and test4.txt are as follows:
1 | // test3.txt is 10 words |