各类纤程/协程使用比较
Posted lsgxeva
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了各类纤程/协程使用比较相关的知识,希望对你有一定的参考价值。
各类纤程/协程使用比较
来源 https://blog.csdn.net/ruhailiu126/article/details/79691839
一:什么是纤程/协程?
纤程(Fiber)是一种最轻量化的线程(lightweight threads)。它是一种用户线程(user thread),让应用程序可以独立决定自己的线程要如何运作。操作系统内核不能看见它,也不会为它进行调度。就像一般的线程,纤程有自己的寻址空间。但是纤程采取合作式多任务(Cooperative multitasking),而线程采取先占式多任务(Pre-emptive multitasking)。应用程序可以在一个线程环境中创建多个纤程,然后手动运行它。纤程不会被自动运行,必须要由应用程序自已指定让它运行,或换到下一个纤程。
二:为什么要使用纤程(纤程的优缺点)?
纤程的优点:
1. 消耗小,切换快,一个进程可以创建成千上万个纤程。
2. 小任务顺序编程很符合人的思维方式, 规避纯异步编程中状态机的复杂性. 使得使用纤程写的程序将更加的直观, 逻辑描述方便, 简化编程.纤程用于化异步为同步, 你可以进行一个异步操作以后就切换纤程,等到异步操作完成以后在切换回来,这样,在逻辑上相关的代码就可以写到一个函数里面,而不用人为的分到多个回调函数中。
3. 没有了线程所谓的安全问题, 避免锁机制
纤程的缺点:
1. 纤程一般只支持所有的纤程函数在一个线程里面跑. 无法充分利用多核CPU, 除非把所有的IO和计算操作都剥离成单独的线程。
2:关于跨平台的纤程的实现和使用资料较少。
三:如何理解纤程?
关于个人理解纤程的关键点:
1)关键点一:纤程用于执行流间的切换。
2)关键点二:纤程切换时,保存当前正在运行的纤程的上下文,恢复到被调用纤程的上下文。
纤程既然用于同一线程之间执行流的切换,为什么不直接采用回调函数调用了,回调函数调用,无法自动保存和恢复被调用函数的上下文信息。
四:关于纤程的windows下纤程的使用及举列子。
一、PVOID ConvertThreadToFiber(PVOID pvParam);
调用这个函数之后,系统为纤程执行环境分配大概200字节的存储空间这个执行环境有以下内容构成:
1、用户定义的值,由参数pvParam参数指定。
2、结构化异常处理链头。
3、纤程内存栈的最高和最低地址,当线程转换为纤程的时候,这也是线程的内存栈。
4、各种CPU寄存器信息,比如堆栈指针寄存器,指令指针寄存器等等。
默认情况下,x86系统的CPU的浮点数状态信息在纤程看来不属于CPU寄存器,因此会导致在纤程中执行一些相关的浮点运算会破坏数据。为了克服这个缺点,你需要呼叫ConvertThreadToFiberEx函数(Windows Vista及其以上版本中才有),并且传递FIBER_FLAG_FLOAT_SWITCH给它的第2个参数dwFlags。
如果一个线程中只有一个纤程,那么是没有必要将该线程转换为纤程的,只有你打算在同一个线程中再创建一个纤程才有转换的必要。要创建一个纤程,使用CreateFiber函数。
二 、 PVOID CreateFiber(DWORD dwStackSize , PFIBER_START_ROUTINE pfnStartAddress , PVOID pvParam):
PVOID CreateFiber( DWORD dwStackSize, // 创建新的堆栈的大小,0表示默认大小 PFIBER_START_ROUTINE pfnStartAddress, // 纤程函数地址 PVOID pvParam); // 传递给纤程函数的参数
当CreateFiber(Ex)函数创建了一个新的堆栈之后,它分配一个新的纤程执行环境结构并初始化之,用户定义的数据通过pvParam参数被保存,新的堆栈的内存空间的最高和最低地址被保存,纤程函数的地址通过pStartAddress参数被保存。 纤程函数的格式必须如下定义:
VOID WINAPI FiberFunc(PVOID pvParam);
这个纤程在第一次被调度的时候,纤程函数被调用,其参数pvParam由CreateFiber(Ex)中的pvParam参数指定。在纤程函数中,你可以做你想做的任何事情。像ConvertThreadToFiber(Ex)函数一样,CreateFiber(Ex)也返回纤程执行环境的内存地址,这个内存地址就像句柄一样,直接标识着一个纤程。
当你使用CreateFiber(Ex)函数创建一个纤程之后,该纤程不会执行,因为系统不会自动调度它。你必须调用函数SwitchToFiber来告诉系统你想要哪个纤程执行:
三、SwitchToFiber函数内部的执行步骤如下: 1、保存当前的CPU寄存器信息,这些信息保存在正在运行的纤程的执行环境中。 2、从将要执行的纤程的执行环境中加载上次保存的CPU寄存器信息。 3、将即将执行的纤程执行环境与线程关联起来,由线程执行指定的纤程。 4、将指令指针设置为保存的值,继续上次的执行。
SwitchToFiber函数是一个纤程能够被调度的唯一的方法,因此,纤程的调度是由用户完全操纵的。纤程的调度和线程的调度无关。一个线程,包含了正在运行的纤程,仍会被其他线程抢占。当一个线程被调度,而它里面有几个纤程,那么只有被选择的那个纤程才会执行,其他纤程的执行需要调用SwitchToFiber函数。
四、DeleteFiber函数一般是由一个纤程调用来删除另一个纤程。
该函数首先清除纤程堆栈,然后删除纤程执行环境。但是,如果参数指定的是一个与当前线程关联的纤程,该函数呼叫ExitThread函数,线程结束,其包含的其他纤程也都结束。因此,DeleteFiber函数一般是由一个纤程调用来删除另一个纤程。
当所有纤程结束了运行,你需要从纤程转换为线程,呼叫ConvertFiberToThread函数。
五、一个线程每次只能执行一个纤程,该纤程与这个线程相关联。
1、你可以使用如下函数来得到正在执行的纤程的执行环境内存地址,返回当前纤程的指针:
PVOID GetCurrentFiber();其实这是个宏
2、通过下函数获取当前纤程数据的指针:
GetFiberData();也是个宏
六、最后,让我们假设一个线程中有2个纤程,总结一下纤程的用法: 1、使用ConverThreadToFiber(Ex)将当前线程转换到纤程,这是纤程F1 2、定义一个纤程函数,用于创建一个新纤程:VOID CALLBACK FiberProc(); 3、纤程F1中调用CreateFiber(Ex)函数创建一个新的纤程F2 4、SwitchToFiber函数进行纤程切换,让新创建的纤程F2执行 5、F2纤程函数执行完毕的时候,使用SwitchToFiber转换到F1 6、在纤程F1中调用DeleteFiber来删除纤程F2 7、纤程F1中调用ConverFiberToThread,转换为线程 8、线程结束。
五:关于跨平台纤程CN_FIBER。
由于在LINUX 及MAC 平台下并没有现成的纤程函数,因此采用了boost库中协程(coroutine),经过多次测试选择了对称协程(symmetric coroutines)。关于boost库的非对称协称和对称协程的区别有以下描述:
In contrast to asymmetric coroutines, where the relationship between caller and callee is fixed, symmetric coroutines are able to transfer execution control to any other (symmetric) coroutine. E.g. a symmetric coroutine is not required to return to its direct caller。
boost协程之间的跳转采用:
Class symmetric_coroutine<>::call_type
Class symmetric_coroutine<>::yield_type
为了跨平台代码移植的需要将boost协程封装为与windows基本一致的接口方式。
相关接口定义如下: 其中CNFiberHandle用于标识fiber句柄。CNFiber类用于封装模拟window下的接口类。
#ifndef _CNFIBER_H #define _CNFIBER_H #pragma once #include <boost/bind.hpp> #include <boost/coroutine/all.hpp> #include "CNBaseType.h" #ifndef CN_WINDOWS #define DLLEXPORT #define FUNCALL __attribute__((stdcall)) #else #define DLLEXPORT __declspec(dllexport) #define FUNCALL __stdcall #endif #define CNAPI typedef void(FUNCALL *CNPFIBER_START_ROUTINE)(void* lpFiberParameter); typedef CNPFIBER_START_ROUTINE CNLPFIBER_START_ROUTINE; //typedef ZPVOID(CNAPI *CN_PFIBER_CALLOUT_ROUTINE)(ZPVOID lpParameter); typedef boost::coroutines::coroutine< void >::pull_type pull_coro_t; typedef boost::coroutines::coroutine< void >::push_type push_coro_t; typedef boost::coroutines::symmetric_coroutine<ZPVOID>::call_type CN_COROUTINE_CALL_TYPE; typedef boost::coroutines::symmetric_coroutine<ZPVOID>::yield_type CN_COROUTINE_YIELD_TYPE; typedef void(*CN_START_COROUTINE)(CN_COROUTINE_YIELD_TYPE& yield, ZPVOID pParam); enum CN_ENUM_CORO_STATE { IN_CN_THREAD=0, IN_CN_CORO =1 }; class CNFiberHandle { public: CNFiberHandle() { m_pCoroutineCallType = NULL; m_pCoroutineYieldType = NULL; m_pFiberStartRoutine = NULL; m_lpParameter = NULL; m_CoroutineFlag = IN_CN_THREAD; }; ~CNFiberHandle() { }; void setCoroutineCallType(CN_COROUTINE_CALL_TYPE * pCoroutineCallType) { m_pCoroutineCallType = pCoroutineCallType; }; void setCoroutineYieldType(CN_COROUTINE_YIELD_TYPE * pCoroutineYieldType) { m_pCoroutineYieldType = pCoroutineYieldType; }; void setFiberStartRoutine(CNPFIBER_START_ROUTINE pFiberStartRoutine) { m_pFiberStartRoutine = pFiberStartRoutine; }; void setParameter(ZPVOID pParameter) { m_lpParameter = pParameter; }; void setCoroutineFlag(CN_ENUM_CORO_STATE CoroutineFlag ) { m_CoroutineFlag = CoroutineFlag; } CN_COROUTINE_CALL_TYPE * getCoroutineCallType( ) { return m_pCoroutineCallType; }; CN_COROUTINE_YIELD_TYPE * getCoroutineYieldType() { return m_pCoroutineYieldType; }; CNPFIBER_START_ROUTINE getFiberStartRoutine() { return m_pFiberStartRoutine; }; ZPVOID getParameter() { return m_lpParameter; }; CN_ENUM_CORO_STATE getCoroutineFlag() { return m_CoroutineFlag; } // void fiberStartRoutine_w(CN_COROUTINE_YIELD_TYPE& coroutineYieldType, ZPVOID lpFiberParameter); private: CN_COROUTINE_CALL_TYPE * m_pCoroutineCallType; CN_COROUTINE_YIELD_TYPE * m_pCoroutineYieldType; CNPFIBER_START_ROUTINE m_pFiberStartRoutine; ZPVOID m_lpParameter; CN_ENUM_CORO_STATE m_CoroutineFlag; }; typedef void(CNAPI *CNPFIBER_START_ROUTINE_W)(CN_COROUTINE_YIELD_TYPE& coroutineYieldType, CNFiberHandle* pFiberHandle); typedef CNPFIBER_START_ROUTINE_W CNLPFIBER_START_ROUTINE_W; void fiberStartRoutine_w(CN_COROUTINE_YIELD_TYPE& coroutineYieldType, CNFiberHandle* pFiberHandle); class CNFiber { public: CNFiber(); ~CNFiber(); //Converts the current thread into a fiber. ZPVOID ConvertThreadToFiber(ZPVOID lpParameter); //Allocates a fiber object, assigns it a stack, and sets up execution to begin at the specified start address, typically the fiber function. ZPVOID CNAPI CreateFiber( ZUINT32 dwStackSize, CNPFIBER_START_ROUTINE lpStartAddress, ZPVOID lpParameter ); //Deletes a fiber. void CNAPI DeleteFiber( ZPVOID lpFiber ); //Is an application - defined function used with the CreateFiber function. void FiberProc( ZPVOID lpParameter ); //Returns the address of the current fiber. ZPVOID GetCurrentFiber(void); //Returns the fiber data associated with the current fiber. ZPVOID CNAPI GetFiberData(void); //Schedules a fiber. The caller must be a fiber. void CNAPI SwitchToFiber( ZPVOID lpFiber ); private: void SetCurrentFiber(ZPVOID pFiber); private: CNFiberHandle *m_pCurrentFiber; }; #endif //_CNFIBER_H
五:Tars协程。
tars中的协程在用于任务调度时与以上介绍的协程使用上有所区别。在官方的举例中:
1) 继承自Coroutine
2) 重载实现handle。
3) /**
* 初始化
* @iNum, 表示要运行多个协程,即会有这个类多少个coroRun运行
* @iTotalNum,表示这个线程最多包含的协程个数
* @iStackSize,协程的栈大小
*/
void setCoroInfo(uint32_t iNum, uint32_t iMaxNum, size_t iStackSize);
调用 setCorolInfo 初始化相关信息。
4) Start 启动
通过查看源码可以看到Coroutine继承自线程,handle相当于一般线程类中run函数。
因此本质上是另外开启了1个线程,在开启的线程中启动了iNum个数的协程,协程执行的函数为handle函数(相互不影响)。
由于目前并没有看到官方,协程与协程,协程与线程切换的使用样例,后续再补充。
================== End
以上是关于各类纤程/协程使用比较的主要内容,如果未能解决你的问题,请参考以下文章