12. 多线程开发

大家好,我是程序喵。


本节我主要向大家介绍埋点项目中的多线程模块。


估计正常做C++多线程开发,大多数朋友都是这样写代码:

std::thread a([]() {
while (xxx) {
yyy;
}
});
if (a.joinable()) {
a.join();
}


或者使用async,或者使用线程池,然后往线程池里抛任务。


这个项目中,我想向大家介绍一种新的多线程开发理念,strand模型。


我们可以理解为strand是一个任务队列,strand会确保任务按顺序执行,它存在于executor体系中,大家可能也听过,executor大概率会进C++26的新标准,我这样做技术选型,也是希望能提前带大家熟悉C++26,熟悉这种编程理念。


个人认为,strand最大的优点就是可以避免任务并发执行时,可能出现的数据竞争问题。也就是说我们不需要做加锁处理,它是用来确保任务按顺序执行的一种机制,可以保证多个任务在同一个线程上按序执行,从而避免了数据竞争问题。


前面介绍过,我们可以将strand理解为一个任务队列,但任务队列自己是不可能调度的,还需要有个调度器,我们可以考虑自己实现这种机制,不过这里,我会选择asio中的调度器,来执行strand中的任务,asio中executor实现很接近标准库提案,我们使用这个大概率没错,而且会让我们将来学习C++26少走一些弯路。


下面是一个简单易懂的boost executor的strand示例代码:


#include <iostream>
#include <boost/asio.hpp>

void task1() {
std::cout << "Task 1 executed." << std::endl;
}

void task2() {
std::cout << "Task 2 executed." << std::endl;
}

int main() {
boost::asio::io_context io_context; // 运行时环境
boost::asio::io_context::strand strand(io_context);

boost::asio::post(strand, task1);
boost::asio::post(strand, task2);

io_context.run();

return 0;
}


在示例代码中,我们创建了一个strand对象,并使用boost::asio::post()将任务提交到strand中。由于strand的存在,任务task1和task2将会按照提交的顺序依次执行,保证了任务的顺序性。


希望通过这个示例,大家能更好地理解C++中的strand理念,以及它们在多线程开发中的作用和优势。


理解了strand后,我们再把它引入到项目中:


class Context {
public:
static Context& GetGlobalContext() {
static Context global_context;
return global_context;
}

~Context();

using Strand = boost::asio::io_context::strand;
using IOContext = boost::asio::io_context;

Strand& GetMainStrand() { return main_strand_; }

Strand& GetReportStrand() { return report_strand_; }

IOContext& GetMainContext() { return main_context_; }

void Start();

private:
Context() : main_strand_(main_context_), report_strand_(report_context_) {}

Context(const Context&) = delete;
Context& operator=(const Context&) = delete;

private:
boost::asio::io_context main_context_;
boost::asio::io_context report_context_;

boost::asio::io_context::strand main_strand_;
boost::asio::io_context::strand report_strand_;

std::unique_ptr<std::thread> main_thread_;
std::unique_ptr<std::thread> report_thread_;

std::atomic<bool> is_start_{false};
std::atomic<bool> is_stop_{false};
};


这里设计了一个Context类来做多线程的任务处理,外部不创建任何线程,线程统一在Context内部管理。


Start后,可以拿到不同的strand,不同的strand可以理解为不同的线程,通过不同的strand执行任务就相当于在某个线程执行任务,既满足了多个线程并行执行任务的需求,又保证一个strand的任务顺序执行,因为一个strand内的任务顺序执行,所以我们不需要做加锁处理数据竞争问题。


我们写代码时,要明确知道哪个函数要执行在哪个线程中,如果开发者都不明确任务运行在哪个线程,那代码大概率会有bug。


我们再看下它的调用示例:


int main() {
buried::Context::GetGlobalContext().Start();

buried::Context::GetGlobalContext().GetMainStrand().post([]() {
std::cout << "Operation 1 executed in strand1 on thread id "
<< std::this_thread::get_id() << std::endl;
});

buried::Context::GetGlobalContext().GetReportStrand().post([]() {
std::cout << "Operation 2 executed in strand2 on thread id "
<< std::this_thread::get_id() << std::endl;

buried::Context::GetGlobalContext().GetReportStrand().post([]() {
std::cout << "Operation 3 executed in strand2 on thread id "
<< std::this_thread::get_id() << std::endl;
});

buried::Context::GetGlobalContext().GetMainStrand().post([]() {
std::cout << "Operation 4 executed in strand1 on thread id "
<< std::this_thread::get_id() << std::endl;
});

buried::Context::GetGlobalContext().GetReportStrand().post([]() {
std::cout << "Operation 5 executed in strand3 on thread id "
<< std::this_thread::get_id() << std::endl;
});
});

std::this_thread::sleep_for(std::chrono::seconds(5));
return 0;
}


这里有两个strand,mainStrand和reportStrand,两个strand内的任务会并行执行,但是,又会保证同一个strand中的任务顺序执行。


埋点项目中,使用两个strand足以,mainStrand用于处理大多数主要逻辑,reportStrand用于执行网络相关的任务。


再看下它的实现:


void Context::Start() {
if (is_start_.load()) {
return;
}
is_start_.store(true);

main_thread_ = std::make_unique<std::thread>([this]() {
for (;;) {
if (is_stop_) {
break;
}
main_context_.run();
}
});
report_thread_ = std::make_unique<std::thread>([this]() {
for (;;) {
if (is_stop_) {
break;
}
report_context_.run();
}
});
}


因为我们需要两个strand并行执行,所以这里创建了两个线程,分别调度strand。


到这里,多线程开发模块已经介绍完毕。下一节我会具体介绍整个上报模块的实现,下周见。


本节相关代码见:

  1. https://github.com/chengxumiaodaren/BuriedPoint/blob/main/src/context/context.cc
  2. https://github.com/chengxumiaodaren/BuriedPoint/blob/main/src/context/context.h
  3. https://github.com/chengxumiaodaren/BuriedPoint/blob/main/examples/context_example.cc


我们其实也可以自己简单实现一个strand和调度器,我星球也发过相关的实现,可以看看这篇文章:

https://articles.zsxq.com/id_dhm6h72wdsk0.html

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
程序喵
下载 APP