12. 多线程开发
大家好,我是程序喵。
本节我主要向大家介绍埋点项目中的多线程模块。
估计正常做C++多线程开发,大多数朋友都是这样写代码:
或者使用async,或者使用线程池,然后往线程池里抛任务。
这个项目中,我想向大家介绍一种新的多线程开发理念,strand模型。
我们可以理解为strand是一个任务队列,strand会确保任务按顺序执行,它存在于executor体系中,大家可能也听过,executor大概率会进C++26的新标准,我这样做技术选型,也是希望能提前带大家熟悉C++26,熟悉这种编程理念。
个人认为,strand最大的优点就是可以避免任务并发执行时,可能出现的数据竞争问题。也就是说我们不需要做加锁处理,它是用来确保任务按顺序执行的一种机制,可以保证多个任务在同一个线程上按序执行,从而避免了数据竞争问题。
前面介绍过,我们可以将strand理解为一个任务队列,但任务队列自己是不可能调度的,还需要有个调度器,我们可以考虑自己实现这种机制,不过这里,我会选择asio中的调度器,来执行strand中的任务,asio中executor实现很接近标准库提案,我们使用这个大概率没错,而且会让我们将来学习C++26少走一些弯路。
下面是一个简单易懂的boost executor的strand示例代码:
在示例代码中,我们创建了一个strand对象,并使用boost::asio::post()将任务提交到strand中。由于strand的存在,任务task1和task2将会按照提交的顺序依次执行,保证了任务的顺序性。
希望通过这个示例,大家能更好地理解C++中的strand理念,以及它们在多线程开发中的作用和优势。
理解了strand后,我们再把它引入到项目中:
这里设计了一个Context类来做多线程的任务处理,外部不创建任何线程,线程统一在Context内部管理。
Start后,可以拿到不同的strand,不同的strand可以理解为不同的线程,通过不同的strand执行任务就相当于在某个线程执行任务,既满足了多个线程并行执行任务的需求,又保证一个strand的任务顺序执行,因为一个strand内的任务顺序执行,所以我们不需要做加锁处理数据竞争问题。
我们写代码时,要明确知道哪个函数要执行在哪个线程中,如果开发者都不明确任务运行在哪个线程,那代码大概率会有bug。
我们再看下它的调用示例:
这里有两个strand,mainStrand和reportStrand,两个strand内的任务会并行执行,但是,又会保证同一个strand中的任务顺序执行。
埋点项目中,使用两个strand足以,mainStrand用于处理大多数主要逻辑,reportStrand用于执行网络相关的任务。
再看下它的实现:
因为我们需要两个strand并行执行,所以这里创建了两个线程,分别调度strand。
到这里,多线程开发模块已经介绍完毕。下一节我会具体介绍整个上报模块的实现,下周见。
本节相关代码见:
- https://github.com/chengxumiaodaren/BuriedPoint/blob/main/src/context/context.cc
- https://github.com/chengxumiaodaren/BuriedPoint/blob/main/src/context/context.h
- https://github.com/chengxumiaodaren/BuriedPoint/blob/main/examples/context_example.cc
我们其实也可以自己简单实现一个strand和调度器,我星球也发过相关的实现,可以看看这篇文章:
