C/C++ 11/14/17 标准及基础类库,上人们可以采用大规模就绪 “有栈协同程序” 来解决 C/C++ 异步编程的编程复杂性。
本文适用群体:资深C/C++ 服务器开发人员(T3.5, 初级工程师[资深],分9级,0级不入流)
设:
我们需要异步等待某个函数 3000 milliseconds(毫秒)那么我们大约会采用,更易于多数开发人员理解的:APM(Asynchronous Programing Models)异步编程模型。
它有两种接口表现形式:
1、BeginSleep(int millisecondsTimeout, Handler&& handler)
EndSleep(...)
2、SleepAsync(int millisecondsTimeout, Handler&& handler)
问题:
C/C++ 11 并不支持匿名的箭头函数(=>),大多数情况下,异步编程适用 lambda 函数式并不能解决代码冗余问题。
当然采用这种方式:可以避免用户代码上下文频繁切换的问题,而且在多个异步调用的情况下,C/C++ 异步代码的维护及编写难易度是呈指数上升的。
解决方案?
那么人们迫切需要寻求一种可以 “高度简化异步编程”,编码复杂度的方式,适用 C/C++ 协同程序会是一个好办法,而可以采用的协同程序类型为以下两种:
1、stackful coroutines
每个协同程序都具备一个独立的计算堆栈。
伪代码:
TResult result = SleepYield(int milliseconds, YieldContext& y);
2、stackless coroutines
await/async # C/C++ 20 co_await, co_yield, co_return
必须被编译器支持,否则用户编码难度及理解成本并不低。
伪代码:
TResult result = co_await SleepYield(int milliseconds)
适用于 C/C++ 的开发人员们,好的一个建议为:
以现有 C/C++ 编程语言及标准支持而言,尽量不要适用 stackless 协同程序,这容易引起不可预测的异步编程疑难杂症,并且人们不升级到 C/C++ 20 的情况下,人们仍无法享受更为易用的协同程序编程。
出于安全及健壮性的考量,人们不应该过早的适用新的编程技术及标准,新的技术普及需要小白鼠,但不意味着小白鼠们,需要拿着贵公司、团队赖以生存的东西来做试错,这是一个错误并不负责任的做法。
现有那些编程语言采用由语言支持的 stackless 协同程序?
1、JavaScript
2、C#、VB.NET
3、C/C++ 20 std
一个有意思的问题:
如果驱动每个协同程序的驱动器(调度器),为多线程架构?那么它还是协同程序?
其实很多人会把协同程序的概念跟单线程挂钩这是错误的,协同程序最基本的定义为:可以理解为由应用程序调度的线程执行单元。
所以:在确保每个协同程序工作流保证 “命令式执行” 的情况下,某个协同程序的处理单元从A工作线程切换为B工作线程并不违背协同程序的概念。
我们在C/C++ 语言中可以设计由 “多线程” 调度驱动的协同程序基础框架类库,此设计可适用于:“stackless”、“stackfull” 类型协同程序。
了解一些基本概念:
P协同程序在A工作线程执行单元上发起了异步的调用、该异步的调用由B工作线程完成并唤醒协同程序,那么从协同程序的工作流(Workflow)上来说,是确保 “命令式执行” 按照顺序完成的。
这好比:操作系统把线程A从#0 CPU,切换到#1 CPU上执行,那么违背线程设计的初衷了吗?显然没有,协程与线程设计思想上是类似的。
多线程调度驱动协同程序引发的所谓 “线程安全” 问题,其与协同程序本身并不相关,这是两个不同领域范畴的问题,不可一概而论。
追求并发式完全异步(Parallel Full Asynchronous)是现代编程追求的一种重要的理念思想,从上述的调度模式上,人们或许可以从中探索到一个潜在的可能性。
从上述描述的内容,我们大家可以获悉:C/C++ 11/14/17 适用 “stackful” 有栈协同程序的编程模型,从编程代码的易用性及可读性而言,相对或许是最高的。
如何设计一套易用的 stackful 有栈式协同程序基础框架类库?
1、人们需要先明白,我们设计基础框架类库最重要的几个指标是什么?
1.1、可维护、可扩展、开闭性
1.2、健壮性
1.3、易用性,框架面向的是普通的编程用户,它们不该涉及框架及基础类库。
1.4、框架所欲解决的问题
2、设计解决特定问题的基础类库框架,我们需要明白框架有哪些 “模块、接口、模型” 的组成成分
如何划分工作领域及职责,这是框架设计者们内部自行抉择的事情,一个好的建议在框架内部代码不可避免被外部用户所获得前提下,将框架内部实现的过于复杂或许会是一个好办法。
3、核心域
3.1、YieldContext
3.2、Scheduling driver
Scheduling driver(调度驱动器)
分以下几类实现方法:
1、单个工作线程,单个调度驱动器
2、单个工作线程,多个调度驱动器
从技术的实现难度上来说:
第一类最容易解决的,实现上述 “核心域” 两个组成部分并不会花费过多的时间,C/C++ 专业开发人员,结合一些库耗费几个小时工作时间则可以轻松办到。
第二类相对难解决些,实现上述 “核心域” 两个组成部分耗费的时间会长很多,或许有些略微的得不偿失,每个工作线程同时驱动多个调度器,从编程用户应用的可能性并不大。
本文将提供第一类的协同程序核心域的两个实现,不过,第二类协同程序核心域的两个实现并不意味着不可以聊一聊。
第二类协同程序核心域的实现,有几种可选的实现方案,但无论哪一种都需要确保在协程让出CPU执行权力的情况下,回到主工作循环继续执行其它的业务,但我们从框架设计可控的前提下,都应该要求回到协同程序调度驱动器,在回到主工作线程循环。
例如:
A协同程序执行,Yield 函数让出CPU使用权,那么则回到驱动器的 Working 驱动器函数中,继续执行下一个协同程序,如果没有协同程序则回到主循环(常见于帧循环架构的服务器)等待下个帧循环执行。
那么,在构建并引导一个有独立执行堆栈的协同程序时,我们需要为每个协同程序构建独立的堆栈内存,人们最好从堆内存中分配,每个栈的大小建议不要超过64KB,大多数协同程序执行来说,执行堆栈内存的需求大约在 16KB~64KB 之间。
那么,此时可以引导当前工作线程切换到有栈协同程序的堆栈上执行特定函数,在协同程序执行 Yield 函数时让出CPU使用权回到主循环并切回原执行堆栈。
但它会引发一个新的问题:如何回到原来的主函数?
那么这需要人们保存上个堆栈的堆栈信息,所以:我们至少需要两个执行堆栈转移上下文:
1、Caller(调用者)
保存调用协同程序的原堆栈信息
2、Callee(被调用)
保存被调用协同程序的堆栈信息
但当我们,Yield 回到主循环时(Callee)则必须要更新保存当前的协同程序堆栈信息,否则我们不能再唤醒协同程序时 “Resume” 回到 “Yield” 让出CPU使用权的位置继续执行。
但某些情况下 “Resume” 也会导致当前执行堆栈上下文保存改变,例如:当我们手段在某个异步函数返回上来控制协同程序的唤醒,则该协同程序继续执行下个协同程序会出现故障。
因为无法在转移回主循环上,那么有以下几个可行的解决方案:
1、丢弃来自 Resume 导致的上下文保存改变
2、单独托管 Resume 导致的上下文保存改变,Callee 不为主循环切入该协同程序的源执行堆栈
但它带来一个新的问题,如果我们不在主循环循环中控制唤醒 Resume,或许会导致潜在的内存泄露问题,当然并非是绝对的。
例如:
执行唤醒时适用:std::function<...>,CPU使用权被转移后不被执行析构释放,那么内存泄露就发生了,但从设计上人们很容易解决该问题,例如:人们可以适用提供受到边界控制的框架内部资源释放管理。
下述提供的代码仅用于 Linux 操作系统平台,其它操作系统平台建议不要适用,跨平台做法不能像下面的偷懒做法。
#include <boost/coroutine/detail/coroutine_context.hpp>
#include <boost/context/detail/fcontext.hpp>
#include <ctime>
#include <unistd.h>
#include <stdlib.h>
#include <setjmp.h>
#include <string.h>
#include <time.h>
#include <exception>
#include <stdexcept>
#include <list>
#include <atomic>
#include <thread>
#include <functional>
inline void* Malloc(const char* file, int line, std::size_t size) noexcept
{
if (size < 1)
{
return NULL;
}
return (void*)malloc(size);
}
inline void Mfree(const char* file, int line, const void* p) noexcept
{
if (NULL != p)
{
free((void*)p);
}
}
class YieldScheduler;
class YieldContext final
{
friend class YieldScheduler;
public:
typedef std::function<void(YieldContext&)> SpawnHander;
private:
YieldContext(YieldScheduler& scheduler) noexcept;
public:
void Yield() noexcept;
void Resume() noexcept;
YieldScheduler* GetScheduler() noexcept;
public:
template <typename T, typename... A>
inline static T* New(A&&... args) noexcept
{
T* p = (T*)Malloc(__FILE__, __LINE__, sizeof(T));
if (NULL == p)
{
return NULL;
}
return new (p) T(std::forward<A&&>(args)...);
}
template <typename T>
inline static void Release(T* p) noexcept
{
if (NULL != p)
{
p->~T();
Mfree(__FILE__, __LINE__, p);
}
}
private:
void Invoke(SpawnHander& f) noexcept;
static YieldContext* New(YieldScheduler& scheduler) noexcept;
private:
static void Release(YieldContext* y) noexcept;
static void Handle(boost::context::detail::transfer_t t) noexcept;
private:
boost::context::detail::fcontext_t callee_;
boost::context::detail::fcontext_t caller_;
SpawnHander* h_;
YieldScheduler& scheduler_;
char stack_[65536];
};
#define YieldContext_Resume(OBJ, FUNC, Y) (OBJ)->FUNC(std::bind(&YieldContext::Resume, (Y)))
#define YieldContext_Yield(Y) (Y)->Yield()
class YieldScheduler
{
friend class YieldContext;
public:
typedef std::function<void()> PostHandler;
typedef YieldContext::SpawnHander SpawnHander;
public:
YieldScheduler();
~YieldScheduler() noexcept;
public:
virtual bool Update() noexcept;
void Spawn(SpawnHander&& handler) noexcept;
void Post(PostHandler&& handler) noexcept;
static YieldScheduler* GetScheduler() noexcept;
private:
static bool ExecuteAllPosts(YieldScheduler* scheduler) noexcept;
static bool ExecuteAllSpawns(YieldScheduler* scheduler) noexcept;
private:
std::list<SpawnHander> spawns_;
std::list<PostHandler> posts_;
static thread_local std::atomic<YieldScheduler*> scheduler_;
};
thread_local std::atomic<YieldScheduler*> YieldScheduler::scheduler_(NULL);
YieldScheduler::YieldScheduler()
{
YieldScheduler* localtion = NULL;
if (!scheduler_.compare_exchange_strong(localtion, this))
{
throw std::runtime_error("Each worker thread is not allowed to run at the same time multiple \"YieldScheduler\".");
}
}
YieldScheduler::~YieldScheduler() noexcept
{
YieldScheduler* localtion = this;
scheduler_.compare_exchange_strong(localtion, NULL);
}
void YieldScheduler::Spawn(SpawnHander&& handler) noexcept
{
if (NULL != handler)
{
spawns_.push_back(std::move(handler));
}
}
void YieldScheduler::Post(PostHandler&& handler) noexcept
{
if (NULL != handler)
{
posts_.push_back(std::move(handler));
}
}
YieldScheduler* YieldScheduler::GetScheduler() noexcept
{
return scheduler_;
}
bool YieldScheduler::Update() noexcept
{
bool nwait = ExecuteAllPosts(this);
nwait |= ExecuteAllSpawns(this);
return nwait;
}
bool YieldScheduler::ExecuteAllPosts(YieldScheduler* scheduler) noexcept
{
bool nwait = false;
std::list<PostHandler>& posts = scheduler->posts_;
for (; ;)
{
auto tail = posts.begin();
auto endl = posts.end();
if (tail == endl)
{
break;
}
nwait = true;
(*tail)();
posts.erase(tail);
}
return nwait;
}
bool YieldScheduler::ExecuteAllSpawns(YieldScheduler* scheduler) noexcept
{
bool nwait = false;
std::list<SpawnHander>& spawns = scheduler->spawns_;
for (; ;)
{
auto tail = spawns.begin();
auto endl = spawns.end();
if (tail == endl)
{
break;
}
SpawnHander* handler = YieldContext::New<SpawnHander>(std::move(*tail));
nwait = true;
spawns.erase(tail);
YieldContext::New(*scheduler)->Invoke(*handler);
}
return nwait;
}
YieldContext::YieldContext(YieldScheduler& scheduler) noexcept
: callee_(NULL)
, scheduler_(scheduler)
, h_(NULL)
{
}
YieldContext* YieldContext::New(YieldScheduler& scheduler) noexcept
{
YieldContext* y = (YieldContext*)Malloc(__FILE__, __LINE__, sizeof(YieldContext));
return new (y) YieldContext(scheduler);
}
YieldScheduler* YieldContext::GetScheduler() noexcept
{
return std::addressof(scheduler_);
}
void YieldContext::Yield() noexcept
{
YieldContext* y = this;
y->caller_ = boost::context::detail::jump_fcontext(y->caller_, y).fctx;
}
void YieldContext::Resume() noexcept
{
YieldContext* y = this;
y->callee_ = boost::context::detail::jump_fcontext(y->callee_, y).fctx;
if (!y->h_)
{
YieldContext::Release(y);
}
}
void YieldContext::Invoke(SpawnHander& f) noexcept
{
YieldContext* y = this;
boost::context::detail::fcontext_t callee = boost::context::detail::make_fcontext(stack_ + sizeof(stack_), sizeof(stack_), &YieldContext::Handle);
y->h_ = std::addressof(f);
y->callee_ = boost::context::detail::jump_fcontext(callee, y).fctx;
if (!y->h_)
{
YieldContext::Release(y);
}
}
void YieldContext::Release(YieldContext* y) noexcept
{
if (NULL != y)
{
y->~YieldContext();
Mfree(__FILE__, __LINE__, y);
}
}
void YieldContext::Handle(boost::context::detail::transfer_t t) noexcept
{
YieldContext* y = (YieldContext*)t.data;
y->caller_ = t.fctx;
{
SpawnHander* h = y->h_;
(*h)(*y);
y->h_ = NULL;
YieldContext::Release(h);
}
boost::context::detail::jump_fcontext(y->caller_, y);
}
/// Demo
static void SleepAsync(int milliseconds, std::function<void(int)>&& handler)
{
std::function<void(int)> h = std::move(handler);
std::thread(
[h, milliseconds] {
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
h(milliseconds);
}).detach();
}
static int SleepYield(int milliseconds, YieldContext& y)
{
int result = 0;
SleepAsync(milliseconds,
[&y, &result](int value)
{
result = value;
YieldContext_Resume(y.GetScheduler(), Post, &y);
});
YieldContext_Yield(&y);
return result;
}
inline uint64_t GetTickCount() noexcept
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
const int mul = 1000 * 1000 * 1000;
uint64_t ticks = ts.tv_nsec;
ticks += ts.tv_sec * mul;
return ticks / 100;
}
int main(int argc, const char* argv[]) noexcept
{
YieldScheduler scheduler;
scheduler.Spawn(
[](YieldContext& y)
{
uint64_t last;
uint64_t now;
last = GetTickCount();
int result = SleepYield(1500, y);
now = GetTickCount();
fprintf(stdout, "result is %d, ticks is %llu\r\n", result, (unsigned long long)(now -last));
last = GetTickCount();
int result2 = SleepYield(1000, y);
now = GetTickCount();
fprintf(stdout, "result2 is %d, ticks is %llu\r\n", result2, (unsigned long long)(now -last));
});
for (;;)
{
if (!scheduler.Update())
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
return 0;
}