C++ 并发编程学习笔记
目录
一. 基本接口
二. 初步了解多线程
三. 线程所属权管理
四. 线程间共享数据
五. 同步并发操作
六. C++内存模型和原子类型操作
七. 基于锁的并发数据结构设计
八. 无锁数据结构
九. 并发代码设计
十. 高级线程管理
十一. 并行算法
十二. 参考资料
基本接口
std::thread 常用成员函数
-
构造和析构函数
// 默认构造函数,创建一个线程,什么也不做 thread() noexcept; // 初始化构造函数,创建一个线程,以 Args 为参数,执行 Fn 函数 template <class Fn, class... Args> explicit thread(Fn &&fn, Args&&..args); // 注意到该函数参数是一个右值,而函数内部又进行了完美转发,但是 fn 所需要的参数可能是个左值引用 // 因此需要使用 std::ref 或者 std::cref 来进行按(const)引用包装 --> ref 如何实现? // 复制构造函数,已被删除 thread(const thread &) = delete; // 移动构造函数,破坏原有的线程对象 thread(thread &&x) noexpect; // 析构函数 ~thread();
-
常用成员函数
// 等待此线程结束并清理资源,会阻塞 void join(); // 返回线程是否可以执行join函数 bool joinable(); // 将线程与调用它的线程分离,彼此独立执行 // 此函数必须在线程创建时立即使用,且调用此函数会使它不能被join void detach(); // 获取线程id std::thread::id get_id(); // 移动赋值函数,如果对象是joinable的那么会调用 std::terminate 结束程序 thread& operator=(thread &&rhs);
注意事项
- 线程是在thread对象被定义的时候开始执行的,而不是在调用join函数时才执行的,调用join函数只是阻塞等待线程结束并回收资源。
- 分离的线程(执行过detach的线程)会在调用它的线程结束或自己结束时释放资源。
- 线程会在函数运行完毕后自动释放,不推荐利用其他方法强制结束线程,可能会因资源未释放而导致内存泄漏。
- 没有执行join或detach的线程在程序结束时会引发异常
std::mutex
mutex常用成员函数
// 将mutex上锁。
// 如果mutex已经被其它线程上锁,那么会阻塞,直到解锁;
// 如果mutex已经被同一个线程锁住,那么会产生死锁。
void lock();
// 解锁mutex,释放其所有权。
// 如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程;
// 如果mutex不是被此线程上锁,那么会引发未定义的异常。
void unlock();
// 尝试将mutex上锁。
// 如果mutex未被上锁,则将其上锁并返回true;
// 如果mutex已被锁则返回false。
bool try_lock();
std::atomic<T>
mutex可以解决多线程资源争夺的问题,但是效率不高
atomic是一个模板类,使用该类可以创建原子类型,实现数据的原子操作。
原子类型名称 | 对应的内置类型名称 |
---|---|
atomic_bool | bool |
atomic_char | char |
atomic_schar | signed char |
atomic_uchar | unsigned char |
atomic_int | int |
atomic_uint | unsigned int |
atomic_short | short |
atomic_ushort | unsigned short |
atomic_long | long |
atomic_ulong | unsigned long |
atomic_llong | long long |
atomic_ullong | unsigned long long |
atomic_char16_t | char16_t |
atomic_char_32_t | char32_t |
atomic_wchar_t | wchar_t |
std::atomic<T>
的构造函数
atomic() noexcept = default;// 默认构造函数 构造一个atomic对象(未初始化,可通过atomic_init进行初始化)
constexpr atomic(T val) noexcept;// 初始化构造函数 构造一个atomic对象,用val的值来初始化
atomic(const atomic&) = delete; // 复制构造函数 (已删除)
atomic变量能够像不同变量一样使用
std::async 和 std::future
头文件
-
std::async
不同于thread,async是一个函数
// 异步或同步(根据操作系统而定)以args为参数执行fn // 同样地,传递引用参数需要std::ref或std::cref template <class Fn, class... Args> future<typename result_of<Fn(Args...)>::type> async(Fn &&fn, Args... args); // 异步或同步(根据policy参数而定)以args为参数执行fn,引用参数同上 template <class Fn, class... Args> future<typename result_of<Fn(Args...)>::type> async(std::launch policy, Fn &&fn, Args&&... args);
std::launch是一个强枚举类,有两个枚举值和一个特殊值
std::launch::async; // 0x1 异步启动 std::launch::deferred; // 0x2 在调用futrue::get或者futruen::wait时启动 std::launch::async | std::launch::deferred; // 0x3 特殊值,操作系统决定使用异步还是同步
template<class ... Args> decltype(auto) sum(Args&&... args) { // C++17折叠表达式 // "0 +"避免空参数包错误 return (0 + ... + args); } int main() { // 注:这里不能只写函数名sum,必须带模板参数 future<int> val = async(launch::async, sum<int, int, int>, 1, 10, 100); // future::get() 阻塞等待线程结束并获得返回值 cout << val.get() << endl; // 111 return 0; }
为什么大多数情况下使用async而不使用thread?
thread可以快速方便的创建线程,但是不如async快速方便
async可以根据情况选择同步执行或者创建新的线程来异步执行,也可以手动选择。
async返回值操作也比thread操作更加方便 -
std::future
构造函数有默认构造函数,移动构造函数,复制构造函数被删去。
常用成员函数:
T get(); // 阻塞当前线程,等待指定线程结束并获取返回值,只允许被调用一次 void wait() const; // 阻塞当前线程等待指定线程结束 template <class Rep, class Period> future_status wait_for(const chrono::duration<Rep, Period>& rel_time) const; /* 阻塞等待rel_time(rel_time是一段时间), 若在这段时间内线程结束则返回future_status::ready 若没结束则返回future_status::timeout 若async是以launch::deferred启动的,则不会阻塞并立即返回future_status::deferred */
std::future的作用并不只有获取返回值,它还可以检测线程是否已结束、阻塞等待,所以对于返回值是void的线程来说,future也同样重要。
例子
// C++ Standard: C++17 #include <iostream> #include <future> using namespace std; void count_big_number() { // C++14标准中,可以在数字中间加上单 // 引号 ' 来分隔数字,使其可读性更强 for (int i = 0; i <= 20'0000'0000; i++); } int main() { future<void> fut = async(launch::async, count_big_number); cout << "Please wait" << flush; // 每次等待1秒 while (fut.wait_for(chrono::seconds(1)) != future_status::ready) cout << '.' << flush; cout << endl << "Finished!" << endl; return 0; }
std::promise
std::asyn
和std::future
配合使用可以获得线程的状态和返回值。而将std::promise
以引用的方式传入线程执行的函数中,也可以实现类似的效果。
std::future
是只读的,而std::promise
是可读可写的,因此可以传入引用。
std::promise
常用成员函数
void set_value(const T &val);
// 设置值,一般是线程返回的值
// 调用此函数后,promise对象的状态(future_status)会被设置为futrue_status::ready用来标志线程执行结束
例子:
// C++ Standard: C++17
#include <iostream>
#include <thread>
#include <future> // std::promise std::future
using namespace std;
template<class ... Args> decltype(auto) sum(Args&&... args) {
return (0 + ... + args);
}
template<class ... Args> void sum_thread(promise<long long> &val, Args&&... args) {
val.set_value(sum(args...)); // std::promis::set_value 获取函数返回值
}
int main() {
promise<long long> sum_value;
thread get_sum(sum_thread<int, int, int>, ref(sum_value), 1, 10, 100);
cout << sum_value.get_future().get() << endl; // 111 通过get_future获取复制的futrue对象
get_sum.join();
return 0;
}
可以看到,std::promise::set_value
在线程执行结束后调用,内部同时设置线程状态。
std::this_thread
std::this_thread 其实是一个命名空间,内部提供一些控制自己线程的方法
std::this_thread::id get_id() noexpect; // 获取线程当前id
template <class Rep, class Period>
void sleep_for(const std::chrono::duartion<Rep, Period> &sleep_duartion); // 当前线程等待一段时间
void yield() noexpect; // 暂时放弃线程的执行,将主动权交给其他线程,注意是暂时,后面还是会尝试执行
案例:
#include <iostream>
#include <thread>
#include <atomic>
atomic_bool ready = 0;
// uintmax_t ==> unsigned long long
void sleep(uintmax_t ms)
{
this_thread::sleep_for(chrono::milliseconds(ms));
}
void count()
{
while (!ready) this_thread::yield();
for (int i = 0; i <= 20'0000'0000; i++);
cout << "Thread " << this_thread::get_id() << " finished!" << endl;
return;
}
int main()
{
thread th[10];
for (int i = 0; i < 10; i++)
th[i] = thread(::count);
sleep(2000);
ready = true;
cout << "Start!" << endl;
for (int i = 0; i < 10; i++)
th[i].join();
return 0;
}
C++ 并发编程实战学习笔记
初步了解多线程
子线程崩溃
我们创建一个子线程,如果这个子线程没有被join
或者被detach
,那么该线程结束之后会直接让进程崩溃,具体原因可以看一下thread
的析构函数。
~thread()
{
if (joinable())
std::__terminate();
}
如果一个线程被调用过join
或者detach
函数吗,那么其joinable
的返回值将是false。因此不使用join
或者detach
,线程对象析构函数会直接调用terminate
函数来终止进程。
使用仿函数作为thread对象的参数
// 仿函数定义
class background_task {
public:
void operator()() { std::cout << "str is " << str << std::endl; }
};
在main函数中执行以下语句
std::thread t(background_task());
t.join(); // 编译出错,因为编译器认为 t 是一个返回值类型为 std::thread,参数是background_task()的函数对象
解决方式,使用 C++11 提供的 {} 对象初始化方式
//可使用{}方式初始化
std::thread t{ background_task() };
t.join(); // 编译成功
解决主线程中局部变量指针或者引用传入子线程,而子线程detach后可能出现的问题
主线程中的局部变量如果被析构,那么子线程中的对应的引用或者指针指向的地址也被回收,如果此时子线程继续对这个地址进行读写操作将会产生未定义的行为,解决方式:
- 使用智能指针,通过增加引用计数的方法延长对象的生命周期
- 采用值传递的方式,前提是目标对象需要能够复制构造
- 使用join而非detach,影响程序执行逻辑。
主线程崩溃时如何正确处理子线程
有时候子线程会处理一些重要的事情,例如支付模块中使用子线程来记录支付日志,如果父线程崩溃,我们依然要保证子线程能够继续执行从而完成日志记录的工作。
一般来说可以使用try
来捕获异常,然后才catch
中执行join
函数。
void catch_exception() {
int some_local_state = 0;
func myfunc(some_local_state);
std::thread functhread{ myfunc };
try {
//本线程做一些事情,可能引发崩溃
std::this_thread::sleep_for(std::chrono::seconds(1));
}catch (std::exception& e) {
functhread.join();
throw;
}
functhread.join();
}
更精简的写法是使用RAII思想实现thread_guard
class thread_guard {
private:
std::thread& _t;
public:
explicit thread_guard(std::thread& t):_t(t){}
~thread_guard() {
//join只能调用一次
if (_t.joinable()) {
_t.join();
}
}
thread_guard(thread_guard const&) = delete;
thread_guard& operator=(thread_guard const&) = delete;
};
thread_guard
中获取一个thread
对象的引用,然后利用RAII的特性在thread_guard
对象析构的时候自动尝试对目标线程的join
,进而保证子线程的正常执行。
慎用隐式转换
这里更主要想说的是我们线程创建之后启动的时机。
void print_str(int n, const string str)
{ for(int i=0;i<n;++i) std::cout<<str<<std::endl; }
void danger_oops(int som_param) {
char buffer[1024];
sprintf(buffer, "%i", som_param);
//在线程内部将char const* 转化为std::string
//指针常量 char * const 指针本身不能变
//常量指针 const char * 指向的内容不能变
std::thread t(print_str, 3, buffer);
t.detach();
std::cout << "danger oops finished " << std::endl;
}
如上述代码,我们在创建了一个线程,使用字符数组作为参数,因为其可以隐式转换为std::string
类型。线程对象t内部启动并运行线程时,参数才会被传递给调用函数。这里存在一种可能,当danger_oops
函数执行完毕后线程t才开始执行,这时其才会进行参数的传递,而此时**buffer
指向的内存已经非法了**,这将导致程序崩溃。究其原因,是因为这里print_str
的参数是一个值传递模式,所以我们在创建线程时将参数的右值传递了进去,而这里恰好是一个指针,因此后续的类型转换将出现内存的非法访问
解决方式很简单,将传递的参数设置为std::string
类型即可,因为我们会传递一个std::string
,这个对象内部管理空间,因此不会出现访问非法地址空间的情况
传入引用参数
线程启动函数的参数如果是引用,那么传入参数需要使用std::ref
进行包装。
void func(int &val) { ++val; std::cout<<val<<std::endl; }
int main()
{
int a = 1;
std::thread th(func, std::ref(a)); // 使用 std::ref 进行包装,否则编译失败
th.join();
return 0;
}
Thread 传参原理
首先看一下thread的构造函数的源码(微软的编译器提供)
template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
_NODISCARD_CTOR explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
_Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
}
template <class _Fn, class... _Args>
void _Start(_Fn&& _Fx, _Args&&... _Ax) {
using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>;
auto _Decay_copied = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{});
#pragma warning(push)
#pragma warning(disable : 5039) // pointer or reference to potentially throwing function passed to
// extern C function under -EHc. Undefined behavior may occur
// if this function throws an exception. (/Wall)
_Thr._Hnd =
reinterpret_cast<void*>(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, &_Thr._Id));
#pragma warning(pop)
if (_Thr._Hnd) { // ownership transferred to the thread
(void) _Decay_copied.release();
} else { // failed to start thread
_Thr._Id = 0;
_Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN);
}
}
构造函数中利用了引用折叠技术进行了参数的接受,并且通过forward
实现了类型的完美转发,但是构造函数还调用了_Start
函数,该函数中有一句是这样的:
using _Tuple = tuple<decay_t<_Fn>, decay_t<_Args>...>;
decay_t
是C++14引入的类型转换工具模板,可以将类型退化为原始类型,例如引用退化为非引用,const退化为非const。因此_Start
函数接受到的参数全部没有了引用的修饰,因此对于需要引用的函数来说就会编译出错。
引用折叠:
[ T && --> T && ]
[ T& && --> T& ]
[ T&& && --> T&& ]
std::ref
利用模版元编程的特性,将传入参数的地址进行了记录,然后传入线程启动参数后直接使用了外部变量的地址,进而实现了引用的行为。引用本质上也就是一个封装了的T * const ptr
类型。
线程所属权管理
不要对一个正在管控着线程的std::thread对象进行赋值
void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); // 2 t1将线程管控权交给t2
t1=std::thread(some_other_function); // 3 t1此时不管控线程,可以进行赋值
std::thread t3; // 4
t3=std::move(t2); // 5
t1=std::move(t3); // 6 t1此时管控线程,赋值操作将使程序崩溃
std::thread 的可移动性
与std::unique_ptr
一样,std::thread
是可移动而不可以复制的。对于这种类型的变量我们可以写出这种代码:
void foo() {}
std::thread func(){ return std::thread(foo); }
int main()
{
std::thread t = func();
t.join();
return 0;
}
这种写法是合理的,因为会使用移动构造函数和移动赋值函数,实现线程所有权的转移。
thread_guard 和 scoped_guard
前面有设计过thread_guard,其作为线程守卫是利用了RAII特性确保线程在thread_guard对象包含的thread对象在生存周期内被正确调用join函数。thread_guard是利用引用实现的。因为使用引用,如果thread_guard的生成周期超过了引用的thread对象的生存周期,那么将产生无法预知的情况,因此我们利用线程移动的特点设计了socped_guard,其将一个线程移动到内部,让线程和自己有相同的生存周期
class scoped_thread
{
std::thread t;
public:
explicit scoped_thread(std::thread t_):t(std::move(t_)) // 转移线程所有权
{
if(!t.joinable()) // 2
throw std::logic_error(“No thread”);
}
~scoped_thread()
{
t.join(); // 3
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&)=delete;
};
joining_thread
C++ 20中添加了jthread
,基本实现和上面的scoped_guard
想法类似,利用线程转移语义让线程生存周期和guard绑定。我们设计一个joining_thread
类,对thread
进一步封装。
class joining_thread
{
public:
joining_thread() noexcept = default; // 默认构造函数
// 使用一个函数进行构造
template <typename Callable, typename ... Args>
explicit joining_thread(Callable &&func, Args && ... args):
t(std::forward<Callable>(func), std::forward<Args>(args)...) {}
// 从另一线程中转移构造
explicit joining_thread(std::thread t_) noexcept : t(std::move(t_)) {}
// 移动构造函数
joining_thread(joining_thread &&other) noexcept : t(std::move(other.t)) {}
// 移动赋值运算符
joining_thread& operator=(std::thread &&ohter) noexcept
{
if(joinable()) join();
t = std::move(ohter);
return *this;
}
joining_thread& operator=(joining_thread &&other) noexcept
{
if(joinable()) join();
t = std::move(other.t);
return *this;
}
~joining_thread() noexcept
{
if(joinable()) join();
}
void swap(joining_thread &&ohter) noexcept
{
t.swap(ohter.t);
}
std::thread::id get_id() const noexcept
{
return t.get_id();
}
bool joinable() {return t.joinable();}
void join() {t.join();}
void detach() {t.detach();}
std::thread& as_thread() noexcept {return t;}
const std::thread& as_thread() const noexcept {return t;}
private:
std::thread t;
};
这里有几点需要注意:
-
赋值运算符内部都尝试调用了
join
函数,这是为了防止因为赋值出现的匿名线程无法汇合的情况产生,而复制构造函数却不需要。 -
t 作为私有成员变量,在复制构造函数中,依然可以通过点号来调用
// 移动构造函数 joining_thread(joining_thread &&other) noexcept : t(std::move(other.t)) {} // 可以直接使用 other.t 这种写法
线程间共享数据
使用std::lock_guard来自动调用std::mutex.unlock()
C++ 中可以使用互斥锁std::mutxt
来对数据进行上锁保护其在多线程处理中的原子性。我们使用std::mutex.lock()
上锁,使用std::mutex.unlock()
解锁,但是有时候程序可能会出现异常或者程序员忘记unlock
,因此使用RAII思想封装互斥锁,自动实现对临界资源的上锁和解锁。
class mutex_guard
{
public:
mutex_guard() noexcept = delete;
mutex_guard(std::mutex &mtx) : _mtx(mtx) { _mtx.lock(); }
~mutex_guard() { _mtx.unlock(); }
private:
std::mutex &_mtx;
};
避免使用游离的指针或引用导致数据加锁失效
看一个例子
int data;
class data_wapper
{
public:
template <typename Function>
void prcess_data(Function func)
{
std::lock_guard<std::mutex> g(_mtx);
func(data);
}
private:
std::mutex _mtx;
};
int *ptr = nullptr;
void dangourse_operator(int &protected_data)
{
ptr = &protected_data;
*ptr = 1; // 直接修改加锁的变量
}
int main()
{
data_wapper x;
x.prcess_data(dangourse_operator); // 在加锁的情况下进行数据修改
return 0;
}
因为接口设计而导致资源冲突
考虑数据结构栈std::stack<T>
,其提供的接口有pop
,push
,empty
,top
,在多线程执行的情况下,这些接口提供的值可能是不安全的。
std::stack<int> sk;
sk.push(1);
例如一个线程执行
if(!sk.empty()) sk.top() = 0;
但是在这个线程进行empty
判断之后到对栈顶元素重新赋值之前这段时间,另外一个线程将栈中唯一一个元素弹出。此时sk.top()
的操作会导致程序的崩溃。这里的危险之处在于我们使用两个接口进行操作,这些冲突是不可避免的。同理,empty
和pop
接口的配合使用也会有同样的问题。
Thread A | Thread B |
---|---|
if(!sk.empty()); | |
if(!sk.empty()); | |
int const value = s.top(); | |
int const value = s.top(); | |
s.pop(); | |
do_something(value); | s.pop(); |
do_something(value); |
在上面过程中,两个线程的操作宏观上弹出了两个元素,实际上他们都只想弹出第一个元素而已。这种操作引发的问题将很那排查,因为他甚至可能不会报错。
一般来说,如果我们想获得栈顶元素并将其弹出,必须要进行两部操作top
+pop
,这样分成两步设计的原因是防止在弹出元素时出现拷贝失败而抛出异常,同时栈中元素也被删除,最终导致数据丢失的情况。
实现线程安全的栈
为了实现线程安全的栈,我们必须要调整接口,因为更少的接口才会减少接口设计而产生的资源竞争问题。这里我们将top
和pop
合并,删去top
接口。对于pop
的设计,我们采用两种策略:第一种,对于很大的对象,我们考虑返回shared_ptr<T>
,这样可以很方便的进行对象生存周期的管理;第二种,对于基本类型例如char
,可以使用引用进行数据的传递。同时,我们在弹出元素时将empty
和pop
合并处理,避免了并发情况下数据不一致的情况。
#include <exception>
#include <memory>
#include <mutex>
#include <stack>
struct empty_stack : public exception
{
const char* what() const throw()
{
return "empty stack!";
}
};
template <typename T>
class threadsafe_stack
{
public:
threadsafe_stack() : stk(std::stack<T>()) {}
threadsafe_stack(const threadsafe_stack &other)
{
std::lock_guard<std::mutex> guard(other.mtx);
stk = other.skt; // 因为需要上锁,所以在函数体内执行拷贝操作
}
threadsafe_stack& operator=(const threadsafe_stack &other) = delete;
void push(T new_value)
{
std::lock_guard<std::mutex> guard(mtx);
stk.push(new_value);
}
// 弹出较大的对象,考虑使用共享指针
std::shared_ptr<T> pop()
{
std::lock_guard<std::mutex> guard(mtx);
if(stk.empty()) throw empty_stack(); // 在调用pop前,需要检查栈是否为空
std::shared_ptr<T> const res(std::make_shared<T>(stk.top()));
stk.pop();
return res;
}
// 弹出小对象,可以考虑使用引用
void pop(T &value)
{
std::lock_guard<std::mutex> guard(mtx);
if(stk.empty()) throw empty_stack();
value = stk.top();
stk.pop();
}
bool empty() const
{
std::lock_guard<std::mutex> guard(mtx);
return stk.empty();
}
private:
std::stack<T> stk;
mutable std::mutex mtx;
};
死锁,问题描述和解决方案
如果在进行某个操作时,我们需要获取多个锁,但是在多线程情况下(以两个线程为例),两个线程分别获得了其中的一部分锁,最后双方将会因为都无法得到全部的锁而同时阻塞,最后导致死锁。解决这种问题一种通用方法是固定获取的锁的顺序。但在交换两个同类型的元素下面,也许会出现问题。
为了解决死锁问题,可以使用std::lock
函数让两个锁“同时”上锁,这里的同时并非是完全同一时间,而是有点像类似事务的处理方式,要么两者都上锁成功,要么将锁都释放。
std::mutex m1,m2;
std::lock(m1,m2); // 线程1
std::lock(m2,m1); // 线程2
在上面情形中,线程1和线程2可能会分别先获得锁1和锁2,之后其中一个线程在尝试获得另一个锁时均会失败,然后std::lock
函数会抛出异常并释放已经获取的锁,另一个线程就可以获得全部的锁,这样就避免了死锁的产生。
// 使用 std::lock + std::adopt + std::lock_guard
void swap(CirSource &first,CirSource &second)
{
std::lock(first.dataLock,secode.dataLock);
std::lock_guard<std::mutex> lockf(first.dataLock,std::adopt_lock);
std::lock_guard<std::mutex> locks(second.dataLock,std::adopt_lock);
int temp = first.n1;
first.n1 = second.n1;
second.n1 = temp;
}
std::adopt_lock
:通过名称中的adopt
,该英文单词对应的中文释义是收养、接受的意思。指的就是接受锁的所有权,通过这个参数的制定之后,std::lock_guard
在构造函数中接受锁的时候,只会记录锁的信息不会对锁进行加锁操作。该RAII
对象会在析构的时候对锁进行解锁。这里之所以这么使用因为锁已经在std::lock
函数中实现了加锁操作,不需要在这个RAII
对象中再次进行加锁操作。
更加灵活的 std::unique_lock
std::unique_lock
和std::lock_guard
用法基本相同,在构造时默认加锁,在析构时默认解锁。但是std::unique_lock
有更好的灵活性,可以使用std::unique_lock.unlock()
函数进行手动释放锁,也可以使用std::unique_lock.owns_lock()
来判断自己自己是否持有锁。
std::mutex mtx;
std::unique_lock<std::mutex> ul(mtx);
ul.owns_lock(); // true
ul.unlock(); // 可以选择手动释放锁,提高灵活性
ul.owns_lock(); // false
std::unique_lock
可以实现延迟加锁
std::mutex mtx;
std::unique_lock<std::mutex> ul(mtx, std::defer_lock); // 使用 std::defer_lock 进行延迟加锁
ul.lock(); // 手动加锁(延迟加锁)
ul.unlock(); // 可选:手动释放锁
std::unique_lock
也支持领养锁
std::mutex mtx;
mtx.lock();
std::unique_lock<std::mutex> ul(mtx, std::adopt_lock); // 领养锁,之后会自动释放锁
ul.owns_lock(); // 返回true
注意,一但mutex
被unique_lock
管理,加锁和解锁的权限就全部交给了unique_lock
,而不能mutex
自己加锁或者解锁。
不同域中互斥量的传递
std::mutex
是不支持移动和拷贝的,但是std::unique_lock
支持移动操作,所以可以使用std::unique_lock
进行互斥量的所有权转移。
假设现在有一个数据,对于这个数据需要有准备和处理两个过程。为了防止在并发情形下数据被破坏,因此我们需要上锁。一种需求是我们处理数据的模块希望继承准备数据模块中的互斥锁,并继续利用这个互斥锁来对数据进行保护。这时可以使用std::unique_lock
void prepare_data();
std::unique_lock<std::mutex> get_lock()
{
extern std::mutex mtx;
std::unique_lock lc(mtx);
prepare_data(); // 准备数据
return lc; // unique_lock 可以移动
}
void process_data() // 处理数据
{
std::unique_lock<std::mutex> lk(get_lock()); // 继承准备数据时使用的锁
do_something();
}
保护共享数据的初始化过程
有些数据在使用之前,必须初始化,初始化只需要执行一次,后面就不需要了。为了能够正常初始化,我们往往需要对变量的值进行判断。
int *ptr = nullptr;
void func()
{
if(ptr == nullptr) ptr = new int(1);
std::cout<<*ptr;
}
在多线程的情况下,我们需要对初始化的过程上锁,才能保证并发情形下不出现意外的情况:
int *ptr = nullptr;
std::mutex mtx;
void func()
{
std::unique_lock<std::mutex> lk(mtx);
if(ptr == nullptr) ptr = new int(1);
lk.unlock();
std::cout<<*ptr;
}
实际上,在初始化一次之后,后面的加锁其实都不需要了,因为不需要再次初始化了。但是由于代码已经写出来,所有后续所有调用func函数时都会尝试获取锁,这对性能是不利的。
C++ 提供了std::call_once
和std::once_flag
来解决这个问题
int *ptr = nullptr;
std::once_flag init_flag;
void init() { if(ptr == nullptr) ptr = new int(1); }
void func()
{
std::call_once(init_flag, init); // 只进行一次初始化,后续调用func不会上锁,减少竞争
std::cout<<*ptr;
}
实现线程安全的单例模式
利用std::call_once
和std::once_flag
实现线程安全的单例模式
class SingletonOnce
{
public:
SingletonOnce(const SingletonOnce &) = delete;
SingletonOnce& operator=(const SingletonOnce &) = delete;
static std::shared_ptr<SingletonOnce> GetInstance()
{
static std::once_flag s_flag;
std::call_once(s_flag, [&](){
_instance = shared_ptr<SingletonOnce>(new SingletonOnce);
});
return _instance;
}
void show() { std::cout<<"hello world\n"; }
private:
static std::shared_ptr<SingletonOnce> _instance;
SingletonOnce() = default;
};
// 静态成员变量,类内声明,类外初始化
std::shared_ptr<SingletonOnce> SingletonOnce::_instance = nullptr;
保护不常更新的数据结构–读写锁
考虑DNS的使用场景,大部分情况下都是查询操作(读操作),这有少部分情况下是更新操作(写操作)。因此,只要是DNS服务器不在更新状态,其内部数据完全可以不上锁的读取,但是在更新状态下就必须要进行互斥访问。为了实现这种功能,C++17标准库提供了std::shared_mutex
来处理这种场景。
#include <map>
#include <string>
#include <mutex>
#include <shared_mutex>
class dns_entry;
class dns_cache
{
std::map<std::string, dns_entry> entries;
mutable std::shared_mutex entry_mutex;
public:
dns_entry find_entry(std::string const &domain) const
{
std::shared_lock<std::shared_mutex> lk(entry_mutex); // 1 使用 shared_lock 检测是否真的加锁了
std::map<std::string, dns_entry>::const_iterator const it = entries.find(domain);
return (it == entries.end()) ? dns_entry() : it->second;
}
void update_or_add_entry(std::string const &domain, dns_entry const &dns_details)
{
std::lock_guard<std::shared_mutex> lk(entry_mutex); // 更新时真正的加锁
entries[domain] = dns_details;
}
};
嵌套锁
当一个线程对一个std::mutex
上锁之后,如果继续上锁,显然是不合法的操作而且会产生未定义的行为。但是有时候我们递归的调用某些函数或者函数嵌套调用的时候可能确实会需要这种操作,因此C++标准库提供了嵌套锁std::recursive_mutex
。这个锁允许同一线程多次上锁
#include <mutex>
#include <iostream>
std::recursive_mutex mtx; // 全局嵌套锁
void funcc()
{
static int n = 3;
lock_guard<std::recursive_mutex> lk(mtx);
--n;
if(n == 0)
{
std::cout<<"finished"<<std::endl;
return;
}
funcc();
}
int main()
{
funcc(); // 输出 finished 没有发生死锁
return 0;
}
同步并发操作
C++标准库中条件变量的实现
C++标准库对条件变量的实现有两套:std::condition_variable
和std::condition_variable_any
,二者的声明都在头文件<conditon_variable>中。二者都需要和互斥量一起使用。前者仅限于与 std::mutex
一起工作,而后者可以和任何满足最低标准的互斥量一 起工作,从而加上了_any
的后缀。因为std::condition_variable_any
更加通用,这就可能从 体积、性能,以及系统资源的使用方面产生额外的开销,所以std::condition_variable
一般 作为首选的类型,当对灵活性有硬性要求时,我们才会去考 虑std::condition_variable_any
。
条件变量使用实例
#include <iostream>
#include <mutex>
#include <queue>
#include <condition_variable>
using namespace std;
std::mutex mtx;
std::queue<int> q;
std::condition_variable data_cond;
void Producer()
{
while(true)
{
const int data = 1;
std::unique_lock<std::mutex> lk(mtx);
data_cond.wait(lk, [](){return q.size() <= 3;});
q.push(data);
data_cond.notify_one();
std::cout<<"Producer\n";
this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
void Consumer()
{
while(true)
{
std::unique_lock<std::mutex> lk(mtx); // 尝试获取锁
data_cond.wait(lk, [](){return !q.empty();}); // 如果lambda返回true继续执行,否则阻塞等待唤醒,
// 唤醒后尝试去获取锁然后继续判断lambda表达式
int data = q.front();
q.pop();
std::cout<<"Consumer\n";
lk.unlock();
data_cond.notify_one();
this_thread::sleep_for(std::chrono::milliseconds(500));
}
}
int main()
{
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
return 0;
}
std::condition_variable::wait
函数接受两个参数,第一个参数是std::unique_lock<std::mutex>
类型,第二个是判断条件。一般来说,wait
函数唤醒时会首先判断自己有没有获取锁,如果没有获取锁则尝试获取锁,在获取锁之后检查判断条件是否满足,如果不满足则释放锁继续睡眠准备下一次被唤醒。
!!! 注意,在wait
的第二个参数可以是一个表达式,这个表达式被执行的次数未知,因此这个表达式最好是单纯的表达式而不要附带一些其他的操作,避免出现不确定的情况。
实现线程安全的队列
与传统的栈一样,传统的队列也会出现接口竞争,例如front
函数和empty
的竞争。这里我们处理竞争的办法将front
和pop
合并,设计try_pop
和wait_and_pop
两种接口。第一种接口是非阻塞的,如果队列中没有元素则也不会返回元素;第二种接口是阻塞,会一直等到队列中有元素时才会返回。返回元素的方式也设计了两种,一种是引用返回,一种是shared_ptr
返回元素。
// 实现线程安全的队列
#include <memory> // shared_ptr
#include <queue>
#include <condition_variable>
template <typename T>
class threadsafe_queue
{
public:
threadsafe_queue() = default;
threadsafe_queue(const threadsafe_queue &other)
{
std::lock_guard<std::mutex> lk(other.mtx);
data_queue = other.data_queue;
}
void push(T value)
{
std::lock_guard<std::mutex> lk(mtx);
data_queue.push(value);
data_cond.notify_one(); // 唤醒 wait_and_pop
}
void wait_and_pop(T &value)
{
std::unique_lock<std::mutex> lk(mtx);
data_cond.wait(lk, [this](){return !data_queue.empty();}); // lambda 表达式传入this
value = data_queue.front();
data_queue.pop();
}
std::shared_ptr<T> wait_and_pop()
{
std::unique_lock<std::mutex> lk(mtx);
data_cond(lk, [this](){return !data_queue.empty();});
std::shared_ptr<T> ret(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return ret;
}
bool try_pop(T &value)
{
std::lock_guard<std::mutex> lk(mtx);
if(data_queue.empty()) { return false; }
value = data_queue.front();
data_queue.pop();
return true;
}
std::shared_ptr<T> try_pop()
{
std::lock_guard<std::mutex> lk(mtx);
if(data_queue.empty()) return std::shared_ptr<T>();
std::shared_ptr ret = std::make_shared<T>(data_queue.front());
data_queue.pop();
return ret;
}
bool empty() const
{
std::lock_guard<std::mutex> lk(mtx);
return data_queue.empty();
}
private:
std::mutex mtx;
std::queue<T> data_queue;
std::condition_variable data_cond; // 同步
};
使用 async 启动异步任务
std::asyn
使用方式和std::thread
很像,第一个参数是执行的函数,后面跟着执行函数的参数。std::async
的返回值是期望std::future
(定义在头文件<future>
中),这意味着std::async
函数可以执行一个带有返回值的函数。
#include <iostream>
#include <future>
int func(int v){ return v + 3; }
int main()
{
std::future<int> f = async(func, 0);
std::cout<<f.get()<<std::endl; // 3
return 0;
}
实际上,std::async
的第一个参数还可以是强枚举类std::launch
std::launch::async; // 0x1 异步启动
std::launch::deferred; // 0x2 在调用futrue::get或者futruen::wait时启动
std::launch::async | std::launch::deferred; // 0x3 特殊值,操作系统决定使用异步还是同步
我们可以这样使用std::asyn
std::future<int> f = async(std::launc::deferred, func, 0);
f.get(); // 只有在调用get方法时,func才会被真正执行 --> 延迟调用
期望 future
std::async
的返回类型叫做期望std::future
,顾名思义,他就是我们希望要得到的东西,但是这个东西不一定现在就需要。下面列出期望的一些成员函数
T get(); // 阻塞当前线程,等待指定线程结束并获取返回值,只允许被调用一次
void wait() const; // 阻塞当前线程等待指定线程结束
template <class Rep, class Period>
future_status wait_for(const chrono::duration<Rep, Period>& rel_time) const;
/*
阻塞等待 rel_time(rel_time是一段时间),
若在这段时间内线程结束则返回 future_status::ready
若没结束则返回 future_status::timeout
若 async 是以 launch::deferred 启动的,则不会阻塞并立即返回 future_status::deferred
*/
期望与任务的关联
标准库提供了std::packaged_task<>
来将一个函数或者可调用对象与期望绑定。std::packaged_task
的模版参数是一个函数签名,和std::function
一样。以std::packaged_task<int(int)>
的特化为例展示一下声明
template<>
class packaged_task<int(int)>
{
public:
template<typename Callable>
explicit packaged_task(Callable&& f); // 使用一个函数或者可调用对象进行构造
std::future<int> get_future(); // 返回任务的期望,注意,期望只能移动不能复制
void operator()(int);
};
以处理GUI任务为例展示std::packaged_task
的使用,实现线程传递任务的功能
#include <deque>
#include <mutex>
#include <future> // std::packaged_task
#include <thread>
#include <utility>
std::mutex m;
std::deque<std::packaged_task<void()>> tasks; // 存储任务的容器
bool gui_shutdown_message_received(); // GUI 终止标志
void get_and_process_gui_message(); // GUI 线程获取处理任务
void gui_thread() // 1
{
while (!gui_shutdown_message_received()) // 2
{
get_and_process_gui_message(); // 3
std::packaged_task<void()> task;
{ // 为了使用锁而添加花括号
std::lock_guard<std::mutex> lk(m);
if (tasks.empty()) // 4
continue;
task = std::move(tasks.front()); // 5
tasks.pop_front();
}
task(); // 6 执行异步任务
}
}
std::thread gui_bg_thread(gui_thread);
template <typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
std::packaged_task<void()> task(f); // 7
std::future<void> res = task.get_future(); // 8
std::lock_guard<std::mutex> lk(m); // 容器的插入删除需要保持原子
tasks.push_back(std::move(task)); // 9 std::packaged_task 只支持移动构造,将任务传递给GUI线程
return res; // 10 返回异步任务的期望
}
从上面的例子中可以看到,std::packaged_task
既可以作为执行的函数,也可以放到线程中作为参数,因为它本身也是一个对象。
使用 std::promises
std::promise
对象可以保存某一类型 T 的值,该值可被 future 对象读取(可能在另外一个线程中),因此std::promise
提供了一种线程同步的手段。在std::promise
对象构造时可以和一个共享状态(通常是std::future
)相关联,并可以在相关联的共享状态(std::future
)上保存一个类型为 T 的值。std::promise
也没有复制语义而只有转移语义。
#include <iostream>
#include <functional>
#include <thread>
#include <future> // std::promise, std::future
void print_int(std::future<int> fut) {
int x = fut.get(); // 获取共享状态的值.
std::cout << "value: " << x << '\n'; // 打印 value: 10.
}
int main ()
{
std::promise<int> prom; // 生成一个 std::promise<int> 对象.
std::future<int> fut = prom.get_future(); // 和 future 关联.
std::thread t(print_int, std::move(fut)); // 将 future 交给另外一个线程t.
prom.set_value(10); // 设置共享状态的值, 此处和线程t保持同步.
t.join();
return 0;
}
这里我们在主线程中创建了一个std::promise
对象并获取了与其相关的std::future
对象(注意futrue只允许转移而不允许复制),之后我们将这个std::futrue
对象通过移动的方式传入其他线程来实现数据共享转移,但是仍然可以通过绑定的std::promise
对象来和这个已经移动的std::future
对象“沟通”。
std::promise
内部通过std::shared_ptr
构造了一个std::future
对象,因此即使他被转移了,依然可以访问。
std::promise::set_value
/std::promise::set_exception
只能调用一次,std::future::get()
也只能调用一次否则会抛出异常。
将异常存放在期望中
当我们使用std::async
进行异步操作时,异步操作任务可能会抛出异常,异常将会在std::future::get()
被调用时抛出。
void func()
{
throw std::exception();
// 抛出异常后,对应期望状态改为 ready
}
int main()
{
std::future<void> ret = std::async(func);
ret.get(); // 此时抛出异常
return 0;
}
需要注意的是func内部抛出异常后,期望ret的状态会被设置为ready,调用ret.get
后抛出异常。
使用std::promise
也可以将异常存储在期望中,这里需要用到std::promise::set_exception
函数。
#inlcude <exception>
void area(std::promise<int> &p, int x)
{
if(x<0)
{
// 设置异常时会将对应的期望转态设置为ready
p.set_exception(std::make_exception_ptr(std::logic_error("number < 0")));
}
else p.set_value(x*x);
}
int main()
{
std::promise<int> p;
std::thread t(area,std::ref(p),1);
t.join();
std::cout<<p.get_future().get()<<std::endl;
auto ret = std::async(func);
ret.get();
return 0;
}
std::shared_future
std::future
使用起来有两个限制,一个是他只有移动语义,这意味他只能在线程之间进行转移;二是他的get()
函数只允许被调用一次,这意味着多个线程无法直接获取期望值。有时候多个线程需要知道同一个期望值的结果,这时就需要使用std::shared_future
了。
std::shared_future
支持默认构造,复制构造,移动构造以及使用std::future
的右值进行构造。因此,一般会有这种用法:
std::promise p;
std::shared_futrue<int> sf(p.get_future()); // 隐式构造
也可以使用std::future::share()
方法构造一个std::shared_future
对象
std::future<void> f;
std::shared_future<void> sf = f.share();
多个线程使用std::shared_futrue
时一般会为每个线程复制一份std::shared_future
对象,这样在使用wait
函数的时候就不会发生数据竞争了。
void myFunction(std::promise<int>&& promise) {
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::seconds(1));
promise.set_value(42); // 设置 promise 的值
}
void threadFunction(std::shared_future<int> future) {
try {
int result = future.get();
std::cout << "Result: " << result << std::endl;
}
catch (const std::future_error& e) {
std::cout << "Future error: " << e.what() << std::endl;
}
}
void use_shared_future() {
std::promise<int> promise;
std::shared_future<int> future = promise.get_future();
std::thread myThread1(myFunction, std::move(promise)); // 将 promise 移动到线程中
std::thread myThread2(threadFunction, future); // std::shared_futrue支持复制构造
std::thread myThread3(threadFunction, future);
myThread1.join();
myThread2.join();
myThread3.join();
}
使用 std::packaged_task 和 std::future 实现线程池
使用std::vector
作为线程容器,使用std::packaged_task<void()>
作为任务队列中所有任务的类型。通过std::bind
函数将有参函数转化为无参函数以符合任务队列的类型要求。提交任务时直接返回对应期望。
#ifndef __THREAD_POOL_H__
#define __THREAD_POOL_H__
#include <iostream>
#include <atomic>
#include <future>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <queue>
#include <vector>
#include <functional>
using namespace std;
class ThreadPool
{
public:
ThreadPool(const ThreadPool &) = delete; // 不允许复制构造
ThreadPool &operator=(const ThreadPool &) = delete; // 不允许复制赋值运算
using Task = std::packaged_task<void()>; // 任务类型
static ThreadPool &instance()
{
static ThreadPool ins;
return ins;
}
~ThreadPool() { stop(); } // 停止线程工作
// 提交一个任务,返回期望
template <class F, class... Args>
auto commit(F &&f, Args &&...args) -> std::future<decltype(f(args...))>
{
using RetType = decltype(f(args...));
if (stop_.load())
return std::future<RetType>{};
auto task = std::make_shared<std::packaged_task<RetType()>>( // 一个指向函数对象的共享指针
std::bind(std::forward<F>(f), std::forward<Args>(args)...)); // std::bind 将函数与参数绑定,生成一个无参的回调函数
std::future<RetType> ret = task->get_future();
{
std::lock_guard<std::mutex> cv_mt(cv_mt_);
tasks_.emplace([task](){ (*task)(); }); // 向 task_ 中插入一个lambda 表达式,表达式里面执行了一个 std::packaged_task 任务,任务是执行回调函数
}
cv_lock_.notify_one();
return ret;
}
int idleThreadCount()
{
return thread_num_;
}
private:
ThreadPool(unsigned int num = 5) : stop_(false)
{
{
if (num < 1)
thread_num_ = 1;
else
thread_num_ = num;
}
start();
}
void start()
{ // 启动函数,用于线程的创建
for (int i = 0; i < thread_num_; ++i)
{
pool_.emplace_back([this](){ // 每个线程的工作,不断判断 stop_ 的值,然后取出一个任务去执行
while (!this->stop_.load())
{
Task task;
{
std::unique_lock<std::mutex> cv_mt(cv_mt_); // 取出任务需要上锁,使用条件变量进行同步
this->cv_lock_.wait(cv_mt, [this] {
return this->stop_.load() || !this->tasks_.empty();
});
if (this->tasks_.empty())
return;
task = std::move(this->tasks_.front());
this->tasks_.pop();
}
// thread_num 是原子类型
this->thread_num_--; // 此线程需要去执行任务了,将可使用线程数量减1
task(); // 执行任务
this->thread_num_++; // 此线程任务执行结束,将可使用线程数量加1
} });
}
}
void stop()
{
stop_.store(true); // 设置停止标志位 true
cv_lock_.notify_all();
for (auto &td : pool_)
{
if (td.joinable())
{
// std::cout << "join thread " << td.get_id() << std::endl;
td.join(); // 线程的生存周期和线程池相同,线程池释放时需要保证线程处于 joinable状态,防止异常退出
}
}
}
private:
std::mutex cv_mt_; // 互斥锁,对 task_ 访问的保护
std::condition_variable cv_lock_; // 条件变量,用于线程同步
std::atomic_bool stop_; // 线程停止标志
std::atomic_int thread_num_; // 当前可用的线程个数
std::queue<Task> tasks_; // 执行的任务队列
std::vector<std::thread> pool_; // 存放线程的容器
};
#endif // !__THREAD_POOL_H__
int main()
{
for(int i=0;i<10;++i)
{
auto ret = ThreadPool::instance().commit([](){return 1;});
cout<<ret.get()<<endl;
}
return 0;
}
使用 std::chrono::duration 表示时延
线程库使用到的所有C++时间处理工具,都在
std::chrono
命名空间内
模板 std::chrono::duration<>
有两个类型参数:
-
Rep
:表示持续时间的数值类型,通常是整数类型,如int64_t
。 -
Period
:表示时间单位的比率,通常是一个std::ratio<>
类型。这个比率特指一个单位时间和秒的比率,例如分钟表示为std::ratio<60,1>
,毫秒可以表示为std::ratio<1,1000>
。
使用例子如下:
std::chrono::duration<int, std::ratio<60, 1>> a(6); // 6分钟
std::chrono::duration<int, std::ratio<1, 1000>> b(100); // 100毫秒
std::chrono
也已经定义了很多类型用来表示各种时间
std::chrono::microseconds; // std::chrono::duration<int, std::ratio<1, 1000000>>
std::chrono::milliseconds; // std::chrono::duration<int, std::ratio<1, 1000>>
std::chrono::seconds; // std::chrono::duration<int, std::ratio<1, 1>>
std::chrono::minutes; // std::chrono::duration<int, std::ratio<60, 1>>
std::chrono::hours; // std::chrono::duration<int, std::ratio<3600, 1>>
时延支持四则运算
使用 std::chrono::time_point<> 表示时间点
auto start = std::chrono::high_resolution_clock().now();
// do something();
auto end = std::chrono::high_resolution_clock().now();
std::cout<<std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()<<std::endl; // 输出时间
对于获取时间戳,也可以使用std::chrono::system_clock::now().time_since_epoch()
。可以通过加减获得相对时间点。
// 获得未来五秒后的时间点
auto futureTime = std::chrono::system_clock::now() + std::chrono::second(5);
具有超时功能的函数
类型/命名空间 | 函数 | 返回值 |
---|---|---|
std::this_thread 命名空间 | sleep_for(duration) 和sleep_until(time_point) | N/A |
std::condition_variable 和std::condition_variable_any | wait_for(lock, duration) 和`wait_until(lock, time_point) | std::cv_status::time_out和 std::cv_status::no_timeout` |
wait_for(lock, duration, predicate) 和wait_until(lock, duration, predicate) | bool —— 当唤醒时,返回 谓词的结果 | |
std::timed_mutex 或 std::recursive_timed_mutex | try_lock_for(duration) 和try_lock_until(time_point) | bool —— 获取锁时返回true ,否则返回fasle |
std::unique_lock\<TimeLockable> | unique_lock(lockable, duration) 和unique_lock(lockable, time_point) | 当获取锁时返回true ,否则返回false |
std::future\<ValueType> 或 std::shared_future\<ValueType> | wait_for(duration) 和wait_until(time_point) | 当期望值准备就绪时,返回 std::future_status::ready , 当期望值持有一个为启动的 延迟函数,返回 std::future_status::deferred |
使用期望值的函数化编程
函数化编程是一种编程化方式,这种方式中的函数结果只依赖于传入函数的参数,并不依赖外部状态,因此这很适用于没有相互关联的线程执行并发计算。看一个使用链表进行快速排序的基本代码:
template <typename T>
std::list<T> sequential_quick_sort(std::list<T> input)
{
if (input.empty()) return input;
std::list<T> result;
result.splice(result.begin(), input, input.begin()); // 1 选取第一个元素来分隔
T const &pivot = *result.begin(); // 2
auto divide_point = std::partition(input.begin(), input.end(),
[&](T const &t)
{ return t < pivot; }); // 3
std::list<T> lower_part;
lower_part.splice(lower_part.end(), input, input.begin(),
divide_point); // 4
auto new_lower(
sequential_quick_sort(std::move(lower_part))); //
5 auto new_higher(
sequential_quick_sort(std::move(input))); // 6
result.splice(result.end(), new_higher); // 7 加在result尾部
result.splice(result.begin(), new_lower); // 8 加在result头部
return result;
}
std::list::splice
函数的作用是剪切链表。快速排序使用的是递归的方法,很容易发现,每一次递归的两个部分是独立的,因此可以很简单的使用多线程进行计算:
template <typename T>
std::list<T> parallel_quick_sort(std::list<T> input)
{
if (input.empty()) return input;
std::list<T> result;
result.splice(result.begin(), input, input.begin());
T const &pivot = *result.begin();
auto divide_point = std::partition(input.begin(), input.end(),
[&](T const &t)
{ return t < pivot; });
std::list<T> lower_part;
lower_part.splice(lower_part.end(), input, input.begin(),
divide_point);
std::future<std::list<T>> new_lower( // 1
std::async(¶llel_quick_sort<T>, std::move(lower_part)));
auto new_higher(
parallel_quick_sort(std::move(input))); // 2
result.splice(result.end(), new_higher); // 3
result.splice(result.begin(), new_lower.get()); // 4 开启一个新的线程计算
return result;
}
这里之所以使用链表进行快速排序,因为这样可以完全做到两个线程之间数据互相不影响,理论情况下,递归n次将会开启2n个线程进行计算,但是一般计算机显然没有这么多核心,所以std::async
会控制线程的数量。
因为避免了共享可变数据,函数化编程可算是作并发编程的范型。
并发技术扩展规范
std::experimental
待补充…
Actor 和 CSP 设计模式
Actor
Actor通过消息传递的方式与外界通信,消息传递时异步的。每一个Actor有自己的邮箱,该邮箱接受并缓存其他Actor发过来的消息,Actor一次只能同步处理一个消息,处理消息的过程中除了可以接受消息不能做其他操作。总结来说,Actor只通过消息传递来交换数据,而不使用共享内存,这样尽可能的避免了上锁解锁等操作。Actor模式还可以减少各个模块间的耦合,在游戏行业用的比较多(比如临时关闭某个功能)。
Redis中的IO多线程也是这种思路,一个主线程使用多路复用监听那些socket可读可写,然后发送信息到对应的IO线程中区,告诉他可以IO了。
CSP
CSP全程Communicating Sequential Process,中文名为通信顺序进程。与Actor不同的是,CSP中的信息发送发会直接将信息放入channel中而不关心谁去取走这条信息,因此CSP的发送方和接收方是完全解耦合的。但是他们共同的设计思路都是利用消息传递代替共享内存实现线程通信。
使用队列和信号量实现一个channel,其实就是一个生产者消费者缓冲区
#include <mutex>
#include <condition_variable>
#include <thread>
#include <queue>
template <typename T>
class Channel {
private:
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable cv_producer_;
std::condition_variable cv_consumer_;
size_t capacity_;
bool closed_ = false;
public:
Channel(size_t capacity = 0) : capacity_(capacity) {}
bool send(T value) {
std::unique_lock<std::mutex> lock(mtx_);
cv_producer_.wait(lock, [this]() {
// 对于无缓冲的channel,我们应该等待直到有消费者准备好
return (capacity_ == 0 && queue_.empty()) || queue_.size() < capacity_ || closed_;
});
if (closed_) { return false; }
queue_.push(value);
cv_consumer_.notify_one();
return true;
}
bool receive(T& value) {
std::unique_lock<std::mutex> lock(mtx_);
cv_consumer_.wait(lock, [this]() { return !queue_.empty() || closed_; });
if (closed_ && queue_.empty()) { return false; }
value = queue_.front();
queue_.pop();
cv_producer_.notify_one();
return true;
}
void close() {
std::unique_lock<std::mutex> lock(mtx_);
closed_ = true;
cv_producer_.notify_all();
cv_consumer_.notify_all();
}
};
// 示例使用
int main() {
Channel<int> ch(10); // 10缓冲的channel
std::thread producer([&]() {
for (int i = 0; i < 5; ++i) {
ch.send(i);
std::cout << "Sent: " << i << std::endl;
}
ch.close();});
std::thread consumer([&]() {
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 故意延迟消费者开始消费
int val;
while (ch.receive(val)) {
std::cout << "Received: " << val << std::endl;
}});
producer.join();
consumer.join();
return 0;
}
std::asyc 使用注意事项
在使用std::async
时,其内部会使用thread
,packaged_task
,future
等机制。async
会返回一个future
,这个期望会等待其绑定的任务完成时才会去析构,例如如下代码看似使用async
实现异步运行,而实际上却是顺序执行的。
void BlockAsync() {
std::cout << "begin block async" << std::endl;
{ // 这里设置了一个局部作用域
std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout << "std::async called " << std::endl;
});
} // 在这里async返回的期望会被析构,但是他需要等待任务完成,因此变成顺序执行
std::cout << "end block async" << std::endl;
}
为什么会这样呢?
首先是std::packaged_task
对象的析构必须要等到对应任务完成才可以执行。而std::future
内部存储了对应std::async
的一个状态,其实也就是std::packaged_task
的执行状态。这个状态析构时会等待std::packaged_task
执行完毕,因此std::future
需要等待对应任务执行完毕才能正确析构。
所以说,使用async
要注意其返回的future是不是shared state的最后持有者。
C++内存模型和原子类型操作
C++原子变量
构造函数
//(1)默认:使对象处于未初始化状态。
atomic<T>() noexcept = default;
//(2)初始化 :使用val初始化对象。
constexpr atomic<T>(T val) noexcept;
//(3)复制 [删除] :无法复制/移动对象。
atomic<T>(const atomic&) = delete;
std::atomic<T>::is_lock_free
用于检测当前atomic<T>
对象是否支持无锁操作。调用此成员函数不会引起数据集竞争。
bool is_lock_free() const volatile noexcept;
bool is_lock_free() const noexcept;
// 如果当前atomic对象支持无锁操作,则返回true;否则返回false。
std::atomic<T>::store
用于将给定的值存放到原子对象中
void store(T desired, std::memory_order order = std::memory_order_seq_cst) volatile noexcept;
void store(T desired, std::memory_order order = std::memory_order_seq_cst) noexcept;
// desired 要存储的值
// order 存储操作的内存顺序
std::atomic<T>::load
用户获取原子变量的当前值,有两种形式
T load(memory_order order = memory_order_seq_cst) const noexcept; // 使用load函数
operator T() const noexcept; // 使用隐式类型转换
std::atomic::exchange
访问和修改包含的值,将包含的值替换并返回他之前的值,整个操作是原子的。
template< class T >
T exchange( T obj, T desired );
std::atomic<T>::compare_exchange_weak
该函数的作用是比较自己值和期望值是否相等,如果相等则将该值替换成一个新值并返回true,否则不做任何操作返回false
bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak (T& expected, T val, memory_order sync = memory_order_seq_cst) noexcept;
bool compare_exchange_weak (T& expected, T val, memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_weak (T& expected, T val, memory_order success, memory_order failure) noexcept;
// expected:期望值的地址,也是输入参数,表示要比较的值;
// val:新值,也是输入参数,表示期望值等于该值时需要替换的值;
// success:表示函数执行成功时内存序的类型,默认为memory_order_seq_cst;
// failure:表示函数执行失败时内存序的类型,默认为memory_order_seq_cst。
注意,compare_exchange_weak
函数是一个弱化版本的原子操作函数,因为在某些平台上它可能会失败并重试。如果需要保证严格的原子性,则应该使用compare_exchange_strong
函数。
std::atomic::compare_exchange_strong
用法与std::atomic::compare_exchange_weak
相同,正如之前所说,std::atomic::compare_exchange_weak
会使用一些硬件进行优化,这会导致罕见的伪失败情况,但是他性能更好。
伪失败
即使原子变量的当前值与
expected
匹配,它也可能返回失败,实际上没有发生任何交换。这种伪失败是罕见的,但在某些硬件和编译器实现中可能会发生。
bool
compare_exchange_strong(T& expected, T desired, memory_order success = memory_order_seq_cst, memory_order failure = memory_order_seq_cst)noexcept;
专业化支持的操作
下面这些操作会返回原来的值,并保存运算后新的值
T std::atomic<T>::fetch_add(T val, memory_order order = memory_order_seq_cst);
// 加法运算
T std::atomic<T>::fetch_sub(T val, memory_order order = memory_order_seq_cst);
// 减法运算
T std::atomic<T>::fetch_and(T val, memory_order order = memory_order_seq_cst);
// 按位AND运算
T std::atomic<T>::fetch_or(T val, memory_order order = memory_order_seq_cst);
// 按位OR运算
T std::atomic<T>::fetch_xor(T val, memory_order order = memory_order_seq_cst);
// 按位xor运算
内存顺序参数
value | 内存顺序 | 描述 | 适用性 |
---|---|---|---|
memory_order_relaxed | 无序的内存访问 | 不做任何同步,仅保证该原子类型变量的操作是原子化的,并不保证其对其他线程的可见性和正确性。 | load/store |
memory_order_consume | 与消费者关系有关的顺序 | 保证本次读取之前所有依赖于该原子类型变量值的操作都已经完成,但不保证其他线程对该变量的存储结果已经可见。 | load |
memory_order_acquire | 获取关系的顺序 | 保证本次读取之前所有先于该原子类型变量写入内存的操作都已经完成,并且其他线程对该变量的存储结果已经可见。 | load |
memory_order_seq_cst | 顺序一致性的顺序(默认) | 保证本次操作以及之前和之后的所有原子操作都按照一个全局的内存顺序执行,从而保证多线程环境下对变量的读写的正确性和一致性。这是最常用的内存顺序。 | load/store |
memory_order_release | 释放关系的顺序 | 保证本次写入之后所有后于该原子类型变量写入内存的操作都已经完成,并且其他线程可以看到该变量的存储结果。 | load/store |
std::memory_order_acq_rel | 复合 | 同时包含memory_order_acquire和memory_order_release | load/store |
原子操作的底层实现
两个机制:总线加锁和缓存加锁
使用atomic_flag实现自旋锁
std::atomic_flag
是最简单的原子类,是无锁实现的。他是一个简单的布尔类型标志,只提供两个基本的功能:
clear(); // 清空状态 --> 变为 false
test_and_set();// 获取当前状态并修改状态为 true,返回原来的状态
std::aotmic_flag
只能使用宏ATOMIC_FLAG_INIT
来初始化,初始值为false。
// 使用std::atomic_flag实现自旋锁
class spinlock_mutex
{
std::atomic_flag flag;
public:
spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}
void lock()
{ // 必须将标志为设置为fase,否则死循环
while (flag.test_and_set(std::memory_order_acquire));
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};
std::atomic<T*>指针运算
使用起来和其他原子类型基本一致,提供了load、store、exchange、compare_exchange_weak、compare_exchange_strong基本接口,同时支持fetch_add和fetch_sub基本运算。
同步和先行
首先介绍一下两个改动序列的术语
-
synchronizes-with
意为同步,“A synchronizes-with B"的意思是A与B同步,例如两个线程,一个线程先修改了变量
val
这个操作被称为A,之后另一个线程读取变量val
这个操作被称为B,那么B操作读取的val
值一定是A操作的值。也可称作"A happens-before B”,即A操作对B可见。 -
happens-before
“A happens-before B”,即A操作对B可见。这其中包含了很多种情况,上述的"A synchronizes-with B"只是其中的一种情况。
实际上"happens-before"的关系可以建立在一个线程之内,也可以建立在两个线程之间
顺序先行(sequenced-before):单线程的情况下前面的语句先执行,后面的语句后执行两者构成顺序先行的关系。顺序先行具有传递性。
线程间先行(inter-thread-happens-before):多线程情况的"happens-before"
依赖关系 carries-dependency-into 和 dependency-ordered-before
如果 “A sequence-before B” 同时B由依赖于A的数据,那么称 “A carries-denpendency-into B”
原子操作的内存顺序
内存序列选项有六个,但是他们起始仅仅代表三种内存模型:
-
排序一致性(sequentially consistent):
memory_order_seq_cst
-
松散序列:
memory_order_relaxed
-
获取释放队列:剩余的其他标志
排序一致性
原子操作默认的内存顺序,在一个线程中,所有使用memory_order_seq_cst
修饰的原子操作只会按照相对的先后顺序执行而不会乱序执行
std::atomic<bool> x,y;
std::atomic<int> z;
void read_x_then_y()
{
while(!x.load(std::memory_order_seq_cst)); // 1
if(y.load(std::memory_order_seq_cst)) // 2
++z;
}
以上代码的位置1和位置2会严格按照先后顺序执行,这就是排序一致性
松散序列
松散序列没有任何同步关系,他只保证原子操作的原子性以及同一线程内部同一原子操作的先后顺序不变
std::atomic<bool> x(false),y(false);
std::atomic<int> z(0);
void read_x_then_y()
{
x.store(true, std::memory_order_relaxed); // 1
while(!x.load(std::memory_order_relaxed)); // 2
if(y.load(std::memory_order_relaxed)) // 3
++z;
}
以上代码位置1和位置2的两处代码会严格按照顺序执行,但是位置3代码执行的顺序却不被保证,因为x和y是两个原子变量了。
松散序列使用起来非常危险,如果不是有必要,不要使用。可以考虑获取–释放序列
获取–释放序列
获取–释放序列是松散序列的加强版,虽然操作依旧没有统一的顺序,但是这个序列引入了同步。
原子加载是获取(acquire)操作–memory_order_acquire
,原子存储就是释放操作–memory_order_release
,原子读-改-写(例如exchange)不是“获取”就是“释放”或者是二者兼有的操作–memory_order_acq_rel
。
同步在线程中的获取和释放是成对的,释放操作与获取操作同步,这样就能保证读取已经写入的值。这意味着不同线程看到的序列虽然不同,但是他们都是受限的。
Acquire-release 的开销比 sequencial consistent 小. 在 x86 架构下, memory_order_acquire 和 memory_order_release 的操作不会产生任何其他的指令, 只会影响编译器的优化:任何指令都不能重排到 acquire 操作的前面, 且不能重排到 release 操作的后面,否则会违反 acquire-release 的语义. 因此很多需要实现 synchronizes-with 关系的场景都会使用 acquire-release。
void TestReleaseAcquire() {
std::atomic<bool> rx, ry;
std::thread t1([&]() {
rx.store(true, std::memory_order_relaxed); // 1 relaxed不能重排到release的后面,所以1会在2执行之前执行
ry.store(true, std::memory_order_release); // 2
});
std::thread t2([&]() {
while (!ry.load(std::memory_order_acquire)); //3
assert(rx.load(std::memory_order_relaxed)); //4 relaxed不能重排acquire之前所以4会在3之后才执行,因此不会断言失败
});
t1.join();
t2.join();
}
总结来说,上面的代码中,线程t1通过memory_order_release
保证了rx
会在ry
之前进行store
操作,而线程t2通过memory_order_acquire
保证了rx
会在ry
之后执行
Release sequence
针对一个原子变量M,其release操作A完成之后接下来可能还会有一连串其他操作,如果接下来的操作是由
- 同一线程的写操作
- 任意线程的 read-modify-write 操作
上述两者中构成的,则称这一连串操作为以release操作A为首的release sequence。这些操作可以使用任意内存顺序而不会影响同步关系,示例代码如下
void ReleaseSequence() {
std::vector<int> data;
std::atomic<int> flag{0};
std::thread t1([&]() {
data.push_back(42); // 1
flag.store(1, std::memory_order_release); // 2
});
std::thread t2([&]() {
int expected = 1;
while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) // 3
expected = 1;
});
std::thread t3([&]() {
while (flag.load(std::memory_order_acquire) < 2); // 4
assert(data.at(0) == 42); // 5
});
t1.join();
t2.join();
t3.join();
}
可以看出,上述代码虽然是多线程执行,但是总体流程顺序是一定的:1->2->3->4->5。这里的2就是上面的A操作,而3就是release sequence,所以3可以使用任意内存序。
memory_order_consume
书中所说”memory_order_consume不应该出现在你的代码中“,也就是不推荐使用,因此在此不做记录
利用内存顺序和原子变量实现无锁队列
如果实现一个线程安全的队列,可以考虑使用互斥锁,如果某个线程无法获得互斥锁则会主动放弃处理机资源这样可以尽可能提高处理机利用率。而使用原子操作更像是使用自旋锁,他不考虑处理机利用率,让全部线程正常运行,通过自旋代替阻塞。
这里实现的无锁队列是一个环形队列,为了描述一个队列,我们需要定义它的最大容量,该值只读所以是一个普通整型,其次我们需要两个原子变量来描述队列的头和尾。考虑插入和弹出操作,如果是大对象插入复制的过程可能比较慢,会严重阻碍多线程执行效率,可以考虑每个线程先占位,然后再复制,这样就需要一个额外的原子变量记录最新的复制好的位置了;弹出操作则比较保守,在完全将复制操作执行完毕后才会修改头部指针。
#include <memory>
#include <atomic>
template <typename T, size_t Cap>
class CircleQueue : private std::allocator<T>
{
public:
CircleQueue() : _max_size(Cap + 1), _data(std::allocator<T>::allocate(_max_size)), _head(0), _tail(0), _tail_update(0) {}
// 复制和赋值操作被删除
CircleQueue(const CircleQueue &) = delete;
CircleQueue &operator=(const CircleQueue &) = delete;
CircleQueue &operator=(const CircleQueue &) volatile = delete;
~CircleQueue()
{
while (_head != _tail)
{ // 逐个析构每个元素
std::allocator<T>::destroy(_data + _head);
_head = (_head + 1) % _max_size;
}
std::allocator<T>::deallocate(_data, _max_size); // 释放空间
}
// 加入元素
bool push(T &val)
{
size_t t;
do
{
t = _tail.load(std::memory_order_relaxed);
if ((t + 1) % _max_size == _head.load(std::memory_order_acquire))
return false; // 队列已满
} while (!_tail.compare_exchange_strong(t, (t + 1) % _max_size), std::memory_order_relaxed); // 先占好位置,插入元素后面再说
_data[t] = val;
size_t tailup;
do
{
tailup = t;
} while (!(_tail_update.compare_exchange_strong(tailup, (tailup + 1) % _max_size, std::memory_order_release)));
// 在多线程并发的情况下通过循环强制保证 _tail_update 按序更新
return true;
}
// 弹出元素
bool pop(T &val)
{
size_t h;
do
{
h = _head.load(std::memory_order_relaxed);
// if(h == _tail.load()) return false;// 队列为空 --> 因为后面会和 _tail_update 比较,所以这一步可以优化掉
// 判断如果此时要读取的数据和tail_update是否一致,如果一致说明尾部数据未更新完
if (h == _tail_update.load(std::memory_order_acquire))
return false;
val = _data[h]; // 取出值,使用值拷贝,也就是需要T类型支持复制赋值操作
} // 考虑到多个线程并发执行,因此如果这次取值失败,需要重新取值
while (!(_head.compare_exchange_strong(h, (h + 1) % _max_size), std::memory_order_release));
return true;
}
private:
size_t _max_size; // 队列最大长度
T *_data; // 队列本体
std::atomic<size_t> _head; // 头部指针
std::atomic<size_t> _tail; // 尾部指针
std::atomic<size_t> _tail_update; // 正在执行插入复制操作的位置
};
这里使用do .... while ....
保证多线程并发下原子变量的正常操作,如果失败则重试。内存顺序方面,除了在循环开始处取出值时可以使用std::memory_order_relaxed
外,其他地方为了保证同步操作,需要使用release -- acquire
模型。当然,tail
变量不参与pop和push之间的竞争,因此可以使用std::memory_order_relaxed
。
我的理解是,这里的无锁队列有点类似于乐观锁的处理方式,我们埋头执行任务,最后检测是不是能够完成,不能完成则重试。无锁队列还有一个优势时只有在相同的操作并发才会进行重试,pop和push并发不会出现重试的情况。缺点是循环重试的开销还是很大的。
栅栏
std::atomic_thread_fence(std::memory_order_release); // 保证其前面的指令不会重排到它后面
std::atomic_thread_fence(std::memory_order_acquire); // 保证其后面的指令不会重排到它前面
基于锁的并发数据结构设计
线程安全的队列
线程安全的栈
线程安全的查找表
存在的问题:迭代器,并发访问时,如果修改容器时,可能导致其他线程迭代器失效
线程安全的链表
无锁数据结构
待补充
并发代码设计
简单看了一下,记录一下一些并行STL算法的设计
实现并行化的 for_each
#include <vector>
#include <thread>
#include <future>
template <typename Iterator, typename Func>
void parallel_for_each(Iterator first, Iterator last, Func f)
{
unsigned long const length = std::distance(first, last);
if (!length)
return;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
// 计算出合理的线程数量
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::vector<std::future<void>> futures(num_threads - 1); // 1
std::vector<std::thread> threads(num_threads - 1);
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0; i < (num_threads - 1); ++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size);
std::packaged_task<void(void)> task( // 2
[=]()
{
std::for_each(block_start, block_end, f);
});
futures[i] = task.get_future();
// 每个线程执行一定任务
threads[i] = std::thread(std::move(task)); // 3
block_start = block_end;
}
std::for_each(block_start, last, f);
for (unsigned long i = 0; i < (num_threads - 1); ++i)
{
futures[i].get(); // 4 获取计算结果
}
}
实现并行化的 find
#include <vector>
#include <thread>
#include <future>
template <typename Iterator, typename MatchType>
Iterator parallel_find(Iterator first, Iterator last, MatchType match)
{
// 仿函数
struct find_element // 1
{
void operator()(Iterator begin, Iterator end,
MatchType match,
std::promise<Iterator> *result,
std::atomic<bool> *done_flag)
{
try
{
for (; (begin != end) && !done_flag->load(); ++begin) // 2
{
if (*begin == match)
{
result->set_value(begin); // 3 保存查询的结果
done_flag->store(true); // 4 标记已经找到
return;
}
}
}
catch (...) // 5
{
try
{
result->set_exception(std::current_exception()); // 6
done_flag->store(true); // 抛出异常也可以终止查找
}
catch (...) // 7
{
}
}
}
};
unsigned long const length = std::distance(first, last);
if (!length)
return last;
unsigned long const min_per_thread = 25;
unsigned long const max_threads =
(length + min_per_thread - 1) / min_per_thread;
unsigned long const hardware_threads =
std::thread::hardware_concurrency();
// 计算线程数量
unsigned long const num_threads =
std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
unsigned long const block_size = length / num_threads;
std::promise<Iterator> result; // 8
std::atomic<bool> done_flag(false); // 9
std::vector<std::thread> threads(num_threads - 1);
{ // 10
join_threads joiner(threads);
Iterator block_start = first;
for (unsigned long i = 0; i < (num_threads - 1); ++i)
{
Iterator block_end = block_start;
std::advance(block_end, block_size);
threads[i] = std::thread(find_element(), // 11
block_start, block_end, match,
&result, &done_flag);
block_start = block_end;
}
find_element()(block_start, last, match, &result, &done_flag); // 12
}
if (!done_flag.load()) // 13
{
return last;
}
return result.get_future().get(); // 14
}
高级线程管理
线程池
中断线程
待补充
并行算法
使用C++17并行算法
待补充
参考资料
参考资料 https://blog.csdn.net/sjc_0910/article/details/118861539