文章目录
一、线程的基本管控
1.1发起线程
线程通过构建std::thread对象而启动,该对象指明线程要运行的任务。
举一个简单的栗子:
cpp
#include <iostream>
#include <thread>
// 线程函数
void threadFunction() {
for (int i = 0; i < 10; ++i) {
std::cout << "Thread function executing..." << std::endl;
}
}
int main() {
// 创建线程并启动
std::thread t(threadFunction);
// 等待线程完成
t.join();
return 0;
}
1.2等待线程完成
若需要等待线程完成,那么可以在与之关联的std::thread实例上,通过调用成员函数join()实现。除了join(),还可以对线程进行判断joinable()。
举一个简单的栗子:
cpp
#include <iostream>
#include <thread>
void threadFunction() {
std::cout << "Thread function executing..." << std::endl;
}
int main() {
// 创建线程并启动
std::thread t(threadFunction);
// 检查线程是否可joinable
if (t.joinable()) {
// 等待线程完成
t.join();
} else {
std::cout << "Thread is not joinable." << std::endl;
}
return 0;
}
1.3出现异常情况下等待
当然程序在运行时,可能会出一些意外情况。为了防止因抛出异常而导致的应用程序终结,我们需要决定如何处理这种情况。
举一个简单的栗子:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void workerThread() {
try {
// 模拟线程执行过程中可能会出现的问题
std::this_thread::sleep_for(std::chrono::seconds(1));
mtx.lock();
ready = true;
cv.notify_one();
mtx.unlock();
} catch (const std::exception& e) {
std::cout << "Thread caught exception: " << e.what() << std::endl;
}
}
int main() {
std::thread t(workerThread);
t.detach(); // 分离线程,使其在后台运行
// 主线程等待子线程完成
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
std::cout << "Thread finished." << std::endl;
return 0;
}
1.4后台运行线程
std::thread::detach 是 C++11 标准库中 头文件提供的一个函数,用于将线程对象与其底层线程分离。一旦线程被分离,当它结束时,其资源会自动被回收,而不需要通过 std::thread::join 进行显式等待。
当调用 detach 之后,该 std::thread 对象将不再代表一个活动线程,并且无法再被 join。如果尝试对已分离的线程调用 join,程序会抛出一个 std::system_error 异常。
举个简单的栗子:
cpp
#include <iostream>
#include <thread>
#include <chrono>
void threadFunction() {
for (int i = 0; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Thread function executing..." << std::endl;
}
}
int main() {
// 创建线程并启动
std::thread t(threadFunction);
// 分离线程,使其在后台运行
t.detach();
// 主线程继续执行其他任务
for (int i = 0; i < 5; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Main thread executing..." << std::endl;
}
return 0;
}
detach函数使用注意事项:
- 线程资源管理:detach函数将线程与主线程分离,这意味着主线程不再负责等待该线程的完成。一旦线程被分离,它的资源将由运行时库负责清理。因此,如果主线程在后台线程完成之前结束,那么后台线程可能会变成"僵尸线程",这可能导致资源泄漏或其他问题。
- 线程安全:使用detach时,需要注意线程安全。如果多个线程同时操作同一个数据结构,而其中一个线程调用了detach,那么这个数据结构可能会在未同步的情况下被修改,导致数据不一致或其他问题。
- 异常处理:如果在线程函数中抛出了异常,而该异常未被捕获和处理,那么当线程结束时,该异常可能会被抛出并导致程序崩溃。因此,在使用detach时,需要确保线程函数中的异常被正确处理。
- joinable检查:在调用detach之前,最好先检查线程是否可以被join。如果线程已经被join或detach过,再次调用detach会导致未定义的行为。
二、向线程函数传递参数
若要向新线程上的函数或可调用对象传递参数,方法相当简单,直接向std::thread的构造函数添加更多参数即可。
举个简单的栗子:
cpp
#include <iostream>
#include <thread>
// 这是线程函数,接受一个整数参数
void threadFunction(int param) {
std::cout << "Thread function received: " << param << std::endl;
}
int main() {
// 创建一个线程,并传递参数
std::thread t(threadFunction, 42);
// 等待线程完成
t.join();
return 0;
}
向线程传递参数的注意点:
向线程函数传递参数时,需要注意以下几点:
- 参数类型匹配:确保传递给线程函数的参数类型与函数期望的参数类型匹配。否则,可能会导致编译错误或运行时错误。
- 参数的拷贝语义:在传递参数时,要注意参数的拷贝语义。如果传递的是大型对象或可变对象,可能会导致不必要的内存分配和拷贝。可以使用引用或指针来避免不必要的拷贝。
- 线程安全:如果多个线程同时访问和修改共享数据,需要确保线程安全。可以使用互斥锁、条件变量等同步机制来保护共享数据。
- 异常处理:如果线程函数可能抛出异常,需要确保异常能够被正确处理。可以使用try-catch语句块来捕获并处理异常。
- 参数的传递方式:可以使用值传递、引用传递或指针传递等方式传递参数。需要根据具体情况选择合适的传递方式,以避免不必要的拷贝和内存分配。
- 线程函数的返回值:如果线程函数有返回值,需要确保返回值的正确性和线程安全性。可以使用std::future或std::async等机制来获取线程函数的返回值。
总之,向线程函数传递参数时,需要注意参数的类型、拷贝语义、线程安全、异常处理、传递方式和返回值等问题,以确保程序的正确性和性能。
三、转移线程归属权
线程运行的过程中会有出现线程归属权需要进行转移的情况:
(1)编写函数,创建线程,线程置于后台运行;但函数本身不等待线程完成,将这个线程的归属权向上移交给函数的调用者;
(2)想创建一个线程,将其归属权传入某个函数,由他负责等待该线程结束;
要想实现线程归属权的转移,可以使用std::move函数来实现;
举个简单的栗子:
cpp
#include <iostream>
#include <thread>
void threadFunction(){
//线程中执行的函数
std::cout << "Thread is runnning!" <<std::endl;
}
int main(){
//创建一个线程
std::thread myThread(threadFunction);
//转移线程所有权
std::thread anotherThread = std::move(myThread);
//等待另一个线程执行完毕
anotherThread.join();
return 0;
//此时,myThread不再拥有线程的所有权,不能再join,
//myThread.join();这一行会导致编译错误;
}
四、运行时选择线程数量
通过hardware_concurrency() 来确定系统上可以并发执行的线程数。
std::thread::hardware_concurrency() 是 C++11 标准库中的一个函数,它返回一个无符号整数,表示在给定的系统上可以并发执行的线程数。这个数字通常表示系统的核心数或逻辑处理器数。
这个函数在编程中有多种用途:
(1)线程数量选择:当你需要并行执行多个任务时,可以使用 std::thread::hardware_concurrency() 来确定最适合的线程数量。这通常是一个合理的起点,但你仍然需要考虑其他因素,如任务的工作负载、内存使用情况等。
(2)资源利用:通过使用与系统核心数相匹配的线程数,可以更有效地利用系统资源。过多的线程可能会导致上下文切换的开销增加,而太少的线程则可能无法充分利用系统资源。
(3)性能测试:在测试多线程应用程序的性能时,可以使用 std::thread::hardware_concurrency() 来确定基准线程数。然后,可以逐渐增加或减少线程数量,以观察性能的变化。
举个简单的栗子:
cpp
#include <iostream>
#include <thread>
#include <vector>
// 简单的任务函数
void simpleTask(int threadId) {
std::cout << "Thread " << threadId << " is running." << std::endl;
}
int main() {
// 获取系统的最大并发线程数
unsigned int maxConcurrentThreads = std::thread::hardware_concurrency();
std::cout << "Maximum concurrent threads: " << maxConcurrentThreads << std::endl;
// 创建线程的向量
std::vector<std::thread> threads(maxConcurrentThreads);
// 启动线程并为其分配简单的任务
for (int i = 0; i < maxConcurrentThreads; ++i) {
threads[i] = std::thread(simpleTask, i);
}
// 等待所有线程完成
for (auto& thread : threads) {
thread.join();
}
std::cout << "All threads completed." << std::endl;
return 0;
}
五、识别线程
在C++多线程编程中,有2种方式能获取线程。
(1)在与线程关联的std::thread对象上调用成员函数get_id(),就可以获得该线程ID;
(2)当前线程的ID可以通过调用std::this_thread::get_id()方法便可以获得。
举个简单的栗子:
cpp
#include <iostream>
#include <thread>
//延时函数
void delay(int milliseconds) {
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
}
//方法一:调用成员函数std::this_thread::get_id()返回当前线程的ID
void print_id(){
std::cout << std::this_thread::get_id() << std::endl;
delay(100);
}
void hello_world(){
std::cout << "Hello world!" << std::endl;
}
int main(){
std::thread t1(print_id);
std::thread t2(hello_world);
//方法二:在与线程关联的std::thread对象上调用成员函数get_id()
std::cout << "t2_id:" << t2.get_id() << std::endl;
t1.join();
t2.join();
}
六、总结
书山有路勤为径,学海无涯苦作舟。
七、参考书籍
《C++并发编程实战(第二版)》