Introduction
This series is written because I found my programming skills to be lacking, so I wanted to create a blog to help myself review.
I hope it can help everyone (and not mislead too much). If you have any questions, feel free to point them out in the comments.
The environment for this article is macOS with the built-in clang, using C++11. The command to compile in the terminal is:
clang++ -std=c++11 -o my_program your_code.cpp
Basic Concepts of Threads
First, we need to understand four basic concepts:
1. Process: A process is the basic unit of resource allocation in an operating system, and each process has its own independent memory space.
2. Thread: A thread is an execution unit within a process, and multiple threads share the resources of the process.
3. Concurrency: Concurrency refers to multiple tasks being executed in an overlapping time period, but not necessarily simultaneously.
4. Parallelism: Parallelism refers to multiple tasks being executed simultaneously at the same time point, usually relying on multi-core processors.
Next, let’s elaborate:
Process & Thread Query: On Windows, you can use Ctrl + Shift + Esc or the tasklist command in cmd; on macOS, just type top in the terminal, or you can use the ps command (same as Linux). You can find that a process often corresponds to multiple threads, and each running program usually corresponds to one process. This will be discussed further in later chapters, as it can be quite complex just to look at.
Understanding the Relationship Between Processes and Threads:
A process has at least one thread, even if it only has a main thread.
Multiple threads within a process share the resources of the process, such as memory space, but each thread has its own execution stack and program counter.
Threads can communicate through shared resources of the process, while communication between processes requires additional mechanisms.
Processor: This refers to the CPU. You can check online shopping platforms, where the most obvious specification is usually the processor, and gaming laptops will also specify a graphics card (perhaps I will introduce processor-related topics in detail later when I review). Processors can be single-core or multi-core. The “core” of a processor refers to the independent computing units within the processor. Each core can independently execute instructions. From the perspective of the processor, we can further understand concurrency and parallelism:
Concurrency: Refers to multiple tasks being performed in the same time period, possibly executed alternately, not necessarily simultaneously. However, due to the high clock frequency of the CPU, it simulates the effect of simultaneous execution through high-speed alternation, so a single-core processor can also achieve concurrency.Key Point: Can be “alternately executed”
Parallelism: Refers to multiple tasks being executed simultaneously by multiple cores at the same time point. On multi-core processors, tasks can be truly processed in parallel.Key Point: “Simultaneously”
Cpp: Thread Library
Since C++11, the <thread> library has become a very important tool.
At its most basic, creating a thread:
#include <iostream>
#include <thread>
void func(int x) {
std::cout << x << "GET\n";
}
int main() {
std::cout << "hello\n";
std::thread t(func, 8);
t.join();
// Created a thread t, passing the func function and parameter 8 to it, meaning the new thread will execute func(8). t.join() blocks the current thread until the new thread completes execution.
std::cout << "end thread\n";
return 0;
}
The above two lines of the thread class and join function
demonstrate the basic process of “creating and starting a thread” and “waiting for the thread to complete”.
Join will be discussed further in later sections.
It can also be seen that the thread can pass parameters, including the function itself (func) + function parameter (8).
Here, I would like to point out that sometimes function parameter passing can consider: std::ref (reference wrapper to avoid copying).
Example:
#include <iostream>
#include <thread>
void threadFunction(int n, const std::string& message) {
for (int i = 0; i < n; ++i) {
std::cout << message << std::endl;
}
}
int main() {
std::string msg = "Hamster Review";
std::thread t(threadFunction, 5, std::ref(msg));
t.join();
return 0;
}
Lambda Expressions
Lambda expressions can also be used with threads.
Since I do not consider myself proficient in lambda expressions, I will dedicate a separate article for review later (it can be considered a more condensed way of expressing functions). Here is a basic example:
#include <iostream>
#include <thread>
int main() {
std::thread t([]() {
std::cout << "hamster is coming" << std::endl;
});
t.join();
return 0;
}
get_id
You can use the get_id method to obtain the thread ID.
#include <iostream>
#include <thread>
void getThreadID() {
std::cout << std::this_thread::get_id() << std::endl;
}
int main() {
std::thread t(getThreadID);
t.join();
return 0;
}
The output will be a unique hexadecimal number, such as: (but I am not sure if it will change with different compilers, you can try it yourself)
BASH:
0x16db8b000
Note,this ID does not indicate anything related to memory addresses.
join, detach, and joinable
join():Wait for the thread to complete—– The current thread will wait until the target thread has finished executing.
🌟Usage Scenario: When the main thread or other threads depend on the execution result of a certain thread, we need to use join() to ensure that the thread has completed execution.
detach():Detach the thread to run independently—– Detaches the thread, allowing it to run independently in the background while the current thread continues executing.
🌟Usage Scenario: When we do not care about the execution result of the thread and want it to continue running in the background, we use detach().
joinable() is a member function of the std::thread class used to determine whether a thread object can be joined, i.e., whether the thread is still active and has not been detached or completed execution.
joinable() : Meaning: join + able?
🌟 Determine if the thread is joinable: A thread is only “joinable” after it has been created and has not been joined or detached.
🌟Avoid calling invalid join(): If a thread has already been joined or detached, calling join() or detach() will cause a crash (error shown below). Therefore, before calling join(), you should first check if the thread is “joinable”.
libc++abi: terminating due to uncaught exception of type std::__1::system_error: thread::join failed: Invalid argument
zsh: abort ./my_program
Here, I will explain the logic of threads from creation:
1. When a std::thread object is created, the thread starts and begins execution. At this point, the thread is active and in a “joinable” state, meaning it can be joined or detached.
2. When the join() method is called, the main thread will block until that thread has finished executing. Once join() is called, the thread object enters a non-joinable state.In other words, the thread has been “joined” to the main thread, and you cannot call join() or detach() on the same thread again.
3. Calling the detach() method will separate the thread from the main thread, allowing it to run independently in the background. The detached thread will continue executing, but the main thread will not wait for it to complete.Once detach() is called, the thread is no longer “joinable” because it has become independent from the main thread and cannot be joined or detached again.
Logically, let’s consider an example:
For instance, you have 10 threads, each operating on different parts of a vector.
Now you want to sum their results.
You must wait for the child threads to confirm they have calculated the results before the main thread can use those results.
Therefore, those that need to be concerned about whether they have finished are joined; you need to know if this small task is completed.
Otherwise, you would detach.
Comprehensive Code
Note that here we use the sleep_for function from chrono to simulate that some threads in complex problems require a certain amount of time (highlighting the necessity of multithreading).
#include <iostream>
#include <thread>
#include <chrono>
void worker(int id) {
std::cout << "Thread " << id << " is working...\n";
if(id==2){
std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate work delay, 2 has a longer delay
}
std::cout << "Thread " << id << " finished work.\n";
}
int main() {
std::thread t1(worker, 1); // Create thread t1
std::thread t2(worker, 2); // Create thread t2
// Use joinable to check if the thread can be joined
if (t1.joinable()) {
std::cout << "Thread 1 is joinable, joining now...\n";
t1.join(); // Wait for t1 to complete
}
// Use detach to separate the thread
if (t2.joinable()) {
std::cout << "Thread 2 is joinable, detaching now...\n";
t2.detach(); // Let t2 execute independently, main thread does not wait for t2
}
// Since t2 has been detached, the main thread does not need to wait for it
std::cout << "Main thread finishes.\n";
return 0;
}
Output (note that the detached thread 2 does not finish, and the main thread ends directly)
Thread Thread 1 is joinable, joining now...
Thread 21 is working...
Thread 1 finished work.
is working...
Thread 2 is joinable, detaching now...
Main thread finishes.
In the following sections:
I will continue to release Part 2 as I progress in my review, supplementing more information about threads and thread synchronization. Stay tuned!~

Thank you for reading, see you next time.