
纤程纤程Fiber是 Windows 操作系统提供的概念。什么是纤程呢当我们需要异步执行一些任务时常用的一种做法就是开启一个工作线程在工作线程中执行我们的任务。但是这样存在两个问题由于线程的调度是操作系统内核控制的我们没法准确地确定操作系统何时运行、暂且该线程的执行对于一些轻量级的任务创建一个新的线程去做消耗比较大我们不希望有这种消耗。那么有没有一种机制既能起到新建线程执行任务又没有新建线程消耗那么大呢有这就是纤程。在 Windows 中一个线程中可以有多个纤程用户可以根据需要自由在各个纤程之间切换。如果要在某个线程中使用纤程必须先将该线程切换成纤程模式可以调用如下 API 函数LPVOID ConvertThreadToFiber(LPVOID lpParameter);这个函数不仅将当前线程切换成纤程模式同时也得到线程中第一个纤程我们可以通过这个函数的返回值来引用和操作纤程这个纤程是线程中的“主纤程”但是这个“主纤程”由于没法指定“纤程”函数所以什么也做不了。可以通过参数lpParameter给主纤程传递数据获取当前纤程的数据使用 API 函数PVOID GetFiberData();当在不同纤程之间切换时也会涉及到纤程上下文的切换包括 CPU 寄存器数据的切换在默认情况下x86 系统的 CPU 浮点状态信息不属于 CPU 寄存器的一部分不会为每个纤程都维护一份因此如果你的纤程中需要执行浮点操作将会导致数据被破坏。为了禁用这种行为我们需要ConvertThreadToFiber函数LPVOID ConvertThreadToFiberEx(LPVOID lpParameter, DWORD dwFlags);将第二个参数dwFlags设置为FIBER_FLAG_FLOAT_SWITCH即可。将线程从纤程模式切回至默认的线程模式使用 API 函数BOOL ConvertFiberToThread();上文我们说了默认的主纤程什么都做不了所以我们在需要的时候要创建新的纤程使用 API 函数LPVOID CreateFiber(SIZE_T dwStackSize, LPFIBER_START_ROUTINE lpStartAddress, LPVOID lpParameter);和创建线程的函数很类似参数dwStackSize指定纤程栈大小如果使用默认的大小将该值设置为 0 即可。我们可以通过CreateFiber函数返回值作为操作纤程的“句柄”。纤程函数签名如下VOID WINAPI FIBER_START_ROUTINE(LPVOID lpFiberParameter);当不需要使用纤程时记得调用DeleteFiber删除纤程对象void DeleteFiber(LPVOID lpFiber);在不同纤程之间切换使用 API 函数void SwitchToFiber(LPVOID lpFiber);参数lpFiber即上文说的纤程句柄。和线程存在线程局部存储一样纤程也可以有自己的局部存储——纤程局部存储获取和设置纤程局部存储数据使用 API 函数DWORD WINAPI FlsAlloc(PFLS_CALLBACK_FUNCTION lpCallback); BOOL WINAPI FlsFree(DWORD dwFlsIndex); BOOL WINAPI FlsSetValue(DWORD dwFlsIndex, PVOID lpFlsData); PVOID WINAPI FlsGetValue(DWORD dwFlsIndex);这个四个函数和介绍线程局部存储章节介绍的对应的四个函数用法一样这里就不再赘述了。Windows 还提供了一个获取当前执行纤程的 API 函数PVOID GetCurrentFiber();返回值也是纤程“句柄”。我们来看一个具体的例子#include Windows.h #include string char g_szTime[64] { time not set... }; LPVOID mainWorkerFiber NULL; LPVOID pWorkerFiber NULL; void WINAPI workerFiberProc(LPVOID lpFiberParameter) { while (true) { //假设这是一项很耗时的操作 SYSTEMTIME st; GetLocalTime(st); wsprintfA(g_szTime, %04d-%02d-%02d %02d:%02d:%02d, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); printf(%s\n, g_szTime); //切换回主纤程 //SwitchToFiber(mainWorkerFiber); } } int main() { mainWorkerFiber ConvertThreadToFiber(NULL); int index 0; while (index 100) { index; pWorkerFiber CreateFiber(0, workerFiberProc, NULL); if (pWorkerFiber NULL) return -1; //切换至新的纤程 SwitchToFiber(pWorkerFiber); memset(g_szTime, 0, sizeof(g_szTime)); strncpy(g_szTime, time not set..., strlen(time not set...)); printf(%s\n, g_szTime); Sleep(1000); } DeleteFiber(pWorkerFiber); //切换回线程模式 ConvertFiberToThread(); return 0; }上述代码只有一个主线程主线程在36行切换至新建的纤程pWorkerFiber中由于新建的纤程函数中是一个while无限循环这样 main 函数中38行及以后的代码永远不会执行。输出结果如下我们将纤程函数workerFiberProc19行注释放开这样 main 函数的38就有机会执行了输出结果如下上述代码跳跃步骤是先从 main 函数36行跳到workerFiberProc函数执行在workerFiberProc19行跳回 main 函数38行执行接着周而复始进行下一轮循环直到 main 函数 while 条件不满足退出程序。纤程从本质上来说就是所谓的**协程coroutine**思想Windows 纤程技术让单个线程能按用户的意愿像线程一样做自由切换且没有线程切换那样的开销和不可控性。Windows 最早引入纤程是为了方便将 Unix 单线程程序迁移到 Windows 上来。当然也有人提出调试时当程序执行点在纤程函数内部时调用堆栈对用户是割裂的对于习惯看连续性上下文堆栈的用户来说可能不太友好。协程如果你理解了上面介绍的纤程概念那么协程coroutine应该也很好理解了。我们知道线程是操作系统的内核对象多线程编程时如果线程数过多就会导致频繁的上下文切换这些对性能是是一种额外的损耗。例如在一些高并发的网络服务器编程中使用一个线程服务一个 socket 连接是很不明智的因此现在的主流做法是利用操作系统提供了基于事件模式的异步编程模型用少量的线程来服务大量的网络连接和 IO。但是采用异步和基于事件的编程模型让程序代码变得复杂非常容易出错也提高排查错误的难度。协程是在应用层模拟的线程它避免了上下文切换的额外损耗同时又兼顾了多线程的优点简化了高并发程序的复杂度。还是以上面的高并发的网络服务器为例可以为每一个 socket 连接使用一个协程来处理在兼顾性能的情况下代码也清晰。协程是在1963 年由 Melvin E. Conway USAF, Bedford, MA 等人提出的一个概念且协程的概念是早于线程提出的。但是由于协程是非抢占式的调度无法实现公平的任务调用也无法直接利用多核 CPU 的优势因此我们不能武断地说协程是比线程更高级的技术。尽管协程的概念早于线程提出但是目前主流的操作系统原生 API 并不支持协程技术新兴的一些高级编程语言如golang都是在语言的运行时环境中自己利用线程技术模拟了一套协程。这些语言协程的内部实现上都是是基于线程的思路是维护了一组数据结构和 n 个线程真正的执行还是线程协程执行的代码被扔进一个待执行队列中由这 n 个线程从队列中拉出来执行。这就解决了协程的执行问题。那么协程是怎么切换的呢以 golang 为例golang 对操作系统的各种 IO 函数如 Linux 的 epoll、select windows 的 IOCP 等进行了封装这些封装的函数提供给应用程序使用而其内部调用了操作系统的异步 IO 函数当这些异步函数返回 busy 或 blocking 时golang 利用这个时机将现有的执行序列压栈让线程去拉另外一个协程的代码来执行。由于 golang 是从编译器和语言基础库多个层面对协程做了实现所以golang 的协程是目前各类存在协程概念的语言中实现的最完整和成熟的十万个协程同时运行也毫无压力。带来的优势就是程序员可以在编写 golang 代码的时候可以更多的关注业务逻辑的实现更少的在这些关键的基础构件上耗费太多精力。现今之所以协程技术这么流行是因为大多数设计喜欢使用异步编程以追求程序的性能这就强行的将线性的程序逻辑打乱程序逻辑变得非常的混乱与复杂。对程序状态的管理也变得异常困难例如NodeJS那恶心的层层Callback。在我们疯狂被NodeJS的层层回调恶心到的时候golang 作为名门之后开始进入广大开发者的视野并且迅速的在 Web 后端攻城略地。例如以 Docker 以及围绕 Docker 展开的整个容器生态圈为代表其最大的卖点就是协程技术至此协程技术开始真正的流行与被讨论起来。腾讯公司开源了一套 C/C 版本的协程库 libcohttps://github.com/Tencent/libco有兴趣的读者可以研究一下其实现原理。所以协程技术从来不是什么新东西只是人们为了从重复复杂的底层技术中解脱出来能够快速专注业务开发而带来的产物。万变不离其宗只要我们掌握了多线程编程的技术的核心原理我们也能快速的学习协程技术。专栏总结本章介绍了介绍了目前主流的 C/C 开发环境下的两个操作系统 Windows 和 Linux 系统层面上的线程原理和多线程资源同步技术的方方面面同时基于这些基础知识延伸出了更高级的线程池技术和队列系统也介绍了目前前沿的协程技术。无论某种编程语言和其运行时环境如 Java、Python对操作系统的线程功能增加了多少中间层、封装了多少功能的操作线程提供的线程相关 API 和同步接口是最基础的且由于这些接口是操作系统提供的它们在相当长的时间内都会基本保持不变一旦你理解并熟练使用它们你不仅可以灵活地学习和开发出强大的多线程程序来同时也能快速地理解其他语言中的各种线程同步概念和技术。多线程编程技术是怎么强调也不过份基本功希望读者能够非常熟练地掌握它们这种熟练掌握不仅是理解其原理一定是熟悉到具体的 API 层面来上。