在 C++11 之前,不同的操作系统平台为多线程提供了各自的标准。在 Windows 平台上,开发者可以使用 Win32 API 来创建和管理多线程;而在 Linux 和 Mac OS 等类 Unix 平台上,可以通过 POSIX 线程库提供的 API 进行多线程编程。
在 C++11 之后,引入了一个统一的跨平台线程库,即 std::thread
库,它可以方便地创建和管理多线程。这个库可以被视为对不同平台多线程 API 的一层包装。因此,使用 C++11 新标准提供的线程库编写的程序具有跨平台的特性,可以在不同操作系统上运行,而不必关心底层不同的多线程实现。
这种标准化的多线程库提供了更加统一和便利的方式来处理多线程编程,避免了因为不同操作系统而需要分别处理多线程部分的麻烦。这在实现跨平台的应用程序时非常有价值,让我们能够更专注于业务逻辑的实现。
https://zh.cppreference.com/w/cpp/thread
std::thread
类用于表示单个执行线程,它的构造函数可以构造新的 std::thread
对象并将它与执行线程关联,从而使新的执行线程开始执行。
下面是一个使用 std::thread
创建线程的简单示例:
#include
#include
using namespace std;
void thread_function() {
cout << "My pid is " << getpid() << ", my tid is " << this_thread::get_id() << "." << endl;
}
int main() {
thread my_thread(thread_function);
my_thread.join();
cout << "My pid is " << getpid() << ", my tid is " << this_thread::get_id() << "." << endl;
return 0;
}
My pid is 4848, my tid is 2.
My pid is 4848, my tid is 1.
此外,还可以通过 Lambda 函数简化线程创建:
#include
#include
using namespace std;
int main() {
thread([] {
cout << "My pid is " << getpid() << ", my tid is " << this_thread::get_id() << "." << endl;
}).join();
cout << "My pid is " << getpid() << ", my tid is " << this_thread::get_id() << "." << endl;
return 0;
}
My pid is 25228, my tid is 2.
My pid is 25228, my tid is 1.
下例则演示了这两种构造方式如何为线程函数传递参数,不过要注意的是这里的交换操作实际上是线程不安全的,这里仅为演示:
#include
#include
using namespace std;
void swap_function(int &a, int &b) {
a = a + b;
b = a - b;
a = a - b;
}
int main() {
int a = 0, b = 1;
cout << "Before swapping: a = " << a << ", b = " << b << endl;
thread t1(swap_function, std::ref(a), std::ref(b));
t1.join();
cout << "After swapping: a = " << a << ", b = " << b << endl;
thread t2([&]() {
swap_function(a, b);
});
t2.join();
cout << "After swapping: a = " << a << ", b = " << b << endl;
return 0;
}
Before swapping: a = 0, b = 1
After swapping: a = 1, b = 0
After swapping: a = 0, b = 1
std::thread
类提供了很多与线程操作相关的方法:
常用方法 | 说明 |
---|---|
bool joinable() | 检查 std::thread 对象是否标识活跃的执行线程 |
std::thread::id get_id() | 用于获取当前线程的标识符 |
void join() | 等待线程的执行完成,并阻塞当前线程,直到被等待的线程执行结束为止 |
void detach() | 用于分离线程的执行。当一个线程被分离后,该线程的执行状态就与主线程无关,主线程不再等待分离的线程执行完成 |
void sleep_for(const std::chrono::duration | 阻塞当前线程执行,至少经过指定的 sleep_duration ,由于调度或资源争议延迟,此函数实际可能阻塞长于 sleep_duration |
join()
、sleep_for()
和 get_id()
的基本使用示例:
#include
#include
using namespace std;
void thread_function() {
this_thread::sleep_for(chrono::seconds(3)); // 休眠三秒
}
int main() {
thread my_thread(thread_function);
cout << "my_thread's pid is " << getpid() << ", my_thread's tid is " << my_thread.get_id() << "." << endl;
my_thread.join(); // 主线程会在此处等待子线程执行完毕
cout << "main's pid is " << getpid() << ", main's tid is " << this_thread::get_id() << "." << endl;
return 0;
}
my_thread pid is 27072, my_thread tid is 2.
main pid is 27072, main tid is 1.
mutex
、recursive_mutex
和 shared_mutex
是三种最常用的互斥锁(Mutex),用于实现线程之间的互斥访问,以确保在多线程环境下数据的正确性和一致性。
mutex
(互斥锁):
std::mutex
是 C++11 引入的基本互斥锁类型,用于实现对临界区的互斥访问。只允许一个线程同时访问被保护的数据,其他线程需要等待锁的释放。mutex
,那么其他线程在访问同一个 mutex
时会获取失败,直到持有锁的线程解锁。recursive_mutex
(递归互斥锁):
std::recursive_mutex
与 std::mutex
类似,但允许同一个线程多次获取同一个锁而不会被死锁,类似于 Java 中的 ReentrantLock
。递归锁在同一个线程内保持一个计数器,用于记录锁被重复锁定的次数。shared_mutex
(共享锁):
std::shared_mutex
是 C++17 引入的互斥锁类型,提供了更灵活的互斥访问控制。它允许多个线程共享访问数据,互斥写入数据,类似于 Java 中的 ReadWriteLock
。这种机制可以提高读多写少的场景中的并发性能,不过 shared_mutex
不可重入。std::shared_lock
获取共享锁,允许多个线程同时获取锁。在写入数据时,需要使用 std::unique_lock
获取独占锁。在不采用 RAII 机制的情况下,mutex
、recursive_mutex
通常通过 lock
、try_lock
和 unlock
进行操作:
lock()
: 锁定互斥锁。如果互斥锁已被其他线程锁定,则当前线程将被阻塞,直到锁可用。try_lock()
: 尝试锁定互斥锁。如果互斥锁未被其他线程锁定,则当前线程将获得锁,否则返回而不阻塞。unlock()
: 解锁互斥锁,允许其他线程获得访问权限。由于 shared_mutex
同时支持共享锁和互斥锁,对应有以下五个锁操作方法:
lock()
: 获取独占锁,阻塞其他线程的读和写访问。try_lock()
: 尝试获取独占锁,成功返回则获得锁,失败返回避免阻塞。lock_shared()
: 获取共享锁,允许多个线程同时访问共享资源,但阻塞写访问。try_lock_shared()
: 尝试获取共享锁,成功返回则获得共享访问权限,失败返回避免阻塞。unlock()
: 解锁独占锁或共享锁,允许其他线程获得访问权限。不过在实际使用中,这些加锁与解锁操作通常会封装在 std::lock_guard
或 std::unique_lock
等 RAII 锁类中使用,而不会直接调用,以确保锁的自动释放。这有助于避免因为程序出现异常或提前返回而导致的锁未释放的情况。
lock_guard
、unique_lock
、shared_lock
和 scoped_lock
都是 C++ 标准库中用于管理互斥锁的 RAII(Resource Acquisition Is Initialization)类,它们的出现使得锁的管理更加简单、安全和易于维护。
lock_guard
:lock_guard
是一个简单的互斥锁包装器,它在创建时获取锁,在其作用域结束时自动释放锁。它适用于需要在作用域内持有锁的场景,一般情况下是对 mutex
和 unique_lock
使用。但不支持手动释放锁或条件变量等操作。unique_lock
:unique_lock
提供了更多的灵活性,可以在创建时不获取锁,并在需要时手动获取或释放锁。它适用于需要在不同地方获取和释放锁的场景,同时支持条件变量和超时等操作,从而实现更复杂的锁管理逻辑。shared_lock
:shared_lock
是用于管理 shared_mutex
的 RAII 类,它支持在多个线程之间共享读锁。与 unique_lock
类似,它可以在作用域内自动获取和释放锁,也支持条件变量和超时等操作。scoped_lock
:scoped_lock
是 C++17 引入的 RAII 类,用于同时获取多个互斥锁,以避免死锁。它可以在一个函数中获取多个锁,即使在异常抛出时也会自动释放锁,确保锁的一致性和安全性。这些 RAII 锁类可以有效地防止锁忘记释放、避免死锁、提高代码可读性,并简化多线程编程。在使用时,应根据具体的场景选择合适的锁类,以满足线程安全的需求。
通过 shared_lock
和 unique_lock
分别操作 shared_mutex
的共享锁和互斥锁:
#include
#include
#include
#include
using namespace std;
shared_mutex dataMutex;
int sharedData = 0;
string get_current_time() {
time_t now = time(nullptr);
char *cur_time = ctime(&now);
string ret = cur_time;
ret.erase(ret.size() - 1);
return ret;
}
void reader(const string& name) {
shared_lock<shared_mutex> lock(dataMutex);
this_thread::sleep_for(chrono::seconds(3));
cout << get_current_time() << " Thread " << name << " is reading: " << sharedData << endl;
}
void writer(const string& name) {
unique_lock<shared_mutex> lock(dataMutex);
sharedData += 10;
this_thread::sleep_for(chrono::seconds(3));
cout << get_current_time() << " Thread " << name << " has modified sharedData to: " << sharedData << endl;
}
int main() {
thread writers[2];
thread readers[5];
for (int i = 0; i < 2; ++i) {
writers[i] = thread(writer, "writer-" + to_string(i));
}
for (int i = 0; i < 5; ++i) {
readers[i] = thread(reader, "reader-" + to_string(i));
}
for (auto &writer: writers) {
writer.join();
}
for (auto &reader: readers) {
reader.join();
}
return 0;
}
Fri Sep 01 11:20:52 2023 Thread writer-0 has modified sharedData to: 10
Fri Sep 01 11:20:55 2023 Thread reader-0 is reading: 10
Fri Sep 01 11:20:58 2023 Thread writer-1 has modified sharedData to: 20
Fri Sep 01 11:21:01 2023 Thread reader-4 is reading: 20
Fri Sep 01 11:21:01 2023 Thread reader-2 is reading: 20
Fri Sep 01 11:21:01 2023 Thread reader-1 is reading: 20
Fri Sep 01 11:21:01 2023 Thread reader-3 is reading: 20
通过 scoped_lock
锁住多个互斥锁以实现从一个账户向另一个账户转账:
#include
#include
#include
using namespace std;
class BankAccount {
public:
double balance;
std::mutex mtx;
public:
explicit BankAccount(double initialBalance) : balance(initialBalance) {}
};
void transfer(BankAccount &from, BankAccount &to, double amount) {
std::scoped_lock lock(from.mtx, to.mtx);
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
std::cout << "Transferred " << amount << " from account 1 to account 2." << std::endl;
} else {
std::cout << "Insufficient balance in account 1." << std::endl;
}
}
int main() {
BankAccount account1(100.0);
BankAccount account2(200.0);
std::thread t1(transfer, std::ref(account1), std::ref(account2), 50.0);
std::thread t2(transfer, std::ref(account2), std::ref(account1), 50.0);
std::thread t3(transfer, std::ref(account2), std::ref(account1), 100.0);
t1.join();
t2.join();
t3.join();
std::cout << "Account 1 balance: " << account1.balance << std::endl;
std::cout << "Account 2 balance: " << account2.balance << std::endl;
return 0;
}
Transferred 50 from account 1 to account 2.
Transferred 50 from account 1 to account 2.
Transferred 100 from account 1 to account 2.
Account 1 balance: 200
Account 2 balance: 100
condition_variable
类能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable
。与 POSIX 线程库中的 pthread_cond_t
类似,条件变量必须一个互斥锁配套使用。一个线程调用 wait()
后会自动阻塞,并释放自己所持有的互斥锁。另一个线程获得此锁,当条件改变时,它会发信号给关联的条件变量,释放互斥锁,通知一个或多个等待它的线程,这些线程将重新锁定互斥锁并重新测试条件是否满足。
等待和通知操作主要通过 wait()
以及 notify_one()
和 notify_all()
实现。
wait()
主要有以下两种调用方式:
void wait(std::unique_lock<std::mutex>& lock);
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
前一种方式会原子地解锁互斥锁,阻塞当前线程,并将它添加到 *this
上等待的线程列表。线程将在执行 notify_all()
或 notify_one()
时被解除阻塞。解阻塞时,无关乎原因,会再次锁定互斥锁同时 wait()
退出。后一种方式等价于 while (!pred()) { wait(lock); }
,会一直等待知道条件满足。
通过互斥锁和条件变量实现同步的简单示例:
#include
#include
#include
#include
std::mutex mtx;
std::condition_variable cond;
int sum = 0;
void thread_add_fun() {
while (true) {
{
std::unique_lock<std::mutex> lock(mtx);
sum += 1;
std::cout << "[add] sum = " << sum << ".\n";
if (sum == 3) {
/* 完成自加任务后通知show线程并把锁释放,然后退出 */
cond.notify_one();
break;
}
}
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void thread_show_fun() {
std::unique_lock<std::mutex> lock(mtx);
std::cout << "[show][NO] sum = " << sum << ".\n";
/* 通过条件变量等待直到sum到达3 */
cond.wait(lock, [] { return sum == 3; });
/* 满足条件后输出并把锁释放 */
std::cout << "[show][YES] sum = " << sum << ".\n";
}
int main() {
std::thread thread_show(thread_show_fun);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 让show线程优先执行
std::thread thread_add(thread_add_fun);
thread_add.join();
thread_show.join();
return 0;
}
[show][NO] sum = 0.
[add] sum = 1.
[add] sum = 2.
[add] sum = 3.
[show][YES] sum = 3.
C++ 中的 future
是一种用于获取异步操作结果的机制。它是 C++11 引入的一部分,位于
头文件中。future
允许在线程中执行一个异步任务,并在需要的时候获取任务的结果,而不需要显式地进行线程同步。
以下是 future
的一些基本概念和用法:
async
函数启动一个异步任务,它会返回一个 future
对象,用于获取异步任务的结果。future
的 get
方法可以获取异步任务的结果。这个方法会阻塞当前线程,直到异步任务完成并返回结果。如果异步任务抛出异常,异常会在调用 get
方法时被传播。get
方法外,你还可以使用 wait
和 wait_for
方法来等待异步任务的完成。wait
方法会一直阻塞当前线程,直到异步任务完成。wait_for
方法允许你指定一个时间段,在指定时间内等待异步任务的完成,如果超时仍未完成,它会返回一个标志来指示是否超时。future
和异步任务之间共享一种状态,这个状态用于保存异步任务的结果。通过 promise
和 shared_future
可以在多个线程之间共享这个状态。promise
是一个用于设置异步任务结果的对象。它通常由一个线程设置异步任务的结果,并与一个 future
对象关联,以允许其他线程获取结果。可以通过 promise
的 get_future
方法获取一个关联的 future
对象,用于在其他线程中获取异步任务的结果。异步任务执行的线程可以通过 set_value
或 set_exception
方法设置 promise
对象的结果或异常。这将影响与 promise
关联的 future
对象的行为。
下面的例子演示了如何通过 promise
在不同线程中共享异步任务的结果:
#include
#include
#include
void asyncTask(std::promise<int>& p) {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时操作
p.set_value(42); // 设置结果
}
int main() {
std::promise<int> promiseResult;
std::future<int> futureResult = promiseResult.get_future();
std::thread asyncThread(asyncTask, std::ref(promiseResult));
std::cout << "Waiting for the result..." << std::endl;
int result = futureResult.get(); // 获取异步任务的结果
asyncThread.join();
std::cout << "Result: " << result << std::endl;
return 0;
}
Waiting for the result...
Result: 42
基于 C++11 的线程池简单实现:
#ifndef THREAD_POOL_HPP
#define THREAD_POOL_HPP
#include
#include
#include
#include
#include
#define THREAD_MAX_NUM 3 // 线程池最大线程数
using namespace std;
class ThreadPool {
private:
bool m_open_flag; // 表示线程池运行标志,用于线程回收
int m_thread_count; // 线程池中剩余线程数,用于检测线程退出
mutex m_mutex_run; // 用于线程运行时互斥,主要是互斥操作任务队列
mutex m_mutex_quit; // 用于线程退出时互斥
condition_variable m_cond_run; // 用于线程间运行时同步
condition_variable m_cond_quit; // 用于线程间退出时同步
queue<function<void()>> m_task_queue; // 线程池任务队列
public:
explicit ThreadPool(int thread_count = THREAD_MAX_NUM) {
assert(thread_count > 0);
m_open_flag = true;
m_thread_count = thread_count;
for (int i = 0; i < thread_count; i++) {
/* 通过匿名函数依次创建子线程 */
thread([this] {
unique_lock<mutex> locker(m_mutex_run); // 互斥操作任务队列
while (true) {
if (!m_task_queue.empty()) {
auto task = std::move(m_task_queue.front()); // 通过move将任务函数转换为右值引用,提高传递效率
m_task_queue.pop();
locker.unlock(); // 把锁释放,避免自己的业务处理影响其他线程的正常执行
task(); // 执行任务队列中的任务函数
locker.lock(); // 重新获取锁
} else if (!m_open_flag) {
m_thread_count--;
if (m_thread_count == 0) {
m_cond_quit.notify_one(); // 所有子线程均已退出,通知主线程退出
}
break;
} else {
m_cond_run.wait(locker); // 阻塞等待 m_mutex_run
}
}
}).detach();
}
}
~ThreadPool() {
{
unique_lock<mutex> locker(m_mutex_run); // 互斥操作m_open_flag
m_open_flag = false;
}
m_cond_run.notify_all(); // 通知线程队列中的所有子线程退出
{
unique_lock<mutex> locker(m_mutex_quit);
m_cond_quit.wait(locker); // 阻塞等待m_mutex_quit,会由最后一个退出的子线程通知
}
}
template<class T>
void addTask(T &&task) {
{
unique_lock<mutex> locker(m_mutex_run); // 互斥操作任务队列
m_task_queue.emplace(std::forward<T>(task)); // 通过完美转发传递任务函数
}
m_cond_run.notify_one(); // 通知线程队列中的首子线程执行任务
}
};
void threadFun(int x) {
cout << x << endl;
}
int main() {
ThreadPool thread_pool;
for (int i = 0; i < 10; i++) {
thread_pool.addTask(bind(threadFun, i)); // 通过bind绑定参数
}
}
#endif
atreus@MacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
0
3
4
5
6
7
8
9
2
1
atreus@MacBook-Pro %