START
我们都知道自C++11开始引入了线程相关的好多东西。本节我们来学习C++中线程相关的知识点。
std::thread
是 C++ 标准库中提供的用于创建和管理线程的类。通过 std::thread
,可以方便地创建新线程,并在其中执行指定的函数或可调用对象。
以下是 std::thread
的一些重要特点和用法:
std::thread
可以创建新的线程。可以将函数或可调用对象作为参数传递给 std::thread
构造函数,以在新线程中执行该函数或可调用对象。std::thread
对象代表一个线程,可以通过该对象来管理线程的状态和行为,如启动线程、等待线程结束、查询线程 ID 等。std::thread
可以与其他同步原语(如互斥量、条件变量等)一起使用,实现线程间的同步和通信。std::thread
支持移动语义,可以通过 std::move
函数将一个 std::thread
对象的所有权转移给另一个对象。这意味着可以将线程对象作为参数传递给函数或存储在容器中。std::thread
对象被销毁时,它代表的线程也会被销毁。通常情况下,如果一个 std::thread
对象代表的线程还在运行,会调用 std::terminate
终止程序;如果线程已经结束,会释放线程的资源。以下是一个示例,展示了如何使用 std::thread
创建新线程并执行函数:
#include <iostream>
#include <thread>
// 线程函数,打印消息
void printMessage() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// 创建新线程并执行 printMessage 函数
std::thread t(printMessage);
// 主线程继续执行其他操作
std::cout << "Hello from main!" << std::endl;
// 等待子线程执行完毕
t.join();
return 0;
}
在这个示例中,我们通过 std::thread
创建了一个新线程,并将 printMessage
函数作为参数传递给 std::thread
构造函数。在主线程中,我们打印了一条消息,并通过 join
函数等待子线程执行完毕。
通过使用 std::thread
,我们可以方便地进行多线程编程,并实现并行执行任务的目的。需要注意的是,在使用 std::thread
时,要确保线程的正确同步和管理,以避免竞态条件和死锁等问题。
std::mutex
是 C++ 标准库中提供的互斥量类,用于实现线程之间的互斥访问。互斥量是一种同步原语,用于保护共享资源,确保在任意时刻只有一个线程能够访问该资源,从而避免竞态条件和数据竞争。
以下是 std::mutex
的一些重要特点和用法:
std::mutex
对象代表一个互斥锁,可以用于保护共享资源。在访问共享资源之前,线程可以使用 std::mutex
对象进行加锁操作,以确保只有一个线程能够访问共享资源。std::mutex
的 lock()
和 unlock()
方法可以分别对互斥锁进行加锁和解锁操作。当一个线程对互斥锁进行加锁后,其他线程将无法对同一个互斥锁进行加锁,直到持有该互斥锁的线程将其解锁。std::lock_guard
、std::unique_lock
等 RAII 包装类来自动管理互斥锁的加锁和解锁操作,避免忘记手动解锁导致死锁等问题。std::mutex
是线程安全的,可以被多个线程同时访问和操作。它提供了基本的互斥保护,但不提供超时、递归锁等高级功能。以下是一个示例,展示了如何使用 std::mutex
进行线程间的同步:
#include <iostream>
#include <thread>
#include <mutex>
// 共享资源
int counter = 0;
std::mutex mtx; // 互斥锁
// 线程函数,增加 counter 的值
void incrementCounter() {
// 使用 RAII 机制加锁
std::lock_guard<std::mutex> lock(mtx);
// 访问共享资源
counter++;
}
int main() {
// 创建多个线程,并执行 incrementCounter 函数
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
// 等待线程执行完毕
t1.join();
t2.join();
// 输出结果
std::cout << "Counter value: " << counter << std::endl;
return 0;
}
在这个示例中,我们创建了一个共享资源 counter
,并用 std::mutex
对象 mtx
来保护它。在 incrementCounter
函数中,我们使用 std::lock_guard
类对互斥锁进行加锁操作,以确保在访问共享资源时只有一个线程能够访问。通过使用 std::mutex
,我们可以避免多线程访问共享资源时发生数据竞争的问题。
std::lock
是 C++11 标准中提供的函数模板,用于在一次操作中对多个互斥量进行加锁操作,以避免死锁和提高程序性能。
std::lock
函数的作用是将多个互斥量对象进行加锁,如果其中任何一个互斥量对象无法加锁(即已被其他线程锁定),则 std::lock
函数会阻塞当前线程,直到所有互斥量对象都被成功加锁。
以下是 std::lock
的一些重要特点和用法:
std::lock
函数会原子性地对多个互斥量对象进行加锁操作。这意味着要么所有互斥量都成功加锁,要么所有互斥量都不被加锁,不会出现部分加锁的情况。std::lock
函数可以避免死锁的发生。当多个线程需要同时访问多个共享资源时,使用 std::lock
可以确保线程以相同的顺序对互斥量进行加锁,从而避免死锁的发生。std::lock
函数采用一种高效的算法来对多个互斥量进行加锁操作,因此可以提高程序的性能。相比于分别对每个互斥量进行加锁,使用 std::lock
可以减少线程间的竞争,降低锁的粒度,提高并发性能。std::lock
函数支持可变数量的参数,可以同时对任意数量的互斥量进行加锁。参数可以是互斥量对象,也可以是指向互斥量对象的指针。以下是一个示例,展示了如何使用 std::lock
对多个互斥量进行加锁操作:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void func1() {
// 使用 std::lock 对两个互斥量进行加锁
std::lock(mtx1, mtx2);
// 此处对共享资源进行访问
// 解锁两个互斥量
mtx1.unlock();
mtx2.unlock();
}
void func2() {
// 使用 std::lock 对两个互斥量进行加锁
std::lock(mtx1, mtx2);
// 此处对共享资源进行访问
// 解锁两个互斥量
mtx1.unlock();
mtx2.unlock();
}
int main() {
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
return 0;
}
在这个示例中,我们创建了两个互斥量对象 mtx1
和 mtx2
,并在两个线程的函数中使用 std::lock
对它们进行加锁操作。std::lock
会确保在一个操作中对两个互斥量进行加锁,避免死锁的发生。完成共享资源的访问后,我们分别对两个互斥量进行解锁操作。
std::atomic
是 C++ 标准库中提供的原子类型,用于实现多线程环境下的原子操作。原子操作是不可分割的操作,可以保证在多线程环境下对共享变量的读写操作是线程安全的,即不会发生数据竞争和数据不一致的情况。
std::atomic
提供了一系列原子操作函数,用于对原子类型的对象进行读写、赋值、递增、递减、交换等操作,这些操作是原子的,即线程安全的,不会被其他线程中断。
以下是 std::atomic
的一些重要特点和用法:
std::atomic
可以用于创建原子类型的对象,包括原子整型、原子指针等。原子类型的对象具有原子性,可以在多线程环境下安全地进行读写操作。std::atomic
提供了一系列原子操作函数,如 load()
、store()
、exchange()
、fetch_add()
、fetch_sub()
等,用于对原子类型的对象进行读写、赋值、交换、递增、递减等操作。这些操作是原子的,不会被其他线程中断。std::atomic
支持不同的内存顺序模型,包括 memory_order_relaxed
、memory_order_acquire
、memory_order_release
、memory_order_acq_rel
、memory_order_seq_cst
等。通过指定不同的内存顺序,可以控制原子操作的执行顺序和可见性。std::atomic_flag
是 std::atomic
的特殊类型,用于实现原子的布尔类型。它通常用于实现简单的互斥锁,具有较低的开销和较高的性能。以下是一个示例,展示了如何使用 std::atomic
进行原子操作:
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> counter(0); // 原子整型对象
void incrementCounter() {
for (int i = 0; i < 10000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed); // 原子递增操作
}
}
int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
std::cout << "Counter value: " << counter.load(std::memory_order_relaxed) << std::endl;
return 0;
}
在这个示例中,我们创建了一个原子整型对象 counter
,并用两个线程同时对其进行递增操作。通过使用 std::atomic
提供的原子操作函数 fetch_add()
,可以保证对 counter
的递增操作是线程安全的。最后,我们使用 load()
函数读取 counter
的值,确保在输出时能够得到正确的结果。
std::call_once
是 C++ 标准库中提供的用于执行只调用一次的函数的函数模板。它可以确保在多线程环境下,某个函数只被调用一次,即使在多个线程中同时调用 std::call_once
。
std::call_once
的一般形式如下:
template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );
它接受一个 std::once_flag
类型的引用 flag
和一个可调用对象 f
,以及可选的参数列表 args
。std::once_flag
是一个用于标记是否已经执行过某个函数的标志。
以下是 std::call_once
的一些重要特点和用法:
std::call_once
确保传递给它的可调用对象 f
只被执行一次,即使在多个线程中同时调用 std::call_once
。std::call_once
是线程安全的,它使用 std::once_flag
来确保在多线程环境下只执行一次。std::call_once
常用于延迟初始化某些资源,确保初始化操作只执行一次,避免竞态条件和资源浪费。std::call_once
的可调用对象 f
抛出异常,则 std::call_once
会将异常传递给调用者,而且在下一次调用 std::call_once
时仍然会执行 f
。以下是一个示例,展示了如何使用 std::call_once
进行延迟初始化:
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
int value;
void init_value() {
std::cout << "Initializing value..." << std::endl;
value = 42;
}
void get_value() {
std::call_once(flag, init_value);
std::cout << "Value is: " << value << std::endl;
}
int main() {
std::thread t1(get_value);
std::thread t2(get_value);
t1.join();
t2.join();
return 0;
}
在这个示例中,我们使用了 std::call_once
来确保 init_value
函数只被执行一次,即使在多个线程中同时调用 get_value
函数。这样可以避免对 value
的多次初始化。在第一次调用 get_value
时,init_value
函数会被执行以初始化 value
,而在后续的调用中,init_value
不会再次执行,而是直接获取已经初始化过的 value
值。
在 C++ 中,volatile
是一个关键字,用于告诉编译器对某个变量进行特殊处理,以确保对该变量的读写操作不会被优化器优化掉。volatile
关键字通常用于标识那些可能会被意外修改的变量,比如硬件寄存器、中断服务程序中的共享变量等。
以下是 volatile
关键字的一些特性和用法:
volatile
告诉编译器对变量的读写操作不能被优化掉,即使这些操作看起来是多余的或者在代码的执行流程中不是必需的。volatile
也可以防止编译器和 CPU 对变量的读写操作进行重排序。这对于多线程编程和与硬件交互的程序很重要,因为这些场景下的操作顺序可能是关键的。volatile
变量的读写操作会在编译器层面插入内存屏障,以确保对该变量的操作在多线程环境下是按照顺序进行的。volatile
关键字并不保证对变量的操作是原子的。如果需要原子操作,请使用 std::atomic
类型。volatile
可以防止编译器的优化,但它并不提供线程同步的机制。在多线程编程中,应该使用互斥量、原子类型等专门的同步机制来保证线程安全。下面是一个简单的示例,演示了 volatile
的用法:
#include <iostream>
volatile int globalVar = 0;
void foo() {
while (globalVar == 0) {
// do something
}
}
void bar() {
globalVar = 1;
}
int main() {
std::thread t1(foo);
std::thread t2(bar);
t1.join();
t2.join();
return 0;
}
在这个示例中,globalVar
被声明为 volatile int
,这告诉编译器对它的读写操作不能被优化掉。因此,foo
函数中的循环会一直等待 globalVar
的值被改变,即使在 bar
函数中修改了它的值。
std::condition_variable
是 C++ 标准库中提供的用于线程间同步的条件变量类。它配合 std::mutex
使用,用于在多线程环境中实现线程的等待和唤醒机制,允许线程在某个特定条件下进行等待,直到其他线程满足条件后进行唤醒。
以下是 std::condition_variable
的一些重要特点和用法:
std::condition_variable
允许线程在某个特定条件下进行等待,并在条件满足时进行唤醒。它与 std::mutex
配合使用,用于实现线程间的同步和通信。wait()
函数在条件变量上等待,当其他线程调用 notify_one()
或 notify_all()
函数时,等待的线程将被唤醒。notify_one()
用于唤醒单个等待线程,而 notify_all()
用于唤醒所有等待线程。wait()
函数时,需要传入一个已经加锁的 std::unique_lock<std::mutex>
对象,以确保在等待期间对共享资源的访问是线程安全的。等待期间,std::mutex
会自动释放,允许其他线程对共享资源进行访问。std::condition_variable
还支持超时等待的功能,可以指定等待的最长时间。如果超过指定的时间仍然没有被唤醒,等待函数会返回,线程可以继续执行其他操作。以下是一个简单的示例,演示了 std::condition_variable
的基本用法:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; });
std::cout << "Worker thread is awake." << std::endl;
}
int main() {
std::thread t(worker);
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::seconds(2));
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one();
t.join();
return 0;
}
在这个示例中,主线程创建了一个工作线程 t
,然后在一段时间后唤醒了该工作线程。工作线程在 cv.wait()
中等待条件变量 ready
为 true
,一旦主线程修改了 ready
的值并调用了 cv.notify_one()
,工作线程将被唤醒并继续执行。
std::future
是 C++ 标准库中提供的用于异步任务的类,它用于获取异步操作的结果,或者等待异步操作的完成。std::future
表示一个可能会在将来完成的操作的结果,允许程序在等待异步操作完成时继续执行其他任务。
以下是 std::future
的一些重要特点和用法:
std::future
可以用于表示一个异步操作的结果,允许程序在等待操作完成时继续执行其他任务。异步操作可以通过 std::async
、std::packaged_task
、std::promise
等方式创建。get()
函数获取异步操作的结果。如果异步操作尚未完成,调用 get()
函数将会阻塞当前线程,直到异步操作完成并返回结果。wait()
函数等待异步操作完成。wait_for()
和 wait_until()
函数可以用于等待一段时间或者直到特定时间点。std::future
将会保存该异常,并在调用 get()
函数时重新抛出异常。可以使用 std::future::exception()
函数获取异常信息。std::future
和其相关的类(如 std::promise
)共享一个状态,用于表示异步操作的结果。异步操作完成后,std::future
将保存该结果,并提供给调用者。以下是一个简单的示例,演示了如何使用 std::future
获取异步操作的结果:
#include <iostream>
#include <future>
#include <chrono>
int calculate() {
// 模拟一个耗时操作
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
}
int main() {
// 创建一个异步任务,并获取其 future 对象
std::future<int> result = std::async(std::launch::async, calculate);
// 执行其他任务...
std::cout << "Waiting for result..." << std::endl;
// 等待异步操作完成,并获取结果
int value = result.get();
std::cout << "Result: " << value << std::endl;
return 0;
}
在这个示例中,我们使用 std::async
创建了一个异步任务,并将其结果保存在 std::future
对象 result
中。然后,我们执行其他任务,并调用 result.get()
等待异步操作完成并获取结果。一旦异步操作完成,我们就可以从 result
中获取到异步操作的结果。
std::async
是 C++ 标准库中提供的用于创建异步任务的函数,用于启动一个新的线程或者在线程池中执行指定的任务,并返回一个 std::future
对象,用于获取异步操作的结果。
以下是 std::async
的一些重要特点和用法:
std::async
可以用于创建异步任务,执行指定的函数或可调用对象,并返回一个 std::future
对象,用于获取任务的结果。std::async
支持三种执行策略:std::launch::async
、std::launch::deferred
和默认策略。std::launch::async
策略表示在新线程或者线程池中执行任务,std::launch::deferred
策略表示延迟执行任务直到调用 get()
函数时,而默认策略由编译器决定。std::async
返回一个 std::future
对象,用于获取异步任务的结果。通过 std::future
对象的 get()
函数可以获取任务的结果,该函数会阻塞当前线程直到任务完成并返回结果。std::future
对象将会保存该异常,调用 get()
函数时会重新抛出异常。可以通过 std::future::exception()
函数获取异常信息。std::future::wait()
函数等待异步任务完成,也可以通过 std::future::wait_for()
和 std::future::wait_until()
函数等待一段时间或者直到特定时间点。以下是一个简单的示例,演示了如何使用 std::async
创建异步任务并获取结果:
#include <iostream>
#include <future>
#include <chrono>
int calculate() {
// 模拟一个耗时操作
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
}
int main() {
// 创建一个异步任务,并获取其 future 对象
std::future<int> result = std::async(std::launch::async, calculate);
// 执行其他任务...
std::cout << "Waiting for result..." << std::endl;
// 等待异步操作完成,并获取结果
int value = result.get();
std::cout << "Result: " << value << std::endl;
return 0;
}
在这个示例中,我们使用 std::async
创建了一个异步任务,并将其结果保存在 std::future
对象 result
中。然后,我们执行其他任务,并调用 result.get()
等待异步操作完成并获取结果。一旦异步操作完成,我们就可以从 result
中获取到异步操作的结果。