各类纤程/协程使用比较

   最近在学习tars过程中,遇到了以前用的协程的概念。把以前记录的笔记整理分享出来。最后记录tar协程的使用

一:什么是纤程/协程?

   纤程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);

这个纤程在第一次被调度的时候,纤程函数被调用,其参数pvParamCreateFiber(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 4SwitchToFiber函数进行纤程切换,让新创建的纤程F2执行 5F2纤程函数执行完毕的时候,使用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函数(相互不影响)。

    由于目前并没有看到官方,协程与协程,协程与线程切换的使用样例,后续再补充。


展开阅读全文

没有更多推荐了,返回首页