Hi,大家好!今天我们来学习一下C++中线程相关的所有知识点。
std::thread
std::thread
是 C++11 标准库中用于创建和管理线程的类,它提供了一种简单的方式来启动新的线程并执行指定的函数或可调用对象。
下面是 std::thread
的主要特点和用法:
-
创建线程:使用
std::thread
类可以创建新的线程,并指定要执行的函数或可调用对象。 -
并发执行:通过创建多个
std::thread
对象,可以实现多线程并发执行,从而提高程序的性能。 -
参数传递:可以将参数传递给线程的执行函数,以便在线程中使用。
-
线程管理:
std::thread
对象可以用于管理线程的生命周期,包括启动线程、等待线程执行完成、加入线程、分离线程等操作。
下面是 std::thread
的基本用法示例:
#include <iostream>#include <thread>
// 执行函数void hello() { std::cout << "Hello, from thread!" << std::endl;}int main() {
// 创建新线程并执行 hello 函数 std::thread t(hello);
// 主线程继续执行其他任务 std::cout << "Hello, from main thread!" << std::endl;
// 等待子线程执行完成 t.join(); return 0;}
以上我们创建了一个新的线程 t
,并将 hello
函数作为线程的执行函数。主线程继续执行其他任务,然后调用 t.join()
来等待子线程执行完成。最终输出结果会在子线程和主线程执行完成后一起打印。
要注意的是,在实际开发中,需要注意线程的安全性和正确性,尤其是共享资源的访问问题。使用互斥锁、条件变量等机制可以有效地保护共享资源,避免多线程并发访问导致的问题。
std::mutex
std::mutex
是 C++11 标准库中用于实现互斥锁的类,它提供了一种线程同步的机制,用于保护共享资源的访问,防止多个线程同时访问造成的数据竞争和不一致性。
下面是 std::mutex
的主要特点和用法:
-
互斥锁:
std::mutex
提供了一种互斥锁的机制,可以确保在任意时刻只有一个线程可以访问被保护的共享资源。 -
线程安全:通过加锁和解锁操作,可以确保对共享资源的访问是线程安全的,不会发生数据竞争。
-
RAII(资源获取即初始化):通常使用 RAII 技术来管理
std::mutex
对象的生命周期,即在作用域内创建std::mutex
对象并加锁,当作用域结束时自动释放锁。 -
锁的类型:除了
std::mutex
,C++11 还提供了其他类型的互斥锁,如std::recursive_mutex
、std::timed_mutex
、std::recursive_timed_mutex
等,用于满足不同的需求。
下面是一个使用 std::mutex
的基本示例:
#include <iostream>#include <thread>
#include <mutex>std::mutex mtx;void print_hello(int id) { mtx.lock();
// 加锁 std::cout << "Hello, from thread " << id << std::endl; mtx.unlock();
// 锁}int main() { std::thread t1(print_hello, 1); std::thread t2(print_hello, 2); t1.join(); t2.join(); return 0;}
在这个示例中,我们定义了一个全局的 std::mutex
对象 mtx
,并在 print_hello
函数中使用 mtx.lock()
和 mtx.unlock()
分别对临界区进行加锁和解锁操作,保护了对 std::cout
的访问,避免了多线程并发访问的问题。
std::lock
std::lock
是 C++11 标准库中提供的一个函数模板,用于同时对多个互斥锁进行加锁,以避免发生死锁。std::lock
函数可以同时对多个互斥锁进行加锁,如果无法获得所有锁,则会自动释放已经获得的锁,避免了死锁的发生。
std::lock
的语法形式如下:
template <class Lockable1, class Lockable2, class... LockableN>
void lock(Lockable1& lock1, Lockable2& lock2, LockableN&... lockn);
其中 Lockable1
、Lockable2
、LockableN
分别表示互斥锁的类型,函数接受任意数量的互斥锁作为参数,并依次对它们进行加锁操作。
下面是一个示例代码,演示了如何使用 std::lock
函数同时对多个互斥锁进行加锁:
#include <iostream>#include <thread>#include <mutex>std::mutex mtx1;std::mutex mtx2;void foo() {
// 同时对多个互斥锁进行加锁 std::lock(mtx1, mtx2); std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
// 使用 adopt_lock 参数表示已经获得锁 std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 在临界区内执行操作 std::cout << "Critical section in foo" << std::endl;}void bar() {
// 同时对多个互斥锁进行加锁 std::lock(mtx1, mtx2); std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
// 使用 adopt_lock 参数表示已经获得锁 std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 在临界区内执行操作 std::cout << "Critical section in bar" << std::endl;}int main() { std::thread t1(foo); std::thread t2(bar); t1.join(); t2.join(); return 0;}
在这个示例中,我们定义了两个全局的互斥锁 mtx1
和 mtx2
,然后在两个线程的函数中使用 std::lock
函数同时对它们进行加锁。由于 std::lock
函数可以同时对多个互斥锁进行加锁,并且会自动释放已经获得的锁,因此可以避免发生死锁。
std::atomic
std::atomic
是 C++11 标准库中引入的用于原子操作的模板类,它提供了一种线程安全的方式来操作共享变量,避免了数据竞争和不一致性问题。std::atomic
类模板支持原子的读-修改-写操作,可以确保操作的完整性和一致性。
下面是 std::atomic
的主要特点和用法:
-
原子操作:
std::atomic
支持一系列的原子操作,包括读取、写入、比较交换、加法、减法、按位与、按位或等操作,这些操作保证了对共享变量的操作的原子性。 -
线程安全:
std::atomic
提供了一种线程安全的方式来访问共享变量,避免了多个线程同时对同一个变量进行操作造成的数据竞争和不一致性问题。 -
内存顺序:
std::atomic
支持指定内存顺序(memory order),通过指定内存顺序可以控制操作的内存可见性和执行顺序,包括memory_order_relaxed
、memory_order_acquire
、memory_order_release
、memory_order_acq_rel
、memory_order_seq_cst
等。 -
原子类型:
std::atomic
可以用于任意类型的原子操作,包括整数、指针、自定义类型等。
下面是一个简单的示例代码,演示了如何使用 std::atomic
进行原子操作:
#include <iostream>#include <atomic>#include
<thread>std::atomic<int> counter(0);void increment() { for (int i = 0; i < 1000000; ++i) { counter.fetch_add(1, std::memory_order_relaxed);
// 原子的加法操作 }}int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Final counter value: " << counter.load(std::memory_order_relaxed) << std::endl; return 0;}
在这个示例中,我们定义了一个原子类型的计数器 counter
,然后在两个线程中并行地对其进行原子的加法操作,最后输出计数器的最终值。由于原子操作的特性,不会发生数据竞争,最终输出的计数器值是正确的。
std::call_once
std::call_once
是 C++11 标准库中提供的一个函数,用于确保某个函数只被调用一次,即使在多线程环境下也能保证线程安全。通常情况下,std::call_once
用于在多线程环境下执行初始化工作,以保证全局资源的初始化只进行一次。
std::call_once
的语法如下:
template <class Callable, class... Args>void call_once(std::once_flag& flag, Callable&& func, Args&&... args);
其中:
flag
:是一个标志对象,用于标识函数是否已经被调用过。func
:是要调用的函数或可调用对象。args
:是传递给函数的参数。
std::call_once
函数会检查 flag
是否已经被设置过,如果没有,则调用 func
函数,并设置 flag
为已调用状态。在多线程环境下,多个线程同时调用 std::call_once
,但只有一个线程会执行 func
函数,其他线程会被阻塞直到第一个线程执行完成。
下面是一个示例代码,演示了如何使用 std::call_once
来确保全局资源的初始化只进行一次:
#include <iostream>#include <thread>#include <mutex>std::once_flag flag;int global_data = 0;void init_global_data() { global_data = 42; std::cout << "Global data initialized" << std::endl;}void use_global_data() { std::call_once(flag, init_global_data);
// 只有第一个调用会执行 init_global_data 函数 std::cout << "Global data used: " << global_data << std::endl;}int main() { std::thread t1(use_global_data); std::thread t2(use_global_data); std::thread t3(use_global_data); t1.join(); t2.join(); t3.join(); return 0;}
在这个示例中,我们定义了一个全局的 std::once_flag
对象 flag
和一个全局的整型变量 global_data
,然后在 use_global_data
函数中使用 std::call_once
来确保 init_global_data
函数只被调用一次。即使多个线程同时调用 use_global_data
函数,但只有一个线程会执行 init_global_data
函数,其他线程会被阻塞,直到第一个线程执行完成。
std::condition_variable
std::condition_variable
是 C++11 标准库中提供的一个条件变量类,用于在多线程编程中实现线程之间的同步。它允许一个或多个线程在某个条件成立时被唤醒,并在条件不满足时等待。通常情况下,std::condition_variable
配合 std::mutex
使用,以实现线程间的等待和通知机制。
下面是 std::condition_variable
的主要特点和用法:
-
条件变量:
std::condition_variable
提供了一种条件变量的机制,用于在条件满足时唤醒等待线程,条件不满足时等待。 -
等待和通知:等待线程可以通过
wait()
函数在条件不满足时进入等待状态,而唤醒线程可以通过notify_one()
或notify_all()
函数来唤醒等待的线程。 -
线程安全:
std::condition_variable
配合std::mutex
使用,可以确保线程间的等待和唤醒操作是线程安全的。 -
超时等待:
wait_for()
和wait_until()
函数允许线程在一定时间内等待条件满足,超时后自动返回。
下面是一个简单的示例代码,演示了如何使用 std::condition_variable
实现线程间的等待和唤醒:
#include <iostream>#include <thread>#include <mutex>#include <condition_variable>std::mutex mtx;std::condition_variable cv;bool ready = false;void worker_thread() { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return ready; });
// 等待 ready 变为 true std::cout << "Worker thread is processing" << std::endl;}int main() { std::thread t(worker_thread); std::this_thread::sleep_for(std::chrono::seconds(2));
// 主线程等待 2 秒 { std::lock_guard<std::mutex> lock(mtx); ready = true;
// 设置 ready 为 true } cv.notify_one();
// 唤醒等待的线程 t.join(); return 0;}
在这个示例中,我们定义了一个条件变量 cv
和一个布尔变量 ready
,线程 worker_thread
在条件变量 cv
上等待,直到 ready
变为 true。主线程等待 2 秒后,设置 ready
为 true,并通过 notify_one()
函数唤醒等待的线程。
需要注意的是,cv.wait()
函数的第一个参数是一个 std::unique_lock<std::mutex>
对象,用于锁定互斥量,确保在等待条件期间其他线程无法修改条件。第二个参数是一个 lambda 表达式,用于检查条件是否满足。
std::future
std::future
是 C++11 标准库中提供的一个类模板,用于表示异步操作的结果或状态,通常与 std::async
、std::packaged_task
、std::promise
等异步操作相关的类一起使用。通过 std::future
,可以轻松地获取异步操作的结果,并在需要时等待异步操作的完成。
下面是 std::future
的主要特点和用法:
-
表示异步操作的结果:
std::future
用于表示异步操作的结果,可以通过它获取异步操作的结果或状态。 -
等待异步操作完成:可以通过
std::future
的成员函数get()
来等待异步操作的完成,并获取其结果。如果异步操作尚未完成,get()
函数会阻塞当前线程,直到异步操作完成为止。 -
检查异步操作状态:可以通过
std::future
的成员函数valid()
来检查与之关联的异步操作是否有效,以及wait_for()
和wait_until()
函数来检查异步操作的状态和等待一段时间。 -
异步任务的共享:
std::future
可以通过std::shared_future
来实现多个线程共享同一个异步操作的结果。
下面是一个简单的示例代码,演示了如何使用 std::future
获取异步操作的结果:
#include <iostream>#include <future>#include <chrono>int foo() { std::this_thread::sleep_for(std::chrono::seconds(2)); return 42;}int main() {
// 启动一个异步操作,并获取其 future std::future<int> fut = std::async(std::launch::async, foo); std::cout << "Waiting for result..." << std::endl;
// 等待异步操作完成,并获取其结果 int result = fut.get(); std::cout << "Result: " << result << std::endl; return 0;}
在这个示例中,我们通过 std::async
启动一个异步操作 foo
,然后通过 std::future
对象 fut
获取异步操作的结果。在主线程中调用 fut.get()
等待异步操作完成,并获取其结果,然后打印出结果。
std::async
std::async
是 C++11 标准库中提供的一个函数模板,用于创建异步任务并获取与之关联的 std::future
对象,以便在需要时获取异步任务的结果。它是实现异步编程的一种方便方式,能够在多线程环境下执行函数,并且可以方便地获取函数执行的结果。
下面是 std::async
的主要特点和用法:
-
创建异步任务:
std::async
函数用于创建一个异步任务,该任务会在后台线程中执行指定的函数,并返回一个与之关联的std::future
对象,用于获取异步任务的结果。 -
函数执行方式:默认情况下,
std::async
函数会以异步的方式执行指定的函数,即函数会在后台线程中执行。但也可以通过std::launch
参数指定函数的执行方式,包括异步执行 (std::launch::async
) 和延迟执行 (std::launch::deferred
)。 -
获取异步任务结果:通过与
std::future
对象关联,可以在需要时获取异步任务的执行结果。调用std::future
对象的get()
方法可以阻塞当前线程,直到异步任务执行完成并返回结果。 -
异常处理:如果异步任务中抛出了异常,
std::future
对象的get()
方法会重新抛出异常,从而允许在调用方处理异常。
下面是一个简单的示例代码,演示了如何使用 std::async
创建异步任务并获取其结果:
#include <iostream>#include <future>
#include <chrono>int foo() { std::this_thread::sleep_for(std::chrono::seconds(2)); return 42;}int main() { // 创建一个异步任务,并获取与之关联的 std::future 对象 std::future<int> fut = std::async(std::launch::async, foo); std::cout << "Waiting for result..." << std::endl;
// 获取异步任务的结果 int result = fut.get(); std::cout << "Result: " << result << std::endl; return 0;}
在这个示例中,我们通过 std::async
函数创建了一个异步任务,指定了函数 foo
作为要执行的任务,并通过 std::launch::async
参数指定了异步执行方式。然后,我们通过调用 fut.get()
方法获取异步任务的结果,该方法会阻塞当前线程,直到异步任务执行完成并返回结果。