一、概述
Boost.Fiber是一种微线程(用户层),也可以叫作纤程(协程),与一般的协程相比,其内部提供了调度管理器。每个fiber都有自己的栈,它可以保存当前的执行状态,包括所有寄存器的CPU标志、指令指针和堆栈指针,然后可以从此状态恢复。其目的是在单个线程上通过协作调度运行多个可执行序列(即函数)。正在运行的fiber可以明确的决定什么时候yield,从而允许另外一个fiber运行(上下文切换)。
要使用Fiber库,只需要代码中包含头文件:#include <boost/fiber/all.hpp>
二、Fiber和线程
在x86上,线程之间的上下文切换通常要花费数千个CPU周期,而fiber之间的切换只有不到100个周期。fiber在任意时间点都是运行在单一的线程上。
在指定线程上启动的所有fiber,控制指令在它们之间协作传递。在指定的线程上,任意时刻,最多只有一个fiber在运行。
尽管可以更有效的使用内核,但是在线程上创建fiber,并不会把程序分布到更多的硬件内核上。
另一方面,fiber可以安全的访问父线程独占的任何资源,不需要显示的对该资源进行保护,防止同一线程上的其他fiber并发访问该资源。我们可以得到保证该线程上没有其他的fiber并发的接触该资源。要在历史遗留代码中引发并发性,这一方面很重要。通过使用异步I/O交替执行,我们可以安全创建用于运行旧代码的fiber。
实际上,fiber提供了一种基于异步I/O来组织并发代码的方式。在fiber运行的代码可以使其看起来像调用一个普通的阻塞函数,这种调用可以很方便的挂起调用的fiber,从而允许同一个线程上的其他fiber运行。当操作完成时,挂起的fiber将恢复运行,而不必显示的保持或恢复其状态,它的本地堆栈变量在整个调用中中时持久存在的。
fiber可以从一个线程迁移到另一个线程,默认情况下库不会这样处理。但是我们可以自定义在线程之间迁移fiber的调度器,可以自定义fiber属性,以协助调度器决定运行迁移哪些fiber。
在fiber上调用阻塞I/O接口将会阻塞它所在的线程,我们建议在fiber上使用异步I/O接口。Boost.Asio和其他异步I/O操作可以直接适用于Boost.Fiber。
三、boost::fibers::fiber
每个boost::fibers::fiber对象表示一个微线程,调度器将会启动和管理该fiber对象。
boost::fibers::fiber f1; // not-a-fiber
void f() {
boost::fibers::fiber f2( some_fn);
f1 = std::move( f2); // f2 moved to f1
}
1、启动
可以通过向构造函数传入一个可调用类型的对象(比如lambda)来启动一个新的fiber。如果对象不可拷贝或者不可move,那么可以使用std::ref对该对象进行引用,这种情况下,必须保证被应用的该对象生命周期比新创建的fiber长。
struct callable {
void operator()();
};
boost::fibers::fiber copies_are_safe() {
callable x;
return boost::fibers::fiber(x);
} // 函数执行后 x 对象被销毁,但是新创建的fiber对 x 有了一份拷贝,所以这样是可以的
boost::fibers::fiber oops() {
callable x;
return boost::fibers::fiber(std::ref(x));
} // 函数执行后 x 对象被销毁,但是新创建的fiber仍然对 x 进行了引用,这将导致未定义行为
新创建的fiber不会立即开始执行,它会在准备运行的fiber列表中排队,当调度器找到它时,它才会开始运行。
2、异常
传入fiber构造函数的可调用对象或者函数,如果其内部产生了异常,则构造函数将调用std::terminate()
。如果需要知道抛了某种异常,可以使用future<>
或者packaged_task<>
。
3、detach
fiber可以通过显示调用成员函数detach()
来进行分离。当调用了detach()后,该fiber变为not-a-fiber,然后它可以被安全的销毁。
void some_fn() {
...
}
boost::fibers::fiber(some_fn).detach();
Boost.Fiber提供了许多用于等待fiber完成的方法。我们甚至可以使用mutex、condition_variable或者任意其他库提供的同步对象来和已被分离的fiber对象进行协调。如果当线程的主fiber终止时,已分离的fiber仍在运行,则该线程不会被关闭。
4、join
为了等待fiber结束,可以使用成员函数join()
,它将阻塞至fiber对象完成。如果fiber已经完成,那么join将立即返回,该fiber对象变为not-a-fiber。
void some_fn() {
...
}
boost::fibers::fiber f(some_fn);
...
f.join();
5、析构
当fiber对象还有有效可执行的上下文(即fiber是joinable()
)时,如果它被销毁,程序将会终止。如果希望fiber比启动它的对象存活更久,那么请使用detach()
方法。
void some_fn() {
...
}
{
boost::fibers::fiber f(some_fn);
} // std::terminate() 将被调用
{
boost::fibers::fiber f(some_fn);
f.detach();
} // 没问题,程序继续执行
6、ID
类fiber::id的对象可以用来标识fiber。每一个运行的fiber都有一个唯一的fiber::id,通过调用get_id()
成员函数,可以获取对应fiber的id。类fiber::id的对象是可以拷贝的,可以作为关联容器中的键值(它提供了所有的比较运算符),也可以使用流插入操作符将它们写入输出流(输出格式未指定)。
每个fiber::id的实例要么指向fiber对象,要么指向not-a-fiber,指向not-a-fiber的实例彼此相等,但指向实际fiber对象的实例则不相等。
7、枚举类型 - launch
指定控制是否立即传递到新启动的fiber。
enum class launch {
dispatch,
post // 默认值
};
dispatch
:立即运行,换句话说,启动一个新fiber将挂起调用者(之前运行的fiber),被挂起的fiber将等待调度器稍后寻找机会唤醒它。post
:被传给调度器并设置为就绪状态(但还未运行),调用者继续运行(之前运行的fiber),新fiber将等待调度器稍后寻找机会唤醒它时才运行。
四、调度
线程中的fiber是由fiber管理器协调的。fiber的控制是合作性的,而不是先发制人:每当一个fiber挂起(或yield)时,fiber管理器会向调度器咨询下一个将运行那个fiber。
Boost.Fiber提供了fiber管理器,但是调度器是可以自行定制的。
每个线程都有自己的调度器,进程中不同的线程可以使用不同的调度器,默认情况下,Boost.Fiber为每个线程隐式设置了round_robin
的实例作为调度器。
我们可以显示的编写自己的algorithm
子类,大多数情况下,我们的algorithm
子类不需要预防跨线程调用:fiber管理器会拦截并延迟这样的调用。大多数algorithm
方法只直接从它所管理的线程中调用。
特例情况如下:通过调用use_scheduling_algorithm()
接口,让algorithm
子类在一个特定线程上运行。
void thread_fn() {
boost::fibers::use_scheduling_algorithm<my_fiber_scheduler>();
...
}
调度器类必现实现algorithm
接口。Boost.Fiber提供的调度器有:round_robin
、work_stealing
、numa::work_stealing
、shared_work
。
void my_thread(std::uint32_t thread_count) {
// 线程注册 work-stealing 作为自身调度器
boost::fibers::use_scheduling_algorithm< boost::fibers::algo::work_stealing >(thread_count);
...
}
// 逻辑CPU的个数
std::uint32_t thread_count = std::thread::hardware_concurrency();
// 首先启动工作线程
std::vector<std::thread> threads;
for ( std::uint32_t i = 1; i < thread_count; ++i) {
// 创建线程
threads.emplace_back(my_thread, thread_count);
}
// 线程注册 work-stealing 作为自身调度器
boost::fibers::use_scheduling_algorithm<boost::fibers::algo::work_stealing >(thread_count);
...
这个示例创建了std::thread::hardware_concurrency()
返回的线程数。每个线程运行一个work_stealing
调度器。每个调度器的实例需要知道在程序中有多少个线程运行了work-stealing
调度器。如果线程的本地队列用完了就绪的fiber,则该线程会尝从其他同样运行该调度器的线程中获取就绪的fiber。
1、algorithm
algorithm
是定义一个fiber调度器锁所必须实现的抽象接口基类。
#include <boost/fiber/algo/algorithm.hpp>
namespace boost {
namespace fibers {
namespace algo {
struct algorithm {
virtual ~algorithm();
virtual void awakened(context *) noexcept = 0;
virtual context * pick_next() noexcept = 0;
virtual bool has_ready_fibers() const noexcept = 0;
virtual void suspend_until(std::chrono::steady_clock::time_point const&) noexcept = 0;
virtual void notify() noexcept = 0;
};
}}}
2、自定义调度器Fiber属性
一个从algorithm
直接派生的调度器类,可以使用于实现algorithm
接口的上下文的任意信息。但是自定义的调度器可能需要追踪fiber的其他属性。例如,一个基于优先级的调度器可能需要追踪fiber的优先级。Boost.Fiber提供了一种机制,自定义调度器可以将自身的属性和每个fiber进行关联。自定义的fiber属性类必须从fiber_properties
派生。
#include <boost/fiber/properties.hpp>
namespace boost {
namespace fibers {
class fiber_properties {
public:
fiber_properties(context *) noexcept;
virtual ~fiber_properties();
protected:
void notify() noexcept;
};
}}
五、堆栈分配
fiber使用内部的__econtext__
用于管理一组寄存器和堆栈。堆栈使用的内存时通过一个stack_allocator
进行allocated
/deallocated
,这是建模stack-allocator
概念所必须的。stack_allocator
将被传入fiber::fiber()
或fibers::async()
。
六、同步
通常,Boost.Fiber同步对象既不能拷贝也不能移动。同步对象充当不同fiber之间相互约定的集合点。如果将这个对象拷贝到其他地方,那么新的拷贝将没有使用者。如果将这个对象移动到其他地方,那么将是原始实例处于未指定的状态,现有的使用者的行为将会很奇怪。
默认情况下,库所提供的fiber同步对象将运行在不同的线程上安全的同步fiber。然而,通过定义BOOST_FIBERS_NO_ATOMICS
来构建库时,可以删除这种级别的同步(为了提高性能)。当使用该宏构建库时,必须确保所有的fiber引用的指定同步对象都运行在同一个线程上。
1、锁类型
boost::fibers::mutex
boost::fibers::timed_mutex
boost::fibers::recursive_mutex
boost::fibers::recursive_timed_mutex
2、条件变量
3、Barrier
4、Channel
channel
是一种通过消息传递来通信和同步执行线程的模型。channel
操作返回的channel
枚举状态:
enum class channel_op_status {
success, // 操作成功
empty, // channel为空,操作失败
full, // channel已满,操作失败
closed, // channel已关闭,操作失败
timeout // 在指定超时时间发生前,操作还未准备好
};
boost::fibers::buffered_channel
Boost.Fiber提供了一个有边界、缓冲的channel(MPMC队列),它适合通过异步消息传递来同步fiber,可运行在相同或不同的线程上。boost::fibers::unbuffered_channel
Boost.Fiber提供了unbuffered_channel
模板,该模板适合通过同步消息传递来同步fiber,可运行在相同或不同的线程上。fiber等待消费某个值时将阻塞,知道该值被生产。如果fiber试图通过unbuffered_channel
发送某值,且没有fiber在等待接收该值时,channel将阻塞发送的fiber。
5、futures
七、简单的封装
头文件:fiber_executor.h
#ifndef __FIBER_EXECUTOR_H__
#define __FIBER_EXECUTOR_H__
#include <atomic>
#include <memory>
#include <thread>
#include <boost/fiber/all.hpp>
/*
* Brief: 协程程序, 执行具体的代码逻辑
*/
class FiberProc
{
friend class FiberExecutor;
public:
/*
* Brief: 构造函数
* Param: func - 协程程序函数, 若设置且没有重写run, 则会执行该函数, 若重写了run, 则会执行重写的run里面的具体代码逻辑
* stackSize - 协程的栈空间大小
* Return: None
*/
FiberProc(const std::function<void()>& func = nullptr, std::size_t stackSize = (512 * 1024));
virtual ~FiberProc(void);
/*
* Brief: 运行, 子类可以重写该接口来编写执行的代码逻辑
* Param: void
* Return: void
*/
virtual void run(void);
/*
* Brief: 取消
* Param: void
* Return: void
*/
void cancel(void);
/*
* Brief: 是否已取消
* Param: void
* Return: true - 已取消, false - 未取消
*/
bool isCancelled(void);
private: /* noncopale */
FiberProc(const FiberProc&) = default;
FiberProc& operator=(const FiberProc&) = default;
private:
std::function<void()> m_func;
std::size_t m_stackSize;
std::atomic_bool m_cancelled;
};
/*
* Brief: 协程执行者, 内部会创建一个线程用于运行协程程序
*/
class FiberExecutor final
{
public:
/*
* Brief: 构造函数
* Param: channelCapacity - 协程队列容量
* Return: None
*/
FiberExecutor(std::size_t channelCapacity = 1024);
virtual ~FiberExecutor(void);
/*
* Brief: 等待退出(调用该接口会阻塞直到线程中的所有协程都执行完毕才会继续后续流程)
* Param: void
* Return: void
*/
void join(void);
/*
* Brief: 把协程程序对象加入当前队列
* Param: procObj - 协程程序对象
* Return: 协程程序对象(和入参一样)
*/
std::shared_ptr<FiberProc> post(const std::shared_ptr<FiberProc>& procObj);
/*
* Brief: 把协程程序函数加入当前队列
* Param: procFunc - 协程程序函数
* Return: 协程程序对象
*/
std::shared_ptr<FiberProc> post(const std::function<void()>& procFunc);
private: /* noncopale */
FiberExecutor(const FiberExecutor&) = default;
FiberExecutor& operator=(const FiberExecutor&) = default;
private:
std::unique_ptr<boost::fibers::buffered_channel<std::shared_ptr<FiberProc>>> m_channel; /* 有边界, 有序的通道 */
std::unique_ptr<std::thread> m_thread; /* 专门用于运行通道队列里的协程程序 */
};
#endif /* __FIBER_EXECUTOR_H__ */
源文件:fiber_executor.cpp
#include "fiber_executor.h"
FiberProc::FiberProc(const std::function<void()>& func, std::size_t stackSize)
: m_func(func)
, m_stackSize(stackSize)
, m_cancelled(false)
{
}
FiberProc::~FiberProc(void)
{
}
void FiberProc::run(void)
{
if (m_func)
{
m_func();
}
}
void FiberProc::cancel(void)
{
m_cancelled = true;
}
bool FiberProc::isCancelled(void)
{
return m_cancelled;
}
FiberExecutor::FiberExecutor(std::size_t channelCapacity)
{
assert(channelCapacity >= 2);
m_channel = std::make_unique<boost::fibers::buffered_channel<std::shared_ptr<FiberProc>>>(channelCapacity);
std::promise<void> result;
m_thread = std::make_unique<std::thread>([this, &result] {
result.set_value();
std::shared_ptr<FiberProc> proc;
while (boost::fibers::channel_op_status::closed != m_channel->pop(proc)) /* 循环从任务队列中取出任务 */
{
/* 创建fiber并运行 */
boost::fibers::fiber(std::allocator_arg, boost::fibers::default_stack(proc->m_stackSize), [proc]() {
if (!proc->isCancelled()) {
proc->run();
}
}).detach();
}
}); /* 当前线程会等待所有fiber结束才会退出 */
result.get_future().get();
}
FiberExecutor::~FiberExecutor(void)
{
m_channel->close();
m_thread->join();
}
void FiberExecutor::join(void)
{
m_thread->join();
}
std::shared_ptr<FiberProc> FiberExecutor::post(const std::shared_ptr<FiberProc>& procObj)
{
m_channel->push(procObj);
return procObj;
}
std::shared_ptr<FiberProc> FiberExecutor::post(const std::function<void()>& procFunc)
{
std::shared_ptr<FiberProc> procObj = std::make_shared<FiberProc>(procFunc);
m_channel->push(procObj);
return procObj;
}
示例1:
#include <chrono>
#include <iostream>
#include <string>
#include "fiber_executor.h"
static FiberExecutor s_executor(16);
void print(int num)
{
for (int i = 0; i < 2; ++i)
{
std::cout << "--- num: " << num << ", i: " << i << " --- start\n";
if (0 == num % 2) {
boost::fibers::promise<void> result;
s_executor.post([num, i, &result] { // 耗时操作最好放在其他线程执行, 否则会阻塞掉协程所在线程
std::cout << "--- num: " << num << ", i: " << i << " --- sleep\n";
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
result.set_value();
});
result.get_future().get();
}
std::cout << "--- num: " << num << ", i: " << i << " --- end\n";
}
}
int main()
{
for (int num = 0; num < 10; ++num)
{
std::cout << "++++++++++ " << num << " +++ start\n";
s_executor.post([num] {
print(num);
});
std::cout << "++++++++++ " << num << " +++ end\n";
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return 0;
}