I. Overview
A thread is a discrete processing queue that allows different functions to be executed at the same time in the same program, this makes it very important for a function that carries out a special operation for a long time to be executed without interrupting other functions. The thread actually allows two types of functions to be executed simultaneously without waiting for each other.
Once an application is started, it contains only one default thread. This thread executes the main () function. The called function in main () is executed in the context sequence of this thread. Such a program is called a single-threaded program.
On the contrary, programs that create new threads are multi-threaded programs. They can not only execute multiple functions at the same time, but also play an important role in the era of multi-core prevalence. Since multiple cores allow simultaneous execution of multiple functions, this requires developers to use such processing capabilities accordingly. However, threads have been used to execute multiple functions concurrently, and developers have to carefully construct applications to support such concurrency. Multi-threaded programming knowledge has become increasingly important in the multi-core system era.
This chapter introduces the C ++ boost library boost. Thread, which can develop multi-threaded applications independent of the platform.
Ii. Thread Management
The most important class in this library is boost: thread, which is defined in boost/thread. HPP to create a new thread. The following example shows how to use it.
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } void thread() { for (int i = 0; i < 5; ++i) { wait(1); std::cout << i << std::endl; } } int main() { boost::thread t(thread); t.join(); }
The name of the function executed in the new thread is passed to the boost: thread constructor. Once the variable t in the preceding example is created, the thread function is immediately executed in the thread where it is located, and the thread is also concurrently executed in main.
In this example, the join method must be called for the new thread to prevent program termination. The join method is a blocking call: It can pause the current thread until the end of the thread that calls the join operation. This makes the main function always wait until the thread stops running.
As shown in the preceding example, a specific thread can be accessed through a variable such as t, and the variable waits for its termination using the join method. However, the thread will continue to execute even if T is out of bounds or destructed. A thread is always bound to a boost: thread variable at the beginning, but once created, it does not depend on it. There is even a method called detach that allows variables of the boost: thread type to be separated from the corresponding thread. Of course, a join method cannot be called because this variable is no longer a valid thread.
What can be done in any function can also be done in one thread. Therefore, a thread is just a function, except that it is executed simultaneously. In the preceding example, five numbers are written into the standard output stream in a loop. To reduce the output speed, calling the wait function in each loop delays the execution by one second. Wait can call a function named sleep, which also comes from boost. Thread, which is located in the namespace of Boost: this_thread.
Sleep () allows the thread to continue execution after a certain period of time or a specific time point. By passing an object of the boost: posix_time: seconds type, we specify a period of time in this example. Boost: posix_time: seconds comes from the boost. datetime library. It is used by boost. thread to manage and process time data.
Although the previous example shows how to wait for a different thread, the following example shows how to interrupt a thread through a so-called Central breakpoint.
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } void thread() { try { for (int i = 0; i < 5; ++i) { wait(1); std::cout << i << std::endl; } } catch (boost::thread_interrupted&) { } } int main() { boost::thread t(thread); wait(3); t.interrupt(); t.join(); }
Calling interrupt () on a thread object will interrupt the corresponding thread. In this regard, interruption means an exception of the type Boost: thread_interrupted, which will be thrown in this thread. This occurs only when the thread reaches the interrupted point.
A simple call to interrupt does not work if the specified Thread does not contain any breakpoints. Every time a breakpoint occurs in a thread, it checks whether interrupt has been called. A boost: thread_interrupted exception is thrown only when it is called.
Boost. thread defines a series of medium breakpoints, such as the sleep () function, because sleep () is called five times in this example, the thread checks whether the thread should be interrupted for five times. However, calls between sleep () cannot interrupt the thread.
Once the program is executed, it prints only three numbers to the standard output stream. This is because the interrupt () method is called three seconds later in main. Therefore, the corresponding thread is interrupted and a boost: thread_interrupted exception is thrown. This exception is also correctly captured within the thread, and catch processing is empty. Because the thread () function is returned after the processing program, the thread is also terminated. In turn, the entire program will be terminated because main () waits for the thread to terminate the thread using join.
The boost. Thread definition includes ten interruptions, such as the sleep () function. With these breakpoints, the thread can be easily interrupted in a timely manner. However, they are not always the best choice, because the breakpoint must be read beforehand to check for Boost: thread_interrupted exceptions.
To provide an overall overview of the various functions provided in boost. thread, the following example will introduce two more.
#include <boost/thread.hpp> #include <iostream> int main() { std::cout << boost::this_thread::get_id() << std::endl; std::cout << boost::thread::hardware_concurrency() << std::endl; }
Use the boost: this_thread namespace to provide independent functions for the current thread, such as the previous sleep (). Get_id (): returns the ID of the current thread. It is also provided by boost: thread.
Boost: The Thread class provides a static method, hardware_concurrency (), which can return the number of threads simultaneously running on the physical machine based on the number of CPUs or the number of CPU cores. Call this method on a common dual-core machine. The returned value is 2. In this way, we can determine the theoretical maximum number of threads that can be run simultaneously in a multi-core program.
Iii. Synchronization
Although the use of Multithreading can improve the application performance, it also increases complexity. If you use a thread to execute several functions at the same time, you must synchronize the functions when accessing shared resources. Once the application reaches a certain scale, this involves a considerable amount of work. This section describes the classes that boost. Thread provides for Synchronous threads.
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } boost::mutex mutex; void thread() { for (int i = 0; i < 5; ++i) { wait(1); mutex.lock(); std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl; mutex.unlock(); } } int main() { boost::thread t1(thread); boost::thread t2(thread); t1.join(); t2.join(); }
Multi-threaded programs use so-called mutex objects for synchronization. Boost. Thread provides multiple mutex classes. Boost: mutex is the simplest one. It uses binary mutex in Linux. The basic principle of mutex is to prevent other threads from gaining ownership when a specific thread has resources. Once released, other threads can gain ownership. This will cause the thread to wait for another thread to complete some operations and release the ownership of the mutex object accordingly.
The above example uses a mutex global mutex object of the boost: mutex type. The thread () function obtains the ownership of this object and writes it to the standard output stream using the lock () method in the for loop. Once the information is written, use the unlock () method to release the ownership.
Main () creates two threads and executes the thread () function at the same time. With the for loop, each thread is counted to 5, and a message is written to the standard output stream with an iterator. However, the standard output stream is a global object shared by all threads. This standard does not provide any guarantee that STD: cout can be securely accessed from multiple threads. Therefore, access to the standard output stream must be synchronized: at any time, only one thread can access STD: cout.
Because the two threads try to obtain the mutex before writing the standard output stream, only one thread can access STD: cout at a time. No matter which thread successfully calls the lock () method, all other threads must wait until unlock () is called.
Obtaining and releasing mutex is a typical mode supported by boost. thread through different data types. For example, the boost: lock_guard class can also be used to call lock () and unlock () without direct grounding.
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } boost::mutex mutex; void thread() { for (int i = 0; i < 5; ++i) { wait(1); boost::lock_guard<boost::mutex> lock(mutex); std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl; } } int main() { boost::thread t1(thread); boost::thread t2(thread); t1.join(); t2.join(); }
Boost: lock_guard automatically calls lock () and unlock () in its internal constructor and destructor respectively (). Access to shared resources needs to be synchronized because it is displayed as being called by two methods. Boost: The lock_guard class is another raiI term that appeared in my previous 2nd series of smart pointer units.
In addition to boost: mutex and boost: lock_guard, boost. Thread also provides other classes to support various types of synchronization. One of the most important features is boost: unique_lock. Compared with boost: lock_guard, it provides many useful methods.
#include <boost/thread.hpp> #include <iostream> void wait(int seconds) { boost::this_thread::sleep(boost::posix_time::seconds(seconds)); } boost::timed_mutex mutex; void thread() { for (int i = 0; i < 5; ++i) { wait(1); boost::unique_lock<boost::timed_mutex> lock(mutex, boost::try_to_lock); if (!lock.owns_lock()) lock.timed_lock(boost::get_system_time() + boost::posix_time::seconds(1)); std::cout << "Thread " << boost::this_thread::get_id() << ": " << i << std::endl; boost::timed_mutex *m = lock.release(); m->unlock(); } } int main() { boost::thread t1(thread); boost::thread t2(thread); t1.join(); t2.join(); }
The above example uses different methods to demonstrate the boost: unique_lock function. Of course, the usage of these functions is not necessarily applicable to the given scenario; the usage of Boost: lock_guard in the previous example is quite reasonable. This example demonstrates the functions provided by boost: unique_lock.
Boost: unique_lock provides multiple constructors to obtain mutex in different ways. The function that expects to obtain the mutex simply calls the lock () method and waits until the mutex is obtained. So its behavior is the same as that of Boost: lock_guard.
If the second parameter is passed in a value of the boost: try_to_lock type, the corresponding constructor will call the try_lock method. This method returns the bool type value: If the mutex can be obtained, true is returned; otherwise, false is returned. Compared with the lock function, try_lock returns immediately and is not blocked before obtaining the mutex.
The above program passes in boost: try_to_lock to the second parameter of the constructor of Boost: unique_lock. Then, you can use owns_lock () to check whether the mutex can be obtained. If not, owns_lock () returns false. This also uses another function provided by boost: unique_lock: timed_lock () to wait for a certain amount of time to obtain the mutex. The given program waits for up to 1 second and should have enough time to obtain more mutex.
In fact, this example shows three methods to obtain a mutex: Lock () will wait until a mutex is obtained. Try_lock () does not wait, but if it is available only when the mutex is available, otherwise false is returned. Finally, timed_lock () tries to obtain the mutex within a certain period of time. Like try_lock (), the return value of the bool type indicates whether the return value is successful.