目录
- 1. 背景
- 2. 线程创建
- 2.1 使用std::thread类来创建线程
- 2.2 使用std::async 函数来创建线程
- 2.3 std::thread和std::async的区别
- 3. 线程互斥
- 3.1 互斥体std::mutex
- 3.2 互斥体包装器std::lock_guard
- 3.3 条件变量std::condition_variable
- 3.4 原子类型std::atomic
- 4. 线程控制自己
1. 背景
在C++11之前,C++标准库并没有提供直接支持多线程的机制。如果我们想在C++程序中实现多线程,需要依赖操作系统提供的线程API。依赖操作系统API有如下缺点↓
- 导致有跨平台兼容性差:不同的操作系统使用不同的线程API,这使得代码只能在特定的平台上运行)
- 编程复杂度高:使用操作系统API来进行多线程编程通常比使用高级语言的标准库要复杂得多,需要更多的代码和更精细的控制
操作系统API
在Linux平台上,通常会使用POSIX线程(pthread)API。您需要包含<pthread.h>头文件,并使用pthread_create等函数来创建和管理线程
在Windows平台上,可以使用Windows线程API。您需要包含<windows.h>头文件,并使用CreateThread等函数来创建和管理线程
2. 线程创建
C++11引入了多线程支持,通过使用std::thread类和std::async函数来创建和管理线程。解决了跨平台的问题,并且简化了多线程编程,大大提高了代码的可移植性和可维护性。
2.1 使用std::thread类来创建线程
void myThread_func_without_para() {std::cout << "Functions without parameters as parameters: " << std::endl;
}void myThread_func_with_para(int i) {std::cout << "Functions with parameters as parameters: " << i << std::endl;
}void myThread_funcpoint_without_para() {std::cout << "Functionspoint without parameters as parameters: " << std::endl;
}void myThread_funcpoint_with_para(int i) {std::cout << "Functionspoint with parameters as parameters: " << i << std::endl;
}
{/*std::thread类的基本所用使用 std::thread 类的构造函数(拷贝、复制函数已经删除,不能使用)创建一个新线程,并将一个可调用对象(如函数、函数指针或 lambda 表达式)作为参数传递给新线程*/std::cout << "Create thread by std::thread" << std::endl;//无参函数作为参数std::thread myThread1(myThread_func_without_para); // 创建一个新线程myThread1并调用myThread_func_without_para函数//有参函数作为参数std::thread myThread2(myThread_func_with_para, 2); // 创建一个新线程myThread2并调用myThread_func_with_para函数//无参函数指针作为参数std::thread myThread3(&myThread_funcpoint_without_para); // 创建一个新线程myThread3并调用myThread_funcpoint_without_para函数//有参函数指针作为参数std::thread myThread4(&myThread_funcpoint_with_para, 4); // 创建一个新线程myThread2并调用myThread_funcpoint_with_para函数//lambda表达式作为参数std::thread myThread5([](int i) {std::cout << "lambda expressions:" << i << std::endl; }, 5); // 创建一个新线程myThread5并调用 lambda 表达式//堵塞当前线程,等待新线程结束。(join必须有,否则程序可能异常)myThread1.join();myThread2.join();myThread3.join();myThread4.join();myThread5.join();//上述有可能不是按照线程的创建顺序来输出,而是随机的。这是因为多线程是异步方式运行的,谁都有可能先执行完,并不是先创建先执行完。}
2.2 使用std::async 函数来创建线程
int myThread_async(int i) {std::cout << "myThread_async: " << i << std::endl;return i * 10;
}void myThread_getstatus() {std::this_thread::sleep_for(std::chrono::seconds(5));
}
{/*std::async 函数的基本使用可以使用 std::async 函数来创建一个新线程,并将一个可调用对象作为参数传递给新线程,并返回一个std::future对象,该对象可以用于获取任务的结果或状态。*/std::cout << "Create thread by std::async" << std::endl;//获取线程返回值/*异步启动方式std::launch::async 一个任务被异步执行std::launch::deferred 一个任务被延迟执行(延迟到调用 std::future::get() 或 std::future::wait() 时才开始)*/std::future<int> myFuture1 = std::async(std::launch::async, myThread_async, 2); // 创建一个新的异步线程并调用 myThread_async 函数int result = myFuture1.get(); // 阻塞当前线程等待任务结束并获取结果std::cout << "myThread_async result: " << result << std::endl;//获取线程状态(软件的加载画面)/*线程状态std::future_status::ready 操作已经完成std::future_status::timeout 操作超时std::future_status::deferred 操作被延迟*/std::future<void> myFuture2 = std::async(std::launch::async, myThread_getstatus);std::cout << "Please wait 5 seconds:" << std::flush; // std::flush是C++中的一个操纵符,用于刷新缓冲区。当向标准输出流(如std::cout)写入数据时,数据通常首先存储在内部缓冲区中,然后在缓冲区满或者在一些特定的情况下才被实际写入到设备或者文件中。std::flush操纵符可以用于强制将缓冲区中的数据立即写入到设备(通常是控制台)或文件中。本例中如果不调用std::flush,那么这个字符串可能会在程序结束时才被写入到设备。while (myFuture2.wait_for(std::chrono::seconds(1)) != std::future_status::ready) {std::cout << '.' << std::flush; //控制台每隔1秒输出一个'.'}std::cout << "myThread_getstatus Finished" << std::endl;}
2.3 std::thread和std::async的区别
- std::thread 是用于并行执行任务的线程类;std::async 是用于在后台执行任务并返回结果的异步任务函数。
- 使用 std::thread 时,你需要手动管理线程的生命周期。当你不再需要线程时,你需要显式地调用join或detach来管理,线程生命周期( join() 方法来等待线程结束, detach() 方法让线程在后台运行)。
- 使用 std::async 时,你不需要手动管理线程的生命周期。当你调用std::async 函数时,它会自动在线程池中创建、调度和执行任务,并返回一个 std::future 对象,你可以使用该对象来获取任务的结果。
总结一下,当你需要更灵活地控制线程的生命周期和并行执行的方式时,可以使用 std::thread。而当你只需要简单地在后台执行任务并获取结果时,可以使用 std::async,因为它会自动管理线程的生命周期。
3. 线程互斥
3.1 互斥体std::mutex
std::mutex mtx1; //互斥对象
void print_char1(int len, char c) {mtx1.lock(); // 获取互斥对象的锁 for (int i = 0; i < len; i++) {std::cout << c << std::flush;}std::cout << std::endl;mtx1.unlock(); // 释放互斥对象的锁
}
/*互斥体std::mutexstd::mutex是C++标准库中提供的互斥对象,用于实现多线程的互斥。简单地说,互斥就是只允许一个线程访问共享资源。*/{std::thread myThread6(print_char1, 10, '#');std::thread myThread7(print_char1, 10, '*');myThread6.join();myThread7.join();/*不加锁,输出结果(交叉混合,每次执行结果不一样)###*********#####*##加锁后,输出结果(不会交叉混合,但输出顺序可能不一样)##########***********/}
3.2 互斥体包装器std::lock_guard
std::mutex mtx2; //互斥对象
void print_char2(int len, char c) {std::lock_guard<std::mutex> lock(mtx2); // 创建lock_guard对象,自动锁定互斥体for (int i = 0; i < len; i++) {std::cout << c << std::flush;}std::cout << std::endl; // 离开作用域,自动解锁互斥体
}
/*互斥体包装器std::lock_guardstd::lock_guard是C++11标准库中的一种对象,用于封装互斥体(mutex)的锁定和解锁操作(互斥体包装器)。当 std::lock_guard 对象被创建时(构造函数),它会自动锁定互斥量,当对象离开作用域时,它会自动解锁互斥量(析构函数)。主要用途是简化并发编程中的互斥体管理。它减少了因手动调用互斥体的lock()和unlock()方法而产生的错误和复杂性。使用lock_guard可以确保在任何情况下,包括异常和早期返回,都能正确地释放互斥体。std::unique_lock 是std::lock_guard 的升级加强版- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定- std::unique_lock可以随时手动加解锁,而std::lock_guard不能手动加解锁(不手动加解锁的话,同lock_grard一样,构造时自动枷锁,析构时自动释放锁)- 资源占用更多(std::unique_lock切换上下文需要更多资源,而std::lock_guard更轻量级)- 支持移动语义(std::unique_lock支持移动语义,而std::lock_guard不支持)- 与条件变量配合使用时,必须时std::unique_lock,因为它允许你在等待条件变量时解锁互斥对象,等待通知后再重新锁定互斥对象。*/{std::thread myThread8(print_char2, 10, '$');std::thread myThread9(print_char2, 10, '%');myThread8.join();myThread9.join();/*输出结果(不会交叉混合,但输出顺序可能不一样)##########***********/}
3.3 条件变量std::condition_variable
std::mutex mtx3; //互斥对象
std::condition_variable cv; //条件对象
bool ready = false; // 共享条件变量
void print_char3(int len, char c) {std::unique_lock<std::mutex> lock(mtx3); // 创建unique_lock对象,自动锁定互斥体while (!ready) { // 等待条件成立:防止伪唤醒。在多核处理器系统上,由于某些复杂机制的存在,可能发生伪唤醒,即一个线程在没有别的线程发送通知信号时也会被唤醒。因而,当线程唤醒时,检查条件是否成立是必要的。而且,伪唤醒可能多次发生,所以条件检查要在一个循环里进行。cv.wait(lock); // 堵塞当前线程,而且当线程被阻塞时,该函数会自动调用lock.unlock()释放锁,使得其他被阻塞的线程得以继续执行。直到别的线程调用notify_* 唤醒当前线程后,wait()函数也是自动调用 lock.lock(),重新获得锁}//ready为true的时候,跳出循环for (int i = 0; i < len; i++) {std::cout << c << std::flush;}std::cout << std::endl;// 离开作用域,自动解锁互斥体
}void setReady() {std::unique_lock<std::mutex> lock(mtx3); // 创建unique_lock对象,自动锁定互斥体for (int i = 0; i < 100000; i++);ready = true; // 设置条件成立 cv.notify_all(); // 通知所有等待的线程
}std::mutex mtx4; //互斥对象
std::condition_variable cv1; //条件对象
int value;
void print_value() {std::unique_lock<std::mutex> lock(mtx4); // 创建unique_lock对象,自动锁定互斥体std::cout << "Please input an integer" << std::endl;while (cv1.wait_for(lock, std::chrono::seconds(1)) == std::cv_status::timeout) { // 每次等待1秒,如果1秒内没有收到通知,wait_for会返回std::cv_status::timeout,接着程序会打印一个点,然后循环继续等待1秒std::cout << '.' << std::flush;}std::cout << "The input integer is: " << value << std::endl;// 离开作用域,自动解锁互斥体
}void setValue() {//std::unique_lock<std::mutex> lock(mtx4); // 创建unique_lock对象,自动锁定互斥体std::cin >> value; cv1.notify_all(); // 通知所有等待的线程
}
/*条件变量(std::condition_variable)std::condition_variable是C++11标准库中的一个类,用于在多线程编程中实现线程间的条件同步。条件变量用于在一个线程等待某个条件满足时进行阻塞,并在另一个线程满足条件时通知等待的线程继续执行(即只负责线程的阻塞和唤醒,条件的状态由其他变量或标准来完成)。通常与互斥量(std::mutex)一起使用,以确保线程安全。在等待条件之前,线程必须先锁定互斥量;在等待期间,线程会被阻塞,并释放与条件变量关联的互斥量。当其他线程调用notify_one或notify_all成员函数来通知满足条条件变量时,等待的线程将被唤醒并重新获取互斥量,以继续执行。阻塞线程(wait、wait_for、wait_until)wait Wait until notifiedwait_for Wait for timeout or until notifiedwait_until Wait until notified or time pointNote重载版本的最后一个参数_Predicate _Pred是一个返回布尔值的函数或表达式,用于在等待条件变量时进行判断。当传入了这个参数的时候,只有为true,且收到通知的情况下(或者指定了等待时间、时间点),wait系列函数才会返回。唤醒阻塞线程(notify_one或notify_all)notify_one 解锁一个线程,如果有多个,则任意一个线程执行,其余继续等待notify_all 解锁所有线程*/{//waitstd::thread myThread10(print_char3, 10, '@');std::thread myThread11(setReady);myThread10.join();myThread11.join();//wait_forstd::thread myThread13(setValue);std::thread myThread12(print_value);myThread12.join();myThread13.join();//生产者/消费者场景/*生产者-消费者问题是一个经典的并发编程问题,涉及到共享固定大小的缓冲区。生产者在缓冲区中添加数据,消费者从缓冲区中消费数据。当缓冲区已满时,生产者应该等待;当缓冲区为空时,消费者应该等待。*//*std::thread myThread14(producer);std::thread myThread15(consumer);myThread14.join();myThread15.join();*/}
3.4 原子类型std::atomic
int number = 0;
std::atomic<int> atomicNumber(0);
void myThread_atomic(int loop) {for (int i = 0; i < loop; i++) {atomicNumber++;number++;}
}
{/*std::atomic 是C++11引入的一个模板类,用于支持原子操作。它提供了一种线程安全的方式来操作共享变量,避免了多线程环境下的竞态条件。在多线程编程中,多个线程可能会同时访问和修改共享数据,这可能会导致数据竞争(data race)的问题。为了避免数据竞争,可以使用互斥锁(mutex)或条件变量(condition variable)来保护共享数据的访问。然而,这些操作通常比较重量级,会引入额外的开销。为了提高性能,可以使用原子类型。原子类型是一种特殊的数据类型,它在多线程环境中保证对其操作是原子的,即不会被其他线程打断。Notestd::atomic几乎支持所有的内置类型和指针,需要注意的是有些类型的性能并不是很好。*/std::thread myThread16(myThread_atomic, 10000);std::thread myThread17(myThread_atomic, 10000);myThread16.join();myThread17.join();std::cout << "Final number: " << number << std::endl; std::cout << "Final atomicNumber: " << atomicNumber << std::endl; /*输出结果Final number: 19930 // 多线程是同时进行且无序的,所以如果它们同时操作同一个变量,那么肯定会出错Final atomicNumber: 20000 // 原子操作是最小的且不可并行化的操作。所有即使没有加锁,也会像同步进行一样操作atomic对象,从而节省了上锁、解锁的时间消耗*/}
4. 线程控制自己
void myThread_this_thread() {//获取线程idstd::thread::id id = std::this_thread::get_id();std::cout << "thread id: " << id << std::endl;//线程休眠3秒,并且每隔1秒输出一个点std::cout << "thread woke up after 3 seconds: ";for (int i = 0; i < 3; i++) {std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "." << std::flush;}std::cout << std::endl;
}
{/*std::this_threadget_id 获取线程idyield 提示操作系统让出CPU使用权,让其他线程运行,然后等待操作系统重新调度当前线程并分配CPU使用权继续执行sleep_for 使线程休眠到指定时间sleep_until 使线程休眠到指定的时间点*/std::thread myThread16(myThread_this_thread);myThread16.join();}