kylin代码阅读

copy自   http://dirlt.com/kylin.html 。


1 kylin

kylin是baidu in-house的异步编程框架,提供CPU,Network以及Disk异步操作接口,并且内置许多常用编程组件包括定时器和内存池等。

相关材料

  • 异步编程_百度文库 : http://wenku.baidu.com/view/6fa262f67c1cfad6195fa74e.html
  • 三谈火车票系统_新浪轻博客 : http://qing.blog.sina.com.cn/tj/85c4105033000ab2.html
  • SEDA: An Architecture for Well-Conditioned, Scalable Internet Services http://www.eecs.harvard.edu/~mdw/papers/seda-sosp01.pdf
  • Why Threads Are a Bad Idea http://www.stanford.edu/class/cs240/readings/threads-bad-usenix96.pdf
导读:(我添加的)
线程库:CThreadPool  提供按任务多线程执行能力
内存管理:CTranBufPool, 内存管理能力,
内存管理的包装系列函数:BufHandle  系列
以下都是利用上面的基础资源构建
执行cpu任务操作包装类:ExecMan
磁盘(文件)任务类:DiskMan
网络交互任务类:NetworkMan,单独为poll增加了类Epoller   将这两个类封装以下,构成:Socket类
缺点:
1. 代码没有注释,简直没法读,而且没用用户文档,感觉不想让人用(这是未广泛运用的主要原因)
2. 对外开放的接口利用太麻烦,用的人不读源码根本不知道该怎么用
3. 有些变量起名太差,可读性不好
优点:
线程池、内存管理都有了,基本的cpu,文件、网络任务都包括了

总体评价:感觉像是一个练手作品(说这句话,太自大了,主要是我代码能力太差,没读太明白。:))

1.1 share

公共组件代码

1.1.1 atomic

最主要实现了atomic add/swap/cas三个操作。

[cpp]  view plain copy
  1. // return old value  
  2. static inline int atomic_add(volatile int *count, int add) {  
  3.     __asm__ __volatile__(  
  4.         "lock xadd %0, (%1);"  
  5.         : "=a"(add)  
  6.         : "r"(count), "a"(add)  
  7.         : "memory");  
  8.     return add;  
  9. }  
  10.   
  11. // return old value  
  12. static inline int atomic_swap(volatile void *lockword, int value) {  
  13.     __asm__ __volatile__(  
  14.         "lock xchg %0, (%1);"  
  15.         : "=a"(value)  
  16.         : "r"(lockword), "a"(value)  
  17.         : "memory");  
  18.     return value;  
  19. }  
  20.   
  21. // return old value  
  22. // 语义是这样的  
  23. // 如果*lockword==comperand,那么*lockword=exchange  
  24. // 否则不进行任何操作  
  25. // 返回原始的*lockword  
  26.   
  27. // 对于cmpxchg x y的语义是这样的  
  28. // 如果y==%%eax,那么x->y.否则不变。然后y(原始)->%%eax  
  29. static inline int atomic_comp_swap(volatile void *lockword,  
  30.                                    int exchange,  
  31.                                    int comperand)  
  32. {  
  33.     __asm__ __volatile__(  
  34.         "lock cmpxchg %1, (%2)"  
  35.         :"=a"(comperand)  
  36.         :"d"(exchange), "r"(lockword), "a"(comperand));  
  37.     return comperand;  
  38. }     

汇编语言可以参考内核汇编语言规则(转)

然后再上面封装了一系列原子操作。封装的一系列原子操作还是比较好理解的。


[cpp]  view plain copy
  1. #define AtomicGetValue(x)    (atomic_comp_swap(&(x), 0, 0))  
  2. #define AtomicSetValue(x, v)    (atomic_swap(&(x), (v)))  
  3. #define AtomicSetValueIf(x, v, ifn)(atomic_comp_swap(&(x), (v), ifn))  
  4. #define AtomicDec(c)    (atomic_add(&(c), -1) - 1)  
  5. #define AtomicInc(c)    (atomic_add(&(c), 1) + 1)  

1.1.2 spinlock

spinlock直接使用atomic提供的原子操作来实现,理解起来倒不是很麻烦

[cpp]  view plain copy
  1. static inline void spin_lock(volatile int *lock) {  
  2.     int l;  
  3.     int i = 10;  
  4.     int id = thread_getid();  
  5.     //l==0的话说明原来lock==0然后被置为id  
  6.     //l==id的话说明原来lock==id那么就不必在进行加锁操作  
  7.     for (l=atomic_comp_swap(lock, id, 0);  
  8.          l!=0 && l!=id;  
  9.          l=atomic_comp_swap(lock, id, 0)  
  10.          ) {  
  11.         if (i --) {  
  12.             nop();  
  13.         }  
  14.         else {  
  15.             // 进行10次nop之后如果没有得到锁的话  
  16.             // 那么就直接relinquish CPU  
  17.             // #define thread_yield sched_yield  
  18.             i = 10;  
  19.             thread_yield();  
  20.         }  
  21.     }  
  22. }  
  23.   
  24. // 返回值可以知道之前lock是否锁在自己这里  
  25. // 如果为false的话表示自己并没有锁  
  26. static inline bool spin_unlock(volatile int *lock) {  
  27.     int id = thread_getid();  
  28.     return id == atomic_comp_swap(lock, 0, id);  
  29. }  
  30.   
  31. static inline bool spin_trylock(volatile int *lock) {  
  32.     int id = thread_getid();  
  33.     int owner = atomic_comp_swap(lock, id, 0);  
  34.     return (owner==0 || owner==id);  
  35. }  


在spinlock.h下面有一个token实现。token语义非常简单,如果token==0的话那么这个令牌没有被任何人获得, 如果token!=0的话,那么令牌被token标记的对象获取了。token可以是pid,也可以是tid.

[cpp]  view plain copy
  1. static inline int token_acquire(volatile int *token, int id) {  
  2.     return atomic_comp_swap(token, id, 0);  
  3. }  
  4.   
  5. static inline int token_release(volatile int *token, int id) {  
  6.     return atomic_comp_swap(token, 0, id);  
  7. }  
  8.   
  9. static inline int token_transfer(volatile int *token, int oldid, int newid) {  
  10.     return atomic_comp_swap(token, newid, oldid);  
  11. }  
  12.   
  13. static inline int token_set(volatile int *token, int id) {  
  14.     return atomic_comp_swap(token, id, *token);  
  15. }  

1.1.3 cycle

提供开销更小的计时器,使用读取CPU的time stamp counter.这个内容表示自计算机启动以来的CPU运行周期。

[cpp]  view plain copy
  1. static inline uint64 rdtsc() {  
  2.     unsigned int lo, hi;  
  3.     /* We cannot use "=A", since this would use %rax on x86_64 */  
  4.     __asm__ __volatile__ (  
  5.         "rdtsc"  
  6.         : "=a" (lo), "=d" (hi));  
  7.     return (uint64)hi << 32 | lo;  
  8. }  

得到周期之后我们必须转换称为时间(s)。周期转换称为时间就是除CPU的主频。得到CPU主频的话没有什么特别好的办法, 一种简单的方法是通过等待1s然后得到tsc差。对于Linux操作系统的话可以通过读取proc文件系统获得

[[email protected]]$ cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 12
model name      : Intel(R) Xeon(R) CPU           E5620  @ 2.40GHz
stepping        : 2
cpu MHz         : 2400.186
cache size      : 256 KB
physical id     : 0
siblings        : 16
core id         : 0
cpu cores       : 16
fpu             : yes
fpu_exception   : yes
cpuid level     : 11
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm syscall nx lm pni monitor ds_cpl est tm2 cx16 xtpr
bogomips        : 4803.76
clflush size    : 64
cache_alignment : 64
address sizes   : 40 bits physical, 48 bits virtual
power management:

1.1.4 support

从这里面我们可以学习到如何进行系统调用,阅读一下<asm/unistd.h>可以找到系统调用号,然后使用syscall来发起。

[cpp]  view plain copy
  1. #include <unistd.h>  
  2. #include <sys/syscall.h>  
  3. #include <cstdio>  
  4. int main() {  
  5.     printf("%lu\n",syscall(__NR_gettid));  
  6.     return 0;  
  7. }  

1.1.5 futex

关于futex的话可以看看下面这些链接

  • http://en.wikipedia.org/wiki/Futex
  • http://linux.die.net/man/2/futex
  • http://blog.csdn.net/Javadino/article/details/2891385
  • http://blog.csdn.net/Javadino/article/details/2891388
  • http://blog.csdn.net/Javadino/article/details/2891399
  • http://people.redhat.com/drepper/futex.pdf

尤其是最后一篇文章可以好好看看,讲到了关于如何使用futex.futex使用需要用户态和内核态的配合,用户态处理一些uncontented case, 而对于contented case的话交给内核态处理。在实际应用上发现大部分情况都是uncontented case都可以在用户态解决而不用陷入内核态。 如果想要深入了解的话,看看pthread里面同步组件的实现。

这里我们简单地介绍一下kylin里面使用futex实现的功能,先看看futex结构

[cpp]  view plain copy
  1. struct futex {  
  2.     volatile int lock; // futex shared address  
  3.     volatile int count;  
  4. };  

1.1.5.1 sema  (不明白为啥不用系统的Semaphore实现?)

可以认为是操作系统里面的PV实现.count就是资源数目,lock始终==0.理解起来并不会很麻烦。

[cpp]  view plain copy
  1. static inline int futex_sema_down(struct futex* pf, struct timespec* timeout, bool interruptable) {  
  2.     // 首先在用户态尝试取资源  
  3.     // 如果n>0的话,说明资源OK,那么就不需要陷入内核态进行wait.  
  4.     int n = atomic_add(&pf->count, -1);  
  5.     if (n <= 0) {  
  6.   retry:  
  7.         if (0 == sys_futex(&pf->lock, FUTEX_WAIT, 0, timeout)) {  
  8.             return 0;  
  9.         }  
  10.         switch (errno) {  
  11.             case ETIMEDOUT:  
  12.                 atomic_add(&pf->count, 1);  
  13.                 return ETIMEDOUT;  
  14.             case EINTR:  
  15.                 if (!interruptable)  
  16.                     goto retry;  
  17.                 atomic_add(&pf->count, 1);  
  18.                 return EINTR;  
  19.             default:  
  20.                 RaiseError(IMPOSSIBLE__Can_not_lock_in_futex_sema_down);  
  21.         }  
  22.     }  
  23.     return 0;  
  24. }  
  25. static inline int futex_sema_up(struct futex* pf) {  
  26.     int retry;  
  27.     // 首先在用户态释放资源  
  28.     // 如果n<0的话,说明存在等待资源的waiters,我们必须陷入内核态wakeup.  
  29.     int n = atomic_add(&pf->count, 1);  
  30.     if (n < 0) {  
  31.         retry = 10;  
  32.         // 这个地方写得非常仔细需要重试  
  33.         while (1 != (n=sys_futex(&pf->lock, FUTEX_WAKE, 1, NULL))) {  
  34.             /* it means the downer decreases the count but not yet start waiting 
  35.              *   --- may be interrupted near the retry label in the above function; 
  36.              * so we have to wait and retry. 
  37.              */  
  38.             if (retry --) {  
  39.                 nop();  
  40.             }  
  41.             else {  
  42.                 retry = 10;  
  43.                 thread_yield();  
  44.             }  
  45.         }  
  46.         return n;  
  47.     }  
  48.     return 0;  
  49. }  

1.1.5.2 cond(为啥不用pthread_cond?)

这里cond和pthread_cond是有差别的,这里的cond没有和任何mutex相关。kylin这里认为count==0的时候,那么condition才被满足。

[cpp]  view plain copy
  1. static inline int futex_cond_wait(struct futex* pf, struct timespec* timeout, bool interruptable) {  
  2.     /* I dont know whether it is a bug of linux kernel. 
  3.      * Sometimes, sys_futex(.., FUTEX_WAIT, ..) returns 0, but the condition is not satisfied. 
  4.      * So we have to check the condition again after return. 
  5.      */  
  6.     while (0 < AtomicGetValue(pf->count)) {  
  7.         sys_futex(&pf->lock, FUTEX_WAIT, 0, timeout);  
  8.         switch (errno) {  
  9.             case ETIMEDOUT:  
  10.                 return ETIMEDOUT;  
  11.             case EINTR:  
  12.                 if (interruptable) {  
  13.                     return EINTR;  
  14.                 }  
  15.             default:  
  16.                 break;  
  17.         }  
  18.     }  
  19.     return 0;  
  20. }  
  21.   
  22. static inline int futex_cond_signal(struct futex* pf) {  
  23.     int n = atomic_add(&pf->count, -1);  
  24.     if (1 == n) {  
  25.         pf->lock = 1; // 一旦触发之后,那么就不能够再进行wait了。  
  26.         mfence_c();  
  27.         return sys_futex(&pf->lock, FUTEX_WAKE, 65535, NULL);// I hope 65535 is enough to wake up all  
  28.     }  
  29.     return 0;  
  30. }  

1.1.5.3 event

这里的event名字取得也相当的奇怪。这里count实际上有两个状态,>=0以及<0(LARGE_ENOUGH_NEGATIVE).对于count>=0的状态时候, 可以认为当前是没有signaled的需要wait,如果count为<0(LARGE_ENOUGH_NEGATIVE)的时候是有signal的状态的不需要wait。

[cpp]  view plain copy
  1. static inline int futex_event_wait(struct futex* pf, struct timespec* timeout, bool interruptable) {  
  2.     // 如果不是signaled状态的话  
  3.     int n = atomic_add(&pf->count, 1);  
  4.     if (0 <= n) {  
  5.   retry:  
  6.         if (0 == sys_futex(&pf->lock, FUTEX_WAIT, 0, timeout))  
  7.             return 0;  
  8.   
  9.         switch (errno) {  
  10.             case ETIMEDOUT:  
  11.                 atomic_add(&pf->count, -1);  
  12.                 return ETIMEDOUT;  
  13.             case EINTR:  
  14.                 if (!interruptable)  
  15.                     goto retry;  
  16.                 atomic_add(&pf->count, -1);  
  17.                 return EINTR;  
  18.             default:  
  19.                 RaiseError(IMPOSSIBLE__Can_not_lock_in_futex_sema_down);  
  20.         }  
  21.     }  
  22.     else {  // else signaled  
  23.         AtomicSetValue(pf->count, LARGE_ENOUGH_NEGATIVE);  
  24.     }  
  25.     return 0;  
  26. }  
  27.   
  28. static inline int futex_event_signal(struct futex* pf, bool reset) {  
  29.     int m, n, retry;  
  30.     // 看看当前是否signaled  
  31.     // 如果没有signal的话,那么需要wakeup这些waiters.  
  32.     n = AtomicSetValue(pf->count, reset ? 0 : LARGE_ENOUGH_NEGATIVE);  
  33.     if (0 < n) {  
  34.         retry = 10;  
  35.         m = n;  
  36.         do {  
  37.             n -= sys_futex(&pf->lock, FUTEX_WAKE, n, NULL);  
  38.             if (0 == n)  
  39.                 return m;  
  40.             if (retry --) {  
  41.                 nop();  
  42.             }  
  43.             else {  
  44.                 retry = 10;  
  45.                 thread_yield();  
  46.             }  
  47.         } while (1);  
  48.     }  
  49.     return 0;  
  50. }  
  51.   
  52. static inline void futex_event_reset(struct futex* pf) {  
  53.     int n, retry = 10;  
  54.     do {  
  55.         n = AtomicSetValueIf(pf->count, 0, LARGE_ENOUGH_NEGATIVE);  
  56.         if (0<=n || LARGE_ENOUGH_NEGATIVE==n) {  
  57.             return;  
  58.         }  
  59.         if (retry --) {  
  60.             nop();  
  61.         }  
  62.         else {  
  63.             retry = 10;  
  64.             thread_yield();  
  65.         }  
  66.     } while (1);  
  67. }  

1.2 kylin

异步框架代码

1.2.1 Async

kylin对于用户来说首先需要了解的概念就在Async.h文件里面,主要是下面两个类

[cpp]  view plain copy
  1. typedef void (*JOB_PROC)(Job*);  
  2.   
  3. // 对于Job这个内容我们稍后在ThreadPool部分会有详细分析  
  4. struct Job {  
  5.     DLINK link; // 使用link的话可以将Job在JobQ中串联起来可以很方便地取消  
  6.     JOB_PROC fProc; //线程池里面包含JobQ,每取一个Job出来之后就执行fProc.  
  7. };  
  8.   
  9. class CAsyncClient;  
  10. struct AsyncContext : Job {  
  11.     APF_ERROR nErrCode; // 发起调用之后返回的error_code  
  12.     int nAction; // 发起什么调用  
  13.     CAsyncClient *pClient; // 应该使用什么client来处理  
  14. };  
  15.   
  16. class CAsyncClient  
  17. {  
  18. protected:  
  19.     // m_nId仅仅是一个编号,每次创建一个AsyncClient都会全局+1  
  20.     // m_nHostId非常重要,使用这个可以将Job控制丢到哪个线程执行  
  21.     int m_nId, m_nHostId;  
  22.     volatile int m_nRef;  
  23.     CAsyncClient(CAsyncClient* pHost);  
  24.     CAsyncClient();  
  25.     virtual ~CAsyncClient();  
  26. public:  
  27.     int GetId() const { return m_nId; }  
  28.     int GetAsyncId() const { return m_nHostId; }  
  29.     int GetHostThreadId() const;  
  30.     bool IsInHostThread() const;  
  31.     void SetHost(CAsyncClient* pHost);  
  32.     virtual int AddRef() {  
  33.         return AtomicInc(m_nRef);  
  34.     }  
  35.     virtual int Release() {  
  36.         return AtomicDec(m_nRef);  
  37.     }  
  38.     virtual int GetRef() {  
  39.         return AtomicGetValue(m_nRef);  
  40.     }  
  41.     virtual void OnCompletion(AsyncContext* pCtx) = 0; // 用户需要重写这个过程  
  42. };  

对于用户来说使用过程大致是这样的:

  • 创建一个CAsyncClient client实例.当然是我们自己需要继承CAsyncClient重写自己的类。
  • 创建一个AsyncContext ctx(或者是集成AsyncContext).然后将ctx和client绑定。
  • 发起调用op,传入这个ctx,为了方便理解包装成为Task(op,ctx)放入线程池。可能会设置nAction字段。
  • 线程池取出Task,结合ctx调用op.将op返回值放入APF_ERROR里面。
  • 然后根据ctx关联的client,调用client的OnCompletion方法。
  • 调用OnCompletion方法的话会根据ctx里面的标记,可以直接在工作线程调用,也可以丢入CPU线程调用。

可以看到在实现时候,最好一个client就绑定几个相关的ctx最方便了。这里有一个地方需要特别关注就是引用计数。因为C++本身没有GC实现,所以我们必须自己来管理内存分配和释放。 因为client可以一次多个调用,而在OnCompletion里面根本不知道谁先完成谁后完成,也就不能够确定释放责任了。通过引用计数可以很好地解决这个问题。 如果我们直接继承CAsyncClient的话,内部是有引用计数实现的,非常方便我们只需要如何适当地使用就OK了。关于如何适当使用,谢谢sunxiao同学在这里的建议。

  • 一旦发起一次异步调用,那么首先AddRef().当然需要确保这个调用内部没有帮助我们AddRef.
  • 我们不需要显示地DecRef(),因为这个事情在线程池fProc里面调用了Release.

1.2.2 ThreadPool

1.2.2.1 Overview

线程池很简单,取出一个Job出来执行就多了。但是为了更好地理解kylin有必要看看线程池接口/实现。

[cpp]  view plain copy
  1. typedef void (*THREAD_INIT_PROC)(int type, int id); // id表示这个线程的逻辑编号  
  2.   
  3. class CThreadPool  
  4. {  
  5.     bool m_bShareQ; // 是否所有线程共享一个Q  
  6.     int m_nWorkers, m_nMaxWorkers; // 当前线程数和最大线程数  
  7.     volatile int m_nJobs; // 当前有多少个Jobs  
  8.     thread_t *m_hWorkerThreads; // 每个线程的thread结构  
  9.     WorkerContext *m_pContexts; // 每个线程的context  
  10.     int m_nType; // 什么类型线程池,TT_EXEC,TT_NETWORK,TT_DISK  
  11.     THREAD_INIT_PROC m_fInit; // 线程初始化回调函数  
  12.   
  13.     int _AddWorker(int nAdd); // 增加多少个工作线程  
  14.     int _DelWorker(int nDel, bool bFinal); // 取消多少个工作线程  
  15.   public:  
  16.     CThreadPool(int type, int nMaxWorkers, bool bShareQ);  
  17.     virtual ~CThreadPool();  
  18.     int Start(int nWorkers, THREAD_INIT_PROC fInit=NULL);  
  19.     void Stop();  
  20.     void QueueJob(Job* pJob, int nWhich);  
  21.     void QueueEmergentJob(Job* pJob, int nWhich);  
  22.     bool CancelJob(Job* pJob, int nWhich);  
  23. };  

对于线程池部分的话我们比较关心这么几件事情:

  • 如何增加删除线程的
  • 线程是如何进行工作的
  • 如何往线程里面增加取消任务
1.2.2.2 How Thread Works

了解线程是怎么工作的,可以看看线程执行的函数是怎么定义的

[cpp]  view plain copy
  1. static void*  
  2. WorkerProc(void* pData)  
  3. {  
  4.     WorkerContext* pCtx = (WorkerContext*)pData;  
  5.     JobQ* pJobQ = pCtx->pJobQ;  
  6.     Job* pJob;  
  7.   
  8.     TRACE4("%s worker#%d started...\n", ThreadType2Str(pCtx->type), pCtx->id);  
  9.     pCtx->thread_id = thread_getid();  
  10.     if (pCtx->fInit) { // 如果有初始化函数的话那么执行初始化函数  
  11.         pCtx->fInit(pCtx->type, pCtx->id);  
  12.     }  
  13.     while (1) {  
  14.         pJob = pJobQ->pop_front(); // 每次得到一个Job  
  15.         ASSERT_NOT_EQUAL((Job*)NULL, pJob);  
  16.         if (pJob->fProc != 0) { // 如果是普通Job的话那么是调用里面的Job::fProc过程  
  17.             pCtx->bDoing = true;  
  18.             pJob->fProc(pJob);  
  19.             pCtx->bDoing = false;  
  20.         }  
  21.         else { // 否则是控制Job,主要是用于结束线程使用的  
  22.             ControlJob* pCtl = (ControlJob*)pJob;  
  23.             if (!pCtl->fProc(pCtl, pCtx)) {  
  24.                 break;  
  25.             }  
  26.         }  
  27.     }  
  28.     TRACE4("%s worker#%d stopped.\n", ThreadType2Str(pCtx->type), pCtx->id);  
  29.     return NULL;  
  30. }  

普通的Job会在每个Man里面单独提到,我们看看控制Job是怎么定义的。在ThreadPool里面就有一个TermianationJob.

[cpp]  view plain copy
  1. struct TerminationJob : ControlJob {  
  2.     int id;  
  3. };  
  4.   
  5. static bool  
  6. TerminateWorker(ControlJob* pCtl, WorkerContext* pCtx)  
  7. {  
  8.     TerminationJob* pT = (TerminationJob*)pCtl;  
  9.     if (pT->id!=-1 && pT->id!=pCtx->id) { // 如果因为共享队列而没有让对应线程得到Job的话,那么重新放入这个Job.  
  10.         pCtx->pJobQ->push_back((Job*)pCtl); // should be shared queue  
  11.         thread_yield();                     // re-enqueue this job until the owner consumes it  
  12.         return true;  
  13.     }  
  14.     return false;  
  15. }  

通过这种方式来通知线程主动退出。理论上因为shared Queue可能会造成所有永远不会退出但是实际应该不会。

1.2.2.3 AddWorker & DelWorker

AddWorker非常简单

[cpp]  view plain copy
  1. int CThreadPool::_AddWorker(int nAdd)  
  2. {  
  3.     int i;  
  4.   
  5.     for (i=0; i<nAdd && m_nWorkers<m_nMaxWorkers; i++) {  
  6.         m_pContexts[m_nWorkers].fInit = m_fInit;  
  7.         if (m_pContexts[m_nWorkers].pJobQ == NULL) {  
  8.             m_pContexts[m_nWorkers].pJobQ = new JobQ; // 会为每一个WorkerContext分配一个JobQ.对于共享Q的话在初始化就分配好了。  
  9.         }  
  10.         if (0 != thread_create(&m_hWorkerThreads[m_nWorkers], NULL, WorkerProc, &m_pContexts[m_nWorkers])) { // 然后启动线程即可  
  11.             PERROR("thread_create");  
  12.             break;  
  13.         }  
  14.         m_nWorkers ++;  
  15.     }  
  16.     return i;  
  17. }  

DelWorker因为有ControlJob的辅助所以可以很好地解决,只需要在每个线程后面增加一个TerminationJob即可

[cpp]  view plain copy
  1. int CThreadPool::_DelWorker(int nDel, bool bFinal)  
  2. {  
  3.     TerminationJob *pTerminations = new TerminationJob[nDel];  
  4.     int i;  
  5.   
  6.     TRACE4("%s start terminating %d workers...\n", ThreadType2Str(m_nType), nDel);  
  7.     for (i=0; i<nDel && m_nWorkers>0; i++) {  
  8.         m_nWorkers --;  
  9.         DLINK_INITIALIZE(&pTerminations[m_nWorkers].link);  
  10.         pTerminations[m_nWorkers].fZero = 0;  
  11.         pTerminations[m_nWorkers].fProc = TerminateWorker;  
  12.         pTerminations[m_nWorkers].id = bFinal ? -1 : m_nWorkers;  
  13.         m_pContexts[m_nWorkers].pJobQ->push_back((Job*)&pTerminations[m_nWorkers]);  
  14.     }  
  15.     for (int j=m_nWorkers; j<i+m_nWorkers; j++) {  
  16.         TRACE4("%s wait for worker #%d.\n", ThreadType2Str(m_nType), j);  
  17.         thread_join(m_hWorkerThreads[j], NULL);  
  18.     }  
  19.     TRACE4("%s end terminating workers.\n", ThreadType2Str(m_nType));  
  20.     delete[] pTerminations;  
  21.     return i;  
  22. }  

1.2.2.4 QueueJob & CancelJob

相对来说QueueJob也更加简单一些,直接投递到某个线程对应的WorkerContext里面即可。

[cpp]  view plain copy
  1. void QueueJob(Job* pJob, int nWhich) {  
  2.      int nJobs = atomic_add(&m_nJobs, 1);  
  3.     if (-1 == nWhich) {  
  4.         nWhich = nJobs % m_nWorkers;  
  5.     }  
  6.     atomic_add(&m_pContexts[nWhich].nJobs, 1);  
  7.     m_pContexts[nWhich].pJobQ->push_back(pJob);  
  8. }  

而CancelJob则是通过加锁替换这个Job来完成的,还是比较精巧的

[cpp]  view plain copy
  1. static void  
  2. DoNothing(Job* pJob)  
  3. {  
  4.     free(pJob);  
  5. }  
  6.   
  7. bool CThreadPool::CancelJob(Job* pJob, int nWhich)  
  8. {  
  9.     Job* p = ZeroAlloc<Job>(); // 分配一个Job,而DoNothing就是将其释放掉  
  10.     p->fProc = DoNothing;  
  11.     if (m_pContexts[nWhich].pJobQ->replace(pJob, p)) { // replace这个工作是一个加锁完成的  
  12.         return true;  
  13.     }  
  14.     free(p); // 如果没有Cancel的话那么返回失败但是也会释放掉内存  
  15.     return false;  
  16. }  

1.2.3 TranBuf

TranBuf.h CTranBufPool是一个内存分配器。对于很多系统来说,合理地使用资源是非常必要的。

作者linsd对于内存分配器看法是这样的:

要得到稳定的高吞吐,对内存的合理使用是必要条件。是否用Ring Buffer倒不一定,简单的buffer pool效果也差不多。另外,为了应付极限情况,还需要为buffer请求分级,当资源不足时优先给紧急请求。也可设定高低几条watermark,让各种复杂条件下的资源使用变得平顺。

了解一下真实系统里面定制化的内存分配器是非常有帮助的(相对应地来说 TCMalloc 是通用内存分配器).

1.2.3.1 Overview

首先看看CTranBufPool的数据结构,看看里面每个字段含义和作用.对于TranBuf来说的话内部 本质还是一个sample allocator,也是按照固定的BlockSize来进行分配的。构造函数可以看到水位线三个阈值都是0.

[cpp]  view plain copy
  1. class CTranBufPool : public CBufPoolV {  
  2.   struct Handle { // 每个BlockSize字节内存内存由一个Handle管理.  
  3.     DLINK link; // 分配出来之后多个Handle组成环形双向链表.  
  4.     char* pBuffer; // 一个BlockSize的内存.  
  5.     Handle* pRealHdl; // 真实Handler.这个会在后面解释.  
  6.     int nRef; // 引用计数.  
  7.     int nConsBuf; // 对于自己引用的pBuffer后面还有多少个连续内存.  
  8.   };  
  9.   typedef TLinkedList<Handle> FreeList; //  
  10.   typedef std::map<char*, Handle*> BufferMap; // buffer和Handle映射.  
  11.   
  12.   FreeList m_FreeList;  
  13.   BufferMap m_BufferMap;  
  14.   
  15.   // m_nBlockSize 每个sample object即BlockSize  
  16.   // m_nBufferSize 1次连续开辟多少字节.  
  17.   // m_nBlockBase log2(BlockSize)  
  18.   int m_nBlockSize, m_nBufferSize, m_nBlockBase;  
  19.   
  20.   // m_nAlloc 一次开辟多少个BlockSize.其中m_nBufferSize=nAlloc*m_nBlockSize  
  21.   // m_nMaxBuffers 最多分配多少个Blcok  
  22.   // m_nBuffers 当前分配了多少个Block  
  23.   // m_nWaterMarks 分为3个水位线  
  24.   int m_nAlloc, m_nMaxBuffers, m_nBuffers, m_nWaterMarks[3];  
  25.   
  26.   // m_nMin. 一开始至少分配m_nMin*nAlloc个Block  
  27.   // m_nMax 最多分配m_nMax*nAlloc哥block.其中m_nMaxBuffers=m_nMax*nAlloc.  
  28.   int m_nMin, m_nMax;  
  29. };  
  30.   
  31.   CTranBufPool(const char* name, int nCategory) : CBufPoolV(name, nCategory) {  
  32.     m_nBuffers = 0;  
  33.     m_nBlockSize = m_nBufferSize = m_nAlloc = m_nMaxBuffers = m_nMin = 0;  
  34.     m_nWaterMarks[0] = m_nWaterMarks[1] = m_nWaterMarks[2] = 0;  

可以看到TranBuf分配方式是每次分配nAlloc个Block(这个过程在后面叫做AllocOnce).每个Block是BlockSize字节. 然后至少分配m_nMin*nAlloc(首先调用m_nMin个AllocOnce过程),最多分配m_nMax*nAlloc个Block.每个内存 不够的话都会调用AllocOnce这个过程。

这里稍微解释一下RealHdl这个字段的意思。对于单个Block分配出来的内存块,RealHdl==this.但是如果是 连续跨越多个Block内存快的话,那么每个Block对应的Handle里面RealHdl对应的是首地址的Handle.这样做的好处就是, 如果希望对这个内存块增加或者是减少引用计数的话,只是指引到一个Handle,对里面字段修改引用计数。否则的话, 需要遍历每个Block对应的Handle修改引用技术。

1.2.3.2 Create

大部分Create代码都是在设置参数,最后调用m_nMin次AllocOnce来分配初始的内存块。

[cpp]  view plain copy
  1. bool Create(int nBlockSize, int nAlloc, int nMin, int nMax, double fRatio1, double fRatio2) {  
  2.   m_nUnitSize = nBlockSize;  
  3.   m_nBlockSize = nBlockSize;  
  4.   m_nBlockBase = Log_2(nBlockSize);  
  5.   if (-1 == m_nBlockBase) {  
  6.     TRACE0(<span class="org-string">"Fatal: invalid block size of %d\n"</span>, nBlockSize);  
  7.     return false;  
  8.   }  
  9.   m_nAlloc = nAlloc;  
  10.   m_nMaxBuffers = nMax * nAlloc;  
  11.   m_nBufferSize = m_nBlockSize * m_nAlloc;  
  12.   m_nBuffers = 0;  
  13.   m_nMax = nMax;  
  14.   m_nMin = nMin;  
  15.   if (0 != fRatio1 && 0 != fRatio2) {  
  16.     m_nWaterMarks[0] = (int)((double)m_nMaxBuffers * fRatio1);  
  17.     m_nWaterMarks[1] = (int)((double)m_nMaxBuffers * fRatio2);  
  18.     m_nWaterMarks[2] = m_nMaxBuffers - 1;  
  19.   }  
  20.   for (int i = 0; i < m_nMin; i++) {  
  21.     if (!AllocOnce()) // 注意这里没有必要回滚,每次成功都会记录状态,在Destroy里面会释放掉。  
  22.       return false;  
  23.   }  
  24.   return true;  
  25. }  

1.2.3.3 AllocOnce

之前说过AllocOnce是分配一个连续内存块,每个Block大小是m_nBlockSize,而个数是nAlloc. 同时还需要分配nAlloc个Handle.每个Handle管理一个Block.

[cpp]  view plain copy
  1. bool AllocOnce() {  
  2.   char* pBuffer = (char*)AlignAlloc(m_nBlockSize, m_nBufferSize);  // AlignAlloc是什么东西?
  3.   Handle* pHdl = (Handle*)ZeroAlloc(m_nAlloc * sizeof(Handle));  
  4.   if (pBuffer && pHdl) {  
  5.     m_BufferMap.insert(BufferMap::value_type(pBuffer, pHdl)); // 记录下这个连续块的内存地址和Handle地址.  
  6.     // 在Destroy时候有用.  
  7.     m_nBuffers += m_nAlloc;  
  8.     pBuffer += m_nBufferSize - m_nBlockSize;  
  9.     pHdl += m_nAlloc - 1;  
  10.   
  11.     for (int i = 0; i < m_nAlloc; i++) { // 然后将我所有的Block加入到链表里面去.  
  12.       pHdl->pBuffer = pBuffer;  
  13.       pHdl->nRef = 0;  
  14.       pHdl->nConsBuf = i + 1;  
  15.       pHdl->pRealHdl = pHdl;  
  16.       m_FreeList.push_back(pHdl); // 对于ConsBuf大的Handle放在链表最后.  
  17.       // 从后面内存分配策略就可以发现,对于分配连续Handle的话都是从最后开始的。  
  18.   
  19.       pBuffer -= m_nBlockSize;  
  20.       pHdl --;  
  21.     }  
  22.     return true;  
  23.   }  
  24.   if (pBuffer)  
  25.     free(pBuffer);  
  26.   if (pHdl)  
  27.     free(pHdl);  
  28.   return false;  
  29. }  

1.2.3.4 GetHandle

GetHandle是通过传入buffer首地址来确定管理这个buffer的Handle.但是注意不是RealHdl. 如果需要对这个内存做引用计数的话,应该是对RealHdl做引用计数。可以看看下面的AddRef实现。

[cpp]  view plain copy
  1. Handle* GetHandle(char* pBuffer) {  
  2.   BufferMap::iterator it = m_BufferMap.upper_bound(pBuffer);  
  3.   if (it != m_BufferMap.begin()) {  
  4.     it --;  
  5.   
  6.     char* pHead = it->first;  
  7.     ASSERT(pHead <= pBuffer);  
  8.     if (pBuffer < pHead + m_nBufferSize) {  
  9.       int n = (pBuffer - pHead) >> m_nBlockBase;  
  10.       Handle* pHdl = it->second + n;  
  11.       ASSERT(pHdl->pBuffer == pHead + (((uint32)n) << m_nBlockBase));  
  12.       return pHdl;  
  13.     }  
  14.   }  
  15.   return NULL;  
  16. }  

1.2.3.5 AddRef

对某块内存进行引用计数。并且强大的是这个内存地址不必是分配的首地址,可以是连续内存内部任意地址。

[cpp]  view plain copy
  1. int AddRef(char* p, bool bCheck = false) {  
  2.   Handle* pHdl = GetHandle(p);  
  3.   if (NULL == pHdl) {  
  4.     if (!bCheck) {  
  5.       return -1;  
  6.     }  
  7.     RaiseError(Invalid_Block);  
  8.   }  
  9.   
  10.   int n = ++ pHdl->pRealHdl->nRef;  
  11.   ASSERT(2 <= n);  
  12.   return n;  
  13. }  

1.2.3.6 Destroy

Destroy是将AllocOnce分配的内存和Handle全部回收。因为得到了所有分配内存和Handle的起始地址 保存在map里面所以释放并不麻烦.

[cpp]  view plain copy
  1. void Destroy() {  
  2.   m_FreeList.Init();  
  3.   m_nBuffers = 0; // 将分配计数清零.  
  4.   
  5.   BufferMap::iterator it;  
  6.   for (it = m_BufferMap.begin(); it != m_BufferMap.end(); it++) {  
  7.     free(it->first);  
  8.     free(it->second);  
  9.   }  
  10.   m_BufferMap.clear();  
  11. }  

1.2.3.7 Allocate

分配内存。可以从参数里面看出来语义是说分配多少个Block.nPriority参数是说使用哪个水位线。 如果超过水位线的话,那么会使用相应的策略来处理(打印日志)。

[cpp]  view plain copy
  1. // 从freelist里面分配一个block出来.  
  2. #define _ALLOC_TRAN_BUF(p, how)                     \  
  3.   p = m_FreeList.how();                           \  
  4.   ASSERT(DLINK_IS_STANDALONE(&p->link));          \  
  5.   ASSERT(0 == p->nRef);                           \  
  6.   ASSERT(p->pRealHdl == p);                       \  
  7.   p->nRef = 1  
  8.   
  9.   char* Allocate(uint32 nPriority, int count = 1) {  
  10.     int n;  
  11.     ASSERT(0 != count);  
  12.     // 会尝试分配两次。第一次不进行AllocOnce.如果第一次失败的话那么第二次会尝试。  
  13.     for (int i = 0; i < 2; i++) {  
  14.       n = (int)m_FreeList.size();  
  15.       // 如果当前分配内存大于water mark的话会打印日志,但是为了过快的打印这里控制了打印间隔  
  16.       // 从这里可以看到这个是非多线程的。从后面BufHandle使用来看确实是这样的。  
  17.       if (m_nBuffers - n > m_nWaterMarks[nPriority]) {  
  18.         if (nPriority != 0) {  
  19.           static time_t last = 0;  
  20.           time_t now = time(NULL);  
  21.           if (now - last >= 30) {   // avoid too frequent print  
  22.             int n1 = m_nMaxBuffers - m_nBuffers + n;  
  23.             int n2 = m_nMaxBuffers - m_nWaterMarks[nPriority];  
  24.             TRACE0(<span class="org-string">"Warning: available tran buf (#%d) touches watermark(#%d, %.f%%)\n"</span>,  
  25.                    n1, n2, (double)(n1 * 100) / m_nMaxBuffers);  
  26.             last = now;  
  27.           }  
  28.         }  
  29.         return NULL;  
  30.       }  
  31.       if (n >= count) {  // 如果free list里面内容>=count的话,但是有可能没有连续内存用来分配。  
  32.         Handle* pHdl, *pTmp;  
  33.         if (1 == count) { // 如果分配1个的话,那么直接从前面分配  
  34.           _ALLOC_TRAN_BUF(pHdl, pop_front);  
  35.           return pHdl->pBuffer;  
  36.         }  
  37.         // 否则会从后面分配,因为后面Consecutive Buffer的概率会更高。  
  38.         // Big block are formed by multiple consecutive blocks.  
  39.         // We try from the tail of free list, which brings higher probability.  
  40.         _ALLOC_TRAN_BUF(pHdl, pop_back);  
  41.         int i = 1;  
  42.         if (pHdl->nConsBuf >= count) { // 看看最后的Handle的consectutive number是否足够.  
  43.           for ( ; i < count; i++) { // 并且看看是否被占用(通过引用计数判断).这里没有细看链表的组织。  
  44.             pTmp = pHdl + i;  
  45.             UNLIKELY_IF (0 != pTmp->nRef) {  
  46.               break;  
  47.             }  
  48.             m_FreeList.remove(pTmp);  
  49.             DLINK_INSERT_PREV(&pHdl->link, &pTmp->link);  
  50.             pTmp->pRealHdl = pHdl;  
  51.             pTmp->nRef = 1;  
  52.           }  
  53.         }  
  54.         if (i == count) { // 如果分配OK的话,那么返回  
  55.           return pHdl->pBuffer;  
  56.         } else { // 否则的话那么需要进行回滚.  
  57.           for (int j = 0; j < i; j++) {  
  58.             pTmp = pHdl + j;  
  59.             DLINK_INITIALIZE(&pTmp->link);  
  60.             pTmp->pRealHdl = pTmp;  
  61.             pTmp->nRef = 0;  
  62.             m_FreeList.push_front(pTmp);  
  63.           }  
  64.         }  
  65.       }  
  66.       // 如果分配内存超限或者是AllocOnce分配失败的话,那么直接返回。  
  67.       if (m_nBuffers >= m_nMaxBuffers || !AllocOnce()) {  
  68.         return NULL;  
  69.       }  
  70.     }  
  71.     return NULL;  
  72.   }  

1.2.3.8 Free
[cpp]  view plain copy
  1. #ifdef  _DEBUG  
  2. #define _FREE_TRAN_BUF(p, how)                              \  
  3.   memset(p->pBuffer, 0xCC, m_nBlockSize);             \  
  4.   m_FreeList.how(p)  
  5. #else  
  6. #define _FREE_TRAN_BUF(p, how)                              \  
  7.   m_FreeList.how(p)  
  8. #endif  
  9.   
  10.   int Free(char* p, bool bCheck = false) {  
  11.     Handle* pHdl = GetHandle(p);  
  12.     if (NULL == pHdl) {  
  13.       if (bCheck) {  
  14.         RaiseError(Invalid_Block);  
  15.       }  
  16.       return -1;  
  17.     }  
  18.   
  19.     pHdl = pHdl->pRealHdl;  
  20.     int n = -- pHdl->nRef; // 修改引用计数。  
  21.     if (0 == n) {  
  22.       Handle* pTmp = dlink_get_prev(pHdl);  
  23.       if (pTmp == pHdl) { // 如果是一个Block的话.  
  24.         ASSERT_EQUAL(pHdl->pRealHdl, pHdl);  
  25.         ASSERT_EQUAL(0, pHdl->nRef);  
  26.         _FREE_TRAN_BUF(pHdl, push_front);  
  27.         return 0;  
  28.       }  
  29.       // here comes big block  
  30.       Handle* p = pHdl; // 我们知道这个Handle组织称为环形双向链表。  
  31.       // 同样按照AllocOnce的顺序,将consecutive number大的handle放在末尾.  
  32.       do {  
  33.         pHdl = pTmp;  
  34.         pTmp = dlink_get_prev(pTmp);  
  35.         ASSERT_EQUAL(1, pHdl->nRef);  
  36.         ASSERT_EQUAL(p, pHdl->pRealHdl);  
  37.         pHdl->pRealHdl = pHdl;  
  38.         pHdl->nRef = 0;  
  39.         DLINK_INITIALIZE(&pHdl->link);  
  40.         _FREE_TRAN_BUF(pHdl, push_back);  
  41.       } while (p != pTmp);  
  42.       ASSERT_EQUAL(p, p->pRealHdl);  
  43.       ASSERT_EQUAL(0, p->nRef);  
  44.       DLINK_INITIALIZE(&p->link);  
  45.       _FREE_TRAN_BUF(p, push_back);  
  46.       return 0;  
  47.     }  
  48.     return n;  
  49.   }  


1.2.4 BufHandle

如果说TranBuf是底层内存分配器的话,那BufHandle就是应用层的内存分配器。BufHandle底层是通过 两个TranBuf来进行分配的。BufHandle本质上是chained的形式,主要是为了节省mem copy以及适应 network IO app的。通过全局的BufHandlePool对象来分配内存。

1.2.4.1 OverView

首先我们看看BufHandle结构以及提供的API.

[cpp]  view plain copy
  1. struct BufHandle {  
  2.   BufHandle* _next; // 链式指针.  
  3.   char* pBuf; // 管理的内存.  
  4.   int nBufLen;      // available buffer length 可用长度  
  5.   int nDataLen;     // occupied data length 占用长度  
  6. };  
  7.   
  8. // 从[pHdl,pNext)这个区间上面回收nLen长度出来分配出去.  
  9. BufHandle* Reclaim(int nLen, BufHandle* pHdl, BufHandle* pNext);  
  10.   
  11. // 设置TranBuf的参数.这个应该在Kylin调用之前就设置好,如果打算使用BufHandle的话。  
  12. void SetTranBuf(int nSmallNum, int nBigNum,  
  13.                 int nSmallSize = 4096,  
  14.                 float fLowMark = 0.6f,  
  15.                 float fHighMark = 0.9f);  
  16.   
  17. // NOTICE(dirlt):这里如果不允许失败的话,那么就会直接抛出异常.  
  18. // inPool表示这个buf是否在pool里面如果是的话那么可以直接使用引用计数优化减少copy  
  19. // pBuf表示src内存地址,nLen表示src内存长度.pNext表示allocate handle之后next字段值.  
  20. // 如果不是inPool的话,那么从TranBufPool里面分配.  
  21. BufHandle* AllocateHdl(bool bInPool = falsechar* pBuf = NULL,  
  22.                        int nLen = 0, BufHandle* pNext = NULL);  
  23. // 从TranBufPool里面分配允许失败.  
  24. BufHandle* AllocateHdlCanFail(int nSize = 0);  
  25. // 从big pool里面分配1个block.  
  26. BufHandle* AllocateBigHdl();  
  27. BufHandle* AllocateBigHdlCanFail();  
  28.   
  29. // 释放这个Handle.  
  30. void FreeHdl(BufHandle* pHdl);  
  31. // 链式释放[pHdl,pNext)的链式里面的空间.  
  32. void ChainFreeHdl(BufHandle* pHdl, BufHandle* pNext);  
  33. // 这个名字取得不太好听,本质来说就是进行Clone  
  34. // pnLen数据长度是多少.bCopyNonTranBuf表示如果不能够做引用计数的话,是否需要copy.  
  35. BufHandle* CloneHdlAndTerminate(BufHandle* pHdl, BufHandle* pNext,  
  36.                                 int* pnLen = NULL, bool bCopyNonTranBuf = true);  

1.2.4.2 SetTranBuf

首先我们先看看CBufHandlePool的结构然后在看这个API

[cpp]  view plain copy
  1. // 继承于TObjectPool对象池可以直接高效分配出BufHandle对象出来.  
  2. class CBufHandlePool : public TObjectPool<BufHandle> {  
  3.   volatile int m_lock; // 多线程安全.  
  4.   CTranBufPool m_TranBufPool; // tran buf pool  
  5.   CTranBufPool m_BigBufPool; // big buf pool  
  6. };  
  7.   
  8.   CBufHandlePool() : TObjectPool<BufHandle>("BufHandle", BUFPOOL_C2),  
  9.     m_TranBufPool("TranBuffer", BUFPOOL_C1),  
  10.     m_BigBufPool("BigBuffer", BUFPOOL_C1) {  
  11.     m_lock = 0;  
  12.     Create(1024, 1);  
  13.   
  14.     int nAlloc = s_nTranBuf;  
  15.     int nMax = 1;  
  16.     // 一次不要分配超过512M.但是为了保持内存总量允许nMax增大.  
  17.     while ((s_nBufSize / 1024) * nAlloc > 524288) { /* Max alloc: 512M */  
  18.       nAlloc >>= 1;  
  19.       nMax <<= 1;  
  20.     }  
  21.     // tranbuf设置参数.  
  22.     m_TranBufPool.Create(s_nBufSize, nAlloc, 1, nMax, s_fLowMark, s_fHighMark);  
  23.     // 可以看到big buf的block size非常大.并且watermark非常高.分配次数在[0,10]之间.  
  24.     m_BigBufPool.Create(SZ_BIG_BUF, s_nBigTranBuf, 0, 10, 0.9, 0.9);  
  25.   }  
  26.   
  27. // 单例模式.  
  28. static CBufHandlePool* s_pBufHandlePool = NULL;  
  29. static CBufHandlePool* GetBufHdlPool() {  
  30.   if (NULL != s_pBufHandlePool) {  
  31.     return s_pBufHandlePool;  
  32.   } else {  
  33.     LOCK_THIS_BLOCK;  
  34.     if (NULL == s_pBufHandlePool) {  
  35.       s_pBufHandlePool = new CBufHandlePool;  
  36.     }  
  37.     return s_pBufHandlePool;  
  38.   }  
  39. }  

然后来看看这些参数是来如何设置的.

[cpp]  view plain copy
  1. int s_nTranBuf = 1024;  
  2. int s_nBufSize = 4096;  
  3. int s_nBigTranBuf = 64;  
  4. float s_fLowMark = 0.6f;  
  5. float s_fHighMark = 0.9f;  
  6.   
  7. void SetTranBuf(int nSmallNum, int nBigNum, int nSmallSize, float fLowMark, float fHighMark) {  
  8.   LOCK_THIS_BLOCK;  
  9.   
  10.   s_nTranBuf = nSmallNum; // tran buf应该每次alloc多少个block.  
  11.   s_nBigTranBuf = nBigNum; // big tran buf每次应该allocate多少个block.  
  12.   s_nBufSize = nSmallSize; // tran buf的blocksize.  
  13.   s_fLowMark = fLowMark;  
  14.   s_fHighMark = fHighMark;  
  15. }  

1.2.4.3 DoAllocate

这个是底层确保一定分配成功API(如果失败抛异常).来看看实现.使用hang住当前操作等待其他线程归还内存.

[cpp]  view plain copy
  1. // 从什么pool里面进行分配,尝试多少次分配.  
  2. BufHandle* DoAllocate(CTranBufPool* pPool, int nRetry) {  
  3.   BufHandle* pHdl;  
  4.   
  5.   for (int i = 0; i < nRetry; i++) {  
  6.     LOCK;  
  7.     pHdl = TObjectPool<BufHandle>::Allocate(); // 首先从对象池里面分配BufHandle对象.  
  8.     pHdl->pBuf = pPool->Allocate(i > 0 ? 2 : 1); // 然后从tran buf pool里面分配.  
  9.     // 注意这里第一次按照water mark1来分配,之后按照water mark2来分配.  
  10.     if (NULL == pHdl->pBuf) { // 如果分配失败的话,那么返回对象池.  
  11.       TObjectPool<BufHandle>::Free(pHdl);  
  12.       pHdl = NULL;  
  13.     }  
  14.     UNLOCK;  
  15.     if (NULL != pHdl) // 如果成功直接返回.  
  16.       return pHdl;  
  17.     if (i > 1) {  
  18.       TRACE0(<span class="org-string">"No enough memory, sleep %d\n"</span>, i + 1);  
  19.     }  
  20.     sleep(1); // 否则会hang住等待释放.  
  21.   }  
  22.   RaiseError(TODO_NO_ENOUGH_MEMORY); // 如果没有分配成功那么就会抛出异常.  
  23.   return NULL;  
  24. }  

1.2.4.4 DoAllocateCanFail

底层不一定保证分配成功,可能返回NULL表示失败.只是尝试一次分配.

[cpp]  view plain copy
  1. BufHandle* DoAllocateCanFail(CTranBufPool* pPool, int nSize) {  
  2.   BufHandle* pHdl;  
  3.   int nBlockSize = pPool->GetBlockSize();  
  4.   ASSERT(0 != nSize);  
  5.   
  6.   LOCK;  
  7.   pHdl = TObjectPool<BufHandle>::Allocate();  
  8.   // 以water mark0为标记.  
  9.   if (nSize == nBlockSize) {  
  10.     pHdl->pBuf = pPool->Allocate(0);  
  11.   } else {  
  12.     pHdl->pBuf = pPool->Allocate(0, (nSize + nBlockSize - 1) / nBlockSize);  
  13.   }  
  14.   if (NULL == pHdl->pBuf) {  
  15.     TObjectPool<BufHandle>::Free(pHdl);  
  16.     pHdl = NULL;  
  17.   }  
  18.   UNLOCK;  
  19.   return pHdl;  
  20. }  

1.2.4.5 _DoAddRef

对于BufHandle的引用技术和TranPool引用计数有点不同,并且平时思考的也不同。BufHandle的引用计数 只是针对头部的BufHandle增加计数而共用其他部分的BufHandle.

(NOTICE)(dirlt):(不过在外部调用可以看到,CloneAndTerminate实际上也还是遍历了所有的Handle做引用计数).

[cpp]  view plain copy
  1. BufHandle* _DoAddRef(BufHandle* pHdl, BufHandle* pNext, BufHandle** * pppLast) {  
  2.   if (-1 != m_TranBufPool.AddRef(pHdl->pBuf) || -1 != m_BigBufPool.AddRef(pHdl->pBuf)) {  
  3.     BufHandle* pTmp = TObjectPool<BufHandle>::Allocate();  
  4.     pTmp->_next = pNext;  
  5.     pTmp->pBuf = pHdl->pBuf;  
  6.     pTmp->nBufLen = pHdl->nDataLen;  
  7.     pTmp->nDataLen = pHdl->nDataLen;  
  8.     *pppLast = &pTmp->_next;  
  9.     return pTmp;  
  10.   }  
  11.   return NULL;  
  12. }  

1.2.4.6 _DoFree

只是释放单个BufHandle对象.

[cpp]  view plain copy
  1. void _DoFree(BufHandle* pHdl) {  
  2.   if (-1 == m_TranBufPool.Free(pHdl->pBuf))  
  3.     m_BigBufPool.Free(pHdl->pBuf);  
  4.   TObjectPool<BufHandle>::Free(pHdl);  
  5. }  

1.2.4.7 AllocateBig

从BigTranBufPool里面分配大块内存.注意对于大块内存而言的话只允许分配一个Block.

[cpp]  view plain copy
  1. BufHandle* AllocateBig(bool bCanFail) {  
  2.   BufHandle* pHdl;  
  3.   
  4.   pHdl = bCanFail  
  5.          ? DoAllocateCanFail(&m_BigBufPool, SZ_BIG_BUF)  
  6.          : DoAllocate(&m_BigBufPool, 60); // 60s的延迟.  
  7.   if (pHdl) {  
  8.     pHdl->_next = NULL;  
  9.     pHdl->nBufLen = SZ_BIG_BUF;  
  10.     pHdl->nDataLen = 0;  
  11.   }  
  12.   return pHdl;  
  13. }  

1.2.4.8 AllocateCanFail

从TranBufPool里面分配连续内存出来.

[cpp]  view plain copy
  1. BufHandle* AllocateCanFail(int nSize) {  
  2.   BufHandle* pHdl = DoAllocateCanFail(&m_TranBufPool, nSize);  
  3.   if (pHdl) {  
  4.     pHdl->_next = NULL;  
  5.     pHdl->nBufLen = nSize;  
  6.     pHdl->nDataLen = 0;  
  7.   }  
  8.   return pHdl;  
  9. }  

1.2.4.9 AllocForBuf

为某个buf分配内存.把buf内容copy进来.并且设置pNext.pppLast表示最后一个节点的next字段指针(三指针比较难理解…)

[cpp]  view plain copy
  1. BufHandle* AllocForBuf(char* pBuf, int nLen, BufHandle* pNext, BufHandle** * pppLast) {  
  2.   BufHandle* pFirst, *pHdl, **ppLast;  
  3.   
  4.   pFirst = NULL;  
  5.   ppLast = &pFirst;  
  6.   while (nLen > 0) {  
  7.     pHdl = DoAllocate(&m_TranBufPool, 120); // 120s延迟.  
  8.   
  9.     pHdl->nBufLen = s_nBufSize;  
  10.     pHdl->nDataLen = nLen > s_nBufSize ? s_nBufSize : nLen;  
  11.     memcpy(pHdl->pBuf, pBuf, pHdl->nDataLen);  
  12.     pBuf += pHdl->nDataLen;  
  13.     nLen -= pHdl->nDataLen;  
  14.   
  15.     pHdl->_next = pNext; // 设置next字段内容  
  16.     *ppLast = pHdl;  
  17.     ppLast = &pHdl->_next; // 并且得到最后一个item的next字段指针.  
  18.     // 不过因为设置了pNext所以感觉不是特别有用.  
  19.   }  
  20.   if (pppLast) {  
  21.     *pppLast = ppLast;  
  22.   }  
  23.   return pFirst;  
  24. }  

1.2.4.10 Allocate
[cpp]  view plain copy
  1. // 如果是inpool的话,那么pubuf必须是pool分配出来的,  
  2. // 那么我们只是针对这个buffer做一个引用计数  
  3.   
  4. // 如果不是inpool的话,nLen==0或者是pBuf==NULL,分配出一个空单元出来.  
  5. // 否则需要做一个内存copy.使用上面AllocForBuf的API.  
  6. BufHandle* Allocate(bool bInPool = falsechar* pBuf = NULL,  
  7.                     int nLen = 0, BufHandle* pNext = NULL  
  8.                    ) {  
  9.   BufHandle* pHdl;  
  10.   
  11.   UNLIKELY_IF (false == bInPool) {  
  12.     LOCK;  
  13.     pHdl = TObjectPool<BufHandle>::Allocate();  
  14.     if (-1 == m_TranBufPool.AddRef(pBuf))  
  15.       m_BigBufPool.AddRef(pBuf);  
  16.     UNLOCK;  
  17.   
  18.     pHdl->_next = pNext;  
  19.     pHdl->pBuf = pBuf;  
  20.     pHdl->nBufLen = nLen;  
  21.     pHdl->nDataLen = nLen;  
  22.     return pHdl;  
  23.   }  
  24.   if (pBuf == NULL || nLen == 0) {  
  25.     pHdl = DoAllocate(&m_TranBufPool, 120);  
  26.   
  27.     pHdl->_next = pNext;  
  28.     pHdl->nBufLen = s_nBufSize;  
  29.     pHdl->nDataLen = nLen;  
  30.     return pHdl;  
  31.   }  
  32.   
  33.   return AllocForBuf(pBuf, nLen, pNext, NULL);  
  34. }


1.2.4.11 ChainFree

释放[pHdl,pNext)链上的所有item.

[cpp]  view plain copy
  1. void ChainFree(BufHandle* pHdl, BufHandle* pNext) {  
  2.   BufHandle* pTmp;  
  3.   LOCK;  
  4.   for ( ; pHdl != pNext; pHdl = pTmp) {  
  5.     ASSERT(NULL != pHdl);  
  6.     pTmp = pHdl->_next;  
  7.     _DoFree(pHdl);  
  8.   }  
  9.   UNLOCK;  
  10. }  

1.2.4.12 CloneAndTerminate

这个API的语义在之前已经解释过了,来看看代码.

[cpp]  view plain copy
  1. BufHandle* CloneAndTerminate(BufHandle* pHdl, BufHandle* pNext,  
  2.                              int* pnLen, bool bCopyNonTranBuf  
  3.                             ) {  
  4.   BufHandle* pFirst, *pTmp, **ppLast, **ppLastTmp;  
  5.   int nLen = 0;  
  6.   
  7.   pFirst = NULL;  
  8.   ppLast = &pFirst;  
  9.   LOCK;  
  10.   for ( ; pHdl != pNext; pHdl = pHdl->_next) {  
  11.     pTmp = _DoAddRef(pHdl, NULL, &ppLastTmp); // 看看是否可以在直接做引用计数.  
  12.     if (NULL == pTmp) {  
  13.       if (bCopyNonTranBuf) { // 如果需要copy出来的话.  
  14.         UNLOCK;  
  15.         pTmp = AllocForBuf(pHdl->pBuf, pHdl->nDataLen, NULL, &ppLastTmp);  
  16.         LOCK;  
  17.       } else { // 如果显示说不copy只是引用内存的话,那么只是开辟Handle对象.  
  18.         pTmp = TObjectPool<BufHandle>::Allocate();  
  19.         pTmp->pBuf = pHdl->pBuf;  
  20.         pTmp->nDataLen = pTmp->nBufLen = pHdl->nDataLen;  
  21.         pTmp->_next = NULL;  
  22.         ppLastTmp = &pTmp->_next;  
  23.       }  
  24.     }  
  25.     nLen += pHdl->nDataLen;  
  26.     *ppLast = pTmp;  
  27.     ppLast = ppLastTmp;  
  28.   }  
  29.   UNLOCK;  
  30.   
  31.   if (pnLen) {  
  32.     *pnLen = nLen;  
  33.   }  
  34.   if (nLen) {  
  35.     return pFirst;  
  36.   }  
  37.   // 如果失败的话那么释放已经分配出来的.  
  38.   ChainFreeHdl(pFirst, NULL);  
  39.   return NULL;  
  40. }  

1.2.5 Kylin

这个模块主要负责框架的启动和停止,做了一些琐碎的事情方便用户,主要是下面这两个函数

[cpp]  view plain copy
  1. // 启动框架,使用多少个CPU,网络和磁盘线程,至少1个CPU和1个网络线程  
  2. // f表示线程初始化函数  
  3. // nTimerPrecision会影响到定时器实现.如果超时在时间精度一下的话都会通过ExecMan直接触发  
  4. // 否则都会必须通过RunTimer来进行检查  
  5. APF_ERROR InitKylin(int nExecWorkers, int nNetWorkers, int nDiskWorkers,  
  6.                     THREAD_INIT_PROC f, uint32 nTimerPrecision);  
  7. // bWait表示是否等待ExecMan的线程池正常停止,这个会在ExecMan部分提到  
  8. APF_ERROR StopKylin(bool bWait);  

对于InitKylin里面事情就是启动几个Manager,还做了一件tricky事情就是将SIGPIPE信号忽略了。而StopKylin就是停止这些Manager.我们需要仔细关注的就是这些Manager的启停。

1.2.6 ExecMan

1.2.6.1 Overview

我们首先看看ExecMan的接口

[cpp]  view plain copy
  1. #define g_pExecMan CExecMan::Instance() // 直接使用宏g_pExecMan就可以单例  
  2.   
  3. class CExecMan  
  4. {  
  5.     DECLARE_SINGLETON(CExecMan) // 单例模式  
  6.     public:  
  7.     ~CExecMan();  
  8.     APF_ERROR Start(int nWorkers, THREAD_INIT_PROC fInit, uint32 nTimerPrecision);  
  9.     void Stop(bool bWait);  
  10.   
  11.     // 插入一个任务  
  12.     APF_ERROR QueueExec(AsyncContext* pCtx, bool bClientReferred);  
  13.     // 插入一个紧急任务  
  14.     APF_ERROR QueueExecEmergent(AsyncContext* pCtx, bool bClientReferred);  
  15.     // todo(zhangyan04):  
  16.     APF_ERROR ProxyExec(int nAckCode, CAsyncClient* pClient, PROXY_EXEC_PROC fProc, ProxyExecCtx* pCtx);  
  17.     // 提交一个定时器任务  
  18.     APF_ERROR DelayExec(int nAction, CAsyncClient* pClient, uint32 nMilliseconds, AsyncContext* pCtx);  
  19.     // 取消一个任务  
  20.     APF_ERROR CancelExec(AsyncContext* pCtx);  
  21.     // 检查定时器  
  22.     void RunTimer();  
  23.   
  24.   private:  
  25.     CThreadPool m_ThreadPool;  
  26.     volatile int m_nCurJobs; // 在运行期间有多少Job正在被提交  
  27. };  

1.2.6.2 Start & Stop

Start逻辑很简单,包括计算1s对应多少cycle数目以及启动线程池。

[cpp]  view plain copy
  1. APF_ERROR CExecMan::Start(int nWorkers, THREAD_INIT_PROC fInit, uint32 nTimerPrecision)  
  2. {  
  3.     // 计算一下CPU一个tick有多少个cycle数目,这样可以通过rdstc转换成为时间  
  4.     g_nCycleStart = rdtsc();  
  5.     g_nLastTick = 0;  
  6.     g_nTickPrecision = (nTimerPrecision >= 1000) ? 1000 : nTimerPrecision;  
  7.     g_nCyclesInTick = GetCpuFreq() / (1000 / g_nTickPrecision);  
  8.     if (0 < m_ThreadPool.Start(nWorkers, fInit)) { // 启动线程池  
  9.         AtomicSetValue(m_nCurJobs, 0);  
  10.         return APFE_OK;  
  11.     }  
  12.     return APFE_SYS_ERROR;  
  13. }  

Stop逻辑的话可能需要仔细理解一下

[cpp]  view plain copy
  1. // bWait表示是否需要等待kylin的线程池正常结束,执行完成线程池里面任务为止。  
  2. // 不断修正m_nCurJobs作用是为了阻止新任务的提交。这个我们可以在QueuExec部分联合起来一起看看  
  3. void CExecMan::Stop(bool bWait)  
  4. {  
  5.     if (bWait) {  
  6.         int n;  
  7.         while (0 != (n=atomic_comp_swap(&m_nCurJobs, LARGE_ENOUGH_NEGATIVE, 0))) {  
  8.             if (LARGE_ENOUGH_NEGATIVE == n) {  
  9.                 return;  
  10.             }  
  11.             Sleep(1);  
  12.         }  
  13.         m_ThreadPool.Stop();  
  14.     }  
  15.     else {  
  16.         AtomicSetValue(m_nCurJobs, LARGE_ENOUGH_NEGATIVE);  
  17.     }  
  18. }  

1.2.6.3 QueueExec

QueueExec和QueueExecEmergent逻辑非常相似,只不过底层调用线程池的QueueJob和QueueEmergentJob.我们这里只看QueueExec.

[cpp]  view plain copy
  1. static void  
  2. Proc(Job* pJob)  
  3. {  
  4.     AsyncContext* pCtx = (AsyncContext*)pJob;  
  5.     CAsyncClient* pClient = pCtx->pClient;  
  6.   
  7.     pCtx->fProc = NULL;  
  8.     pClient->OnCompletion(pCtx);  
  9.     pClient->Release();  
  10. }  
  11.   
  12. // bClientReferref表明用户是否加了引用  
  13. // 如果按照sunxiao的说明,我们这里最好永远写true,然后我们在外面调用点自己AddRef和DecRef  
  14. APF_ERROR CExecMan::QueueExec(AsyncContext* pCtx, bool bClientReferred)  
  15. {  
  16.     VERIFY_OR_RETURN(NULL != pCtx, APFE_INVALID_ARGS);  
  17.     VERIFY_OR_RETURN(NULL != pCtx->pClient, APFE_INVALID_ARGS);  
  18.   
  19.     // 如果atomic +1 <0的话,那么说明这个时候m_nCurJobs已经被置过LARGE_ENOUGH_NEGATIVE了  
  20.     // 当然我们是有假设m_nCurJobs不会非常快地复位,可以认为这个是成立的  
  21.     if (atomic_add(&m_nCurJobs, 1) >= 0) {  
  22.         // TODO: if the number of workers is dynamic, we may need to lock and re-dispatch exisiting events...  
  23.         if (!bClientReferred) {  
  24.             pCtx->pClient->AddRef();  
  25.         }  
  26.         pCtx->fProc = Proc; // 置ctx的fProc为Proc  
  27.         // 然后根据client的AsyncId来决定指派到哪一个线程工作  
  28.         m_ThreadPool.QueueJob((Job*)pCtx, pCtx->pClient->GetAsyncId() % m_ThreadPool.GetWorkerCount());  
  29.         atomic_add(&m_nCurJobs, -1); // 将当前正在提交的Jobs个数-1.  
  30.         return APFE_OK;  
  31.     }  
  32.     if (bClientReferred) {  
  33.         pCtx->pClient->Release();  
  34.     }  
  35.     // 那么将m_nCurJobs重置  
  36.     AtomicSetValue(m_nCurJobs, LARGE_ENOUGH_NEGATIVE);  
  37.     if (IsKylinRunning()) {  
  38.         TRACE0(<span class="org-string">"Fatal error: Exec workers are not started\n"</span>);  
  39.     }  
  40.     return APFE_NO_WORKER;  
  41. }  

我们这里可以看到m_nCurJobs在QueueExec和Stop之间的配合。然后我们稍微看看Proc这个过程,对于CPU任务直接调用OnCompletion然后调用Release.

1.2.6.4 Timer

定时器任务加入是DelayExec,检查触发是RunTimer.如果查看CallGraph的话会发现RunTimer都是在网络部分调用的,我们在网络部分看看触发的时机。 DelayExec里面的逻辑会根据定时时间来判断如何实现,如果定时时间超过g_nTickPrecision,那么会将超时时间加入一个map里面去,然后让RunTimer去触发。 否则会加入线程池里面去。对于加入到map里面的fProc有一个特殊的标记(JOB_PROC)2.在CancelExec时候会认识这个特殊标记,将事件从map中删除。

[cpp]  view plain copy
  1. APF_ERROR CExecMan::DelayExec(int nAction, CAsyncClient* pClient, uint32 nMilliseconds, AsyncContext* pCtx)  
  2. {  
  3.     VERIFY_OR_RETURN(NULL != pClient, APFE_INVALID_ARGS);  
  4.     VERIFY_OR_RETURN(NULL != pCtx, APFE_INVALID_ARGS);  
  5.   
  6.     pCtx->nAction = nAction;  
  7.     pCtx->pClient = pClient;  
  8.     pCtx->fProc = (JOB_PROC)2;  
  9.   
  10.     if (g_nTickPrecision <= nMilliseconds) {  
  11.         pClient->AddRef();  
  12.   
  13.         s_Lock.Lock();  
  14.         /* milliseconds -> ticks */  
  15.         nMilliseconds = g_nLastTick + nMilliseconds / g_nTickPrecision;  
  16.         pCtx->nErrCode = nMilliseconds;  
  17.         s_TimerMap.insert(nMilliseconds, pCtx);  
  18.         s_Lock.Unlock();  
  19.         return APFE_OK;  
  20.     }  
  21.   
  22.     APF_ERROR err;  
  23.     s_Lock.Lock();  
  24.     err = QueueExec(pCtx, false);  
  25.     s_Lock.Unlock();  
  26.     return err;  
  27. }  

然后我看看看RunTimer这个部分。这个部分非常简单,就是根据当前时间判断map里面哪些定时器需要进行触发,然后将触发逻辑作为Job丢入CPU线程池。 我们这里不看RunTimer具体代码,反而倒是对外面的一些小细节比较感兴趣。我们不希望RunTimer被多个实例调用,只要有一个实例调用就OK,使用CToken完成。 当然可以使用mutex+try_lock来实现但是开销应该会更大。

[cpp]  view plain copy
  1. void CExecMan::RunTimer()  
  2. {  
  3.     static CToken token;  
  4.     UNLIKELY_IF (!token.TryAcquire(1)) {  
  5.         return;  
  6.     }  
  7.     // ...  
  8.     token.Release(1);  
  9. }  

1.2.6.5 Example

我们这里给的例子非常简单,但是希望有启发性.我们从1开始进行打印,每打印1个数字就认为当前任务结束,一直无限打印。 但是我们同时会启动一个定时器,只允许我们做1.2s钟时间的打印。如果我们在1.2s内打印数字个数超过了100个的话,那么我们重启一个定时器1.2s, 而这次打印数字个数阈值为200个之后每次翻倍,直到1.2s内没有打印我们所希望个数的话程序退出。在主线程100ms来检查ExecMan的RunTimer.

#include <cstdio>
#include <vector>
#include <time.h>
#include <span class="org-string">"stdafx.h"</span>
#include <span class="org-string">"Kylin.h"</span>

static volatile int worker=16;
static const int PRINT=0;
static const int TIMEOUT=1;
static const int TIMEOUT_MS=1200;

class XAsyncClient:public CAsyncClient{
  public:
    AsyncContext print_ctx;
    AsyncContext delay_ctx;
    int id;
    int current_number;
    int threshold;
    int last_working_number;
    bool stop; // 一旦stop那么立刻后面内容都不打印了
    XAsyncClient(int id_):
            id(id_),
            current_number(1),
            threshold(100),
            last_working_number(0),
            stop(false){
        InitAsyncContext(&print_ctx);
        InitAsyncContext(&delay_ctx);
        print_ctx.pClient=this;
        delay_ctx.pClient=this;
    }
    int Release(){ // Release通常都是这样写的
        int n=CAsyncClient::Release();
        if(n==0){
            delete this;
        }
        return n;
    }
    void Start(){ // 启动时候我们发起两个Job
        print_ctx.nAction=PRINT;
        CAsyncClient::AddRef();
        g_pExecMan->QueueExec(&print_ctx,true);
        CAsyncClient::AddRef();
        g_pExecMan->DelayExec(TIMEOUT,this,TIMEOUT_MS,&delay_ctx);
    }
    void Print(){
        fprintf(stderr,<span class="org-string">"(%d)xref:%d,current:%d\n"</span>,id,CAsyncClient::GetRef(),
                current_number);
    }
    virtual void OnCompletion(AsyncContext* ctx){
        switch(ctx->nAction){ // 分别处理这两个类型Job
            case PRINT:
                if(stop){
                    break;
                }
                fprintf(stderr,<span class="org-string">"(%d)%d\n"</span>,id,current_number);
                current_number++;
                if((current_number-last_working_number)>=threshold){
                    // update
                    last_working_number=current_number;
                    threshold*=2;
                    // canel timer.
                    fprintf(stderr,<span class="org-string">"(%d)==============================restart timer==============================\n"</span>,id);
                    g_pExecMan->CancelExec(&delay_ctx);
                    g_pExecMan->DelayExec(TIMEOUT,this,TIMEOUT_MS,&delay_ctx);
                }
                CAsyncClient::AddRef();
                g_pExecMan->QueueExec(&print_ctx,true);
                break;
            case TIMEOUT:
                fprintf(stderr,<span class="org-string">"(%d)********************quit********************\n"</span>,id);
                atomic_add(&worker,-1);
                stop=true;
                break;
            default:
                assert(0);
        }
    }
};

int main(){
    // use 4 exec threads.
    InitKylin(4,0,0);
    // 100ms
    const struct timespec spec={0,100*1000000};
    const int worker_num=worker;
    std::vector< XAsyncClient* > vec;
    for(int i=0;i<worker_num;i++){
        XAsyncClient* client=new XAsyncClient(i);
        vec.push_back(client);
        client->Start();
    }
    while(1){
        nanosleep(&spec,NULL);
        //Sleep(1);
        if(AtomicGetValue(worker)==0){
            StopKylin(true);
            break;
        }else{ // 主线程我们每隔100ms检查一次超时情况
            g_pExecMan->RunTimer();
        }
    }
    for(int i=0;i<worker_num;i++){
        XAsyncClient* client=vec[i];
        client->Print(); // 退出时候打印一下信息
        delete client;
    }
    return 0;
}

 
    

1.2.7 DiskMan

1.2.7.1 Overview

我们首先看看和磁盘相关的两个比较重要的类。因为磁盘操作不像CPU操作一样不需要任何辅助数据结构,磁盘操作需要一些信息比如fd等,磁盘操作需要一个特殊的磁盘Context。 然后每次发起磁盘操作使用另外一个结构Request.这里名字上和原来的CPU事件并不太一样,我们可能需要习惯一下。实际上如果我们需要映射到CPU事件里面的话,这两个Context应该结合在一起。 只不过这里DiskContext不是经常变动的部分,而DiskRequest是经常变动的部分所以分离开了。

[cpp]  view plain copy
  1. // 这个是磁盘操作相关的Context  
  2. struct DiskContext {  
  3.     int fd;  
  4.     int diskno;// which disk  
  5.     CAsyncClient *pClient;  
  6.     uint64 nCurOff, nRead, nWrite;  
  7.     char* pPath;// file path  
  8.     int nFlag;// file open flag  
  9. };  
  10.   
  11. // 这个是一次发起的请求  
  12. struct DiskRequest {  
  13.     union { // 这里使用这种方式纯粹是为了写起来方便  
  14.         AsyncContext async;  
  15.         Job job;  
  16.     };  
  17.     /* !!the first element must be AsyncContext */  
  18.     void *buf; // 读写放到什么地方  
  19.     int request; // 读写多少字节数据  
  20.     int xfered; //当前实际读写了多少数据  
  21.     uint64 off; // 在什么偏移上读写  
  22.     DiskContext *pCtx;  
  23. };  

然后在看看DiskMan接口

[cpp]  view plain copy
  1. #define g_pDiskMan CDiskMan::Instance() // 直接使用宏g_pDiskMan就可以单例  
  2.   
  3. class CDiskMan  
  4. {  
  5.     DECLARE_SINGLETON(CDiskMan) // 单例模式  
  6.     public:  
  7.     ~CDiskMan();  
  8.   
  9.     APF_ERROR Start(int nDisks, THREAD_INIT_PROC fInit);  
  10.     void Stop();  
  11.   
  12.     APF_ERROR Associate(int diskno, int fd, CAsyncClient* pClient, DiskContext* pContext);  
  13.     APF_ERROR Associate(int diskno, char* pPath, int nFlag, CAsyncClient* pClient, DiskContext* pContext);  
  14.     APF_ERROR Deassociate(DiskContext* pContext);  
  15.   
  16.     void Read(DiskContext* pContext, void* pBuf, int count, DiskRequest* pReq);  
  17.     void Write(DiskContext* pContext, void* pBuf, int count, DiskRequest* pReq);  
  18.   
  19.   private:  
  20.     CThreadPool m_ThreadPool;  
  21.     bool m_bStarted;  
  22. };  

1.2.7.2 Start & Stop

启动停止逻辑非常简单,就是让线程池启动和停止

[cpp]  view plain copy
  1. APF_ERROR CDiskMan::Start(int nDisks, THREAD_INIT_PROC fInit)  
  2. {  
  3.     ASSERT(nDisks <= MAX_NR_DISKS);  
  4.     ASSERT(!IsStarted());  
  5.     // TODO::: if 0 then check the number of disks  
  6.     if (m_ThreadPool.Start(nDisks, fInit) > 0) {  
  7.         m_bStarted = true;  
  8.         return APFE_OK;  
  9.     }  
  10.     return APFE_SYS_ERROR;  
  11. }  
  12.   
  13. void CDiskMan::Stop()  
  14. {  
  15.     if (m_bStarted) {  
  16.         m_bStarted = false;  
  17.         m_ThreadPool.Stop();  
  18.     }  
  19. }  

1.2.7.3 Associate & Deassociate

逻辑非常简单,就是进行一下DiskContext和CAsyncClient初始化的工作。关于DiskContext里面各个字段含义的话,都是在Read/Write时候解释的。 关于这里最重点的绑定内容就是diskno.diskno非常作用类似于CPU事件里面的AsyncId.相同AsyncId可以分摊到同一个CPU线程这件可以免去加锁开销, 而diskno可以让多个DiskContext分摊到同一个Disk线程,不同线程绑定不同的磁盘驱动器,这样可以让同一个磁盘驱动器仅仅为几个文件服务。

[cpp]  view plain copy
  1. APF_ERROR CDiskMan::Associate(int diskno, char* pPath, int nFlag,  
  2.                               CAsyncClient* pClient, DiskContext* pContext)  
  3. {  
  4.     pContext->fd = -1;  
  5.     pContext->diskno = diskno;  
  6.     pContext->pClient = pClient;  
  7.     pContext->nCurOff = pContext->nRead = pContext->nWrite = 0;  
  8.     pContext->pPath = pPath;  
  9.     pContext->nFlag = nFlag;  
  10.     pClient->AddRef();  
  11.     return APFE_OK;  
  12. }  
  13.   
  14. APF_ERROR CDiskMan::Deassociate(DiskContext* pContext)  
  15. {  
  16.     if (pContext->pPath && pContext->fd!=-1) {  
  17.         close(pContext->fd);  
  18.     }  
  19.     pContext->pClient->Release();  
  20.     return APFE_OK;  
  21. }  

1.2.7.4 Read & Write

文件的Read/Write非常简单,因为本身就是一个阻塞的过程,发起一次就可以保证读取所有内容了,所以不像网络一样需要多次发起。

[cpp]  view plain copy
  1. void CDiskMan::Read(DiskContext* pContext, void* pBuf, int count, DiskRequest* pReq)  
  2. {  
  3.     pReq->async.nAction = AA_READ; // 设置nAction,然后QueueTask,Task中回调就是ReadOp  
  4.     QUEUE_TASK(pContext, pReq, ReadOp, pBuf, count);  
  5. }  
  6.   
  7. void CDiskMan::Write(DiskContext* pContext, void* pBuf, int count, DiskRequest* pReq)  
  8. {  
  9.     pReq->async.nAction = AA_WRITE; // 设置nAction,然后QueueTask,Task中回调就是WriteOp  
  10.     QUEUE_TASK(pContext, pReq, WriteOp, pBuf, count);  
  11. }  
  12.   
  13. // 可以看到这里pClient已经帮我们AddRef了,所以我们在实际编写App不需要再次AddRef  
  14. #define QUEUE_TASK(pContext, pReq, f, pBuf, count)                  \  
  15.     pContext->pClient->AddRef();                                    \  
  16.     pReq->async.pClient = pContext->pClient;                        \  
  17.     pReq->job.fProc = f;                                            \  
  18.     pReq->buf = pBuf;                                               \  
  19.     pReq->request = count;                                          \  
  20.     pReq->xfered = 0;                                               \  
  21.     pReq->pCtx = pContext;                                          \  
  22.     m_ThreadPool.QueueJob(&pReq->job, pContext->diskno)  

从上面分析的话,所有重要的工作都分摊在了ReadOp和WriteOp上面。我们需要做的是Dig下去看看两个是怎么工作的。但是很不幸,两个函数里面内容都是使用了宏DiskOp. DiskOp(a,b,c)其中a表示对应的系统调用叫什么名字,b表示这个Job,c表示读写(没有使用).

[cpp]  view plain copy
  1. static void  
  2. ReadOp(Job* pJob)  
  3. {  
  4.     DISK_OP(read, pJob, 0);  
  5. }  
  6.   
  7. static void  
  8. WriteOp(Job* pJob)  
  9. {  
  10.     DISK_OP(write, pJob, 1);  
  11. }  

继续Dig看看DISKOP是怎么工作的

[cpp]  view plain copy
  1. // 完成之后设置ErrCode,并且加入CPU线程池。用户最终处理的话需要强制转换DiskRequest.  
  2. #define NotifyClient(err, req)    {                                     \  
  3.         req->async.nErrCode = err;                                      \  
  4.         g_pExecMan->QueueExec((AsyncContext*)req, true);                \  
  5.     }  
  6.   
  7. // 1.可以看到如果fd==-1的话会自动打开文件  
  8. // 2.判断一下发起的off和context是否一致,不一致的话使用pread/pwrite,然后修改off  
  9. // 3.读取完成之后使用NotifyClient通知App  
  10. #define DISK_OP(op, j, rw)                                              \  
  11.     DiskRequest* pReq = CONTAINING_RECORD(j, DiskRequest, job);         \  
  12.     DiskContext* pCtx = pReq->pCtx;                                     \  
  13.     UNLIKELY_IF (-1 == pCtx->fd) {                                      \  
  14.         pCtx->fd = open(pCtx->pPath, pCtx->nFlag, 0644);                \  
  15.         UNLIKELY_IF (-1 == pCtx->fd) {                                  \  
  16.             NotifyClient(errno, pReq);                                  \  
  17.             return;                                                     \  
  18.         }                                                               \  
  19.     }                                                                   \  
  20.     uint64 cost = rdtsc();                                              \  
  21.     int len;                                                            \  
  22.     if (pReq->off != pCtx->nCurOff) {                                   \  
  23.         len = p ## op(pCtx->fd, pReq->buf, pReq->request, pReq->off);   \  
  24.         pCtx->nCurOff = pReq->off;                                      \  
  25.     }                                                                   \  
  26.     else {                                                              \  
  27.         len = op(pCtx->fd, pReq->buf, pReq->request);                   \  
  28.     }                                                                   \  
  29.     if (len >= 0) {                                                     \  
  30.         cost = rdtsc() - cost;                                          \  
  31.         int which = (pCtx->diskno<<1) + rw;                             \  
  32.         g_nDiskStats[which] += len;                                     \  
  33.         g_nDiskCosts[which] += cost;                                    \  
  34.         pCtx->nCurOff += len;                                           \  
  35.         pReq->off += len;                                               \  
  36.         pReq->xfered = len;                                             \  
  37.         NotifyClient(0, pReq);                                          \  
  38.     }                                                                   \  
  39.     else {                                                              \  
  40.         NotifyClient(errno, pReq);                                      \  
  41.     }  

1.2.7.5 Example

例子非常简单就是我们首先发起一个磁盘操作写文件然后在将去读取出来。

[cpp]  view plain copy
  1. #include <cstdio>  
  2. #include <vector>  
  3. #include <string>  
  4. #include <time.h>  
  5. #include <span class="org-string">"stdafx.h"</span>  
  6. #include <span class="org-string">"Kylin.h"</span>  
  7.   
  8. static const int worker_num=8;  
  9. static volatile int worker=worker_num;  
  10. static const char* fname_prefix=<span class="org-string">"hello"</span>;  
  11. static const char* content=<span class="org-string">"world"</span>;  
  12. static const int READ=0;  
  13. static const int WRITE=1;  
  14. static const int disk_thread_num=4;  
  15.   
  16. class XDiskRequest:public DiskRequest{  
  17.   public:  
  18.     int nAction; // what kind of operation we init.  
  19. };  
  20.   
  21. class XAsyncClient:public CAsyncClient{  
  22.   public:  
  23.     int id;  
  24.     std::string name;  
  25.     DiskContext disk_ctx;  
  26.     XDiskRequest disk_req;  
  27.     XAsyncClient(int id_):  
  28.             id(id_){  
  29.         // make filename.  
  30.         char tmp[128];  
  31.         snprintf(tmp,sizeof(tmp),<span class="org-string">"%s_%d"</span>,fname_prefix,id);  
  32.         name=tmp;  
  33.         g_pDiskMan->Associate(id%disk_thread_num,const_cast<char*>(name.c_str()),O_RDWR | O_CREAT,this,&disk_ctx);  
  34.     }  
  35.     ~XAsyncClient(){  
  36.         g_pDiskMan->Deassociate(&disk_ctx);  
  37.     }  
  38.     void Start(){  
  39.         disk_req.nAction=WRITE;  
  40.         char* s=strdup(content);  
  41.         // ctx off=0.write from the beginning  
  42.         g_pDiskMan->Write(&disk_ctx,s,strlen(s)+1,&disk_req);  
  43.     }  
  44.     void Print(){  
  45.         fprintf(stderr,<span class="org-string">"(%d)xref:%d\n"</span>,id,CAsyncClient::GetRef());  
  46.     }  
  47.     virtual void OnCompletion(AsyncContext* ctx){  
  48.         XDiskRequest* req=(XDiskRequest*)ctx;  
  49.         if(req->nAction==WRITE){  
  50.             assert(req->xfered==req->request);  
  51.             // free written buffer.  
  52.             free(req->buf);  
  53.             // begin to read.  
  54.             disk_req.nAction=READ;  
  55.             disk_req.off=0; // read from beginning  
  56.             char* s=(char*)malloc(req->request);  
  57.             g_pDiskMan->Read(&disk_ctx,s,req->request,&disk_req);  
  58.         }else if(req->nAction==READ){  
  59.             assert(req->xfered==req->request);  
  60.             fprintf(stderr,<span class="org-string">"(%d)%s\n"</span>,id,req->buf);  
  61.             // free read buffer.  
  62.             free(req->buf);  
  63.             atomic_add(&worker,-1);  
  64.         }  
  65.     }  
  66. };  
  67.   
  68. int main(){  
  69.     // use 4 disk threads.  
  70.     InitKylin(1,1,disk_thread_num);  
  71.     std::vector< XAsyncClient* > vec;  
  72.     for(int i=0;i<worker_num;i++){  
  73.         XAsyncClient* client=new XAsyncClient(i);  
  74.         vec.push_back(client);  
  75.         client->Start();  
  76.     }  
  77.     // 100ms.  
  78.     const struct timespec timeout={0,100*1000000};  
  79.     while(1){  
  80.         nanosleep(&timeout,NULL);  
  81.         if(AtomicGetValue(worker)==0){  
  82.             StopKylin(true);  
  83.             break;  
  84.         }  
  85.     }  
  86.     for(int i=0;i<worker_num;i++){  
  87.         XAsyncClient* client=vec[i];  
  88.         client->Print();  
  89.         delete client;  
  90.     }  
  91.     return 0;  
  92. }  

1.2.8 NetworkMan

1.2.8.1 Overview

和网络相关的也有两个比较重要的类。同样和DiskMan相同,NetworkMan也提供了NetContext和NetRequest.

[cpp]  view plain copy
  1. // 网络请求  
  2. struct NetRequest {  
  3.     union {  
  4.         AsyncContext async;  
  5.         DLINK link;  
  6.     };  
  7.     /* !! the first element must be AsyncContext */  
  8.     union {  
  9.         BufHandle hdl;  
  10.         struct {  
  11.             BufHandle *pHdl;    // not used by read  
  12.             void* buf;  
  13.             int len;            // buffer len  
  14.             int request;        // request len  
  15.         };  
  16.     };  
  17.     int xfered; // 已经读取了多少个字节  
  18.     uint32 ip;                  // for UDP  
  19.     uint16 port;                // for UDP  
  20. };  
  21.   
  22. // Socket相关状态  
  23. enum SocketState {  
  24.     SS_VOID = 0,  
  25.     SS_LISTENING_0,  
  26.     SS_LISTENING,  
  27.     SS_CONNECTING_0,  
  28.     SS_CONNECTING,  
  29.     SS_CONNECTED_0,  
  30.     SS_CONNECTED,  
  31.     SS_ERROR,  
  32.     SS_SHUTDOWN,  
  33. };  
  34.   
  35. // Socket Flag  
  36. enum SocketFlag {  
  37.     SF_DIRECT_CALLBACK  = 0x1, // 处理完成之后回调函数直接在Network线程执行而不丢到CPU线程  
  38.     SF_PERMANENT        = 0x2, // todo(zhangyan04):???  
  39.     SF_UDP              = 0x4, // 使用UDP协议  
  40.     SF_DONT_EXHAUST     = 0x8, // todo(zhangyan04):???  
  41. };  
  42.   
  43. // 网络相关操作的Context  
  44. struct NetContext {  
  45.     SOCKET s; // 网络socket  
  46.     SocketState state; // socket状态  
  47.   
  48.     DLINK link;                 // to link all active sockets  
  49.     CLockedInt tWrite, tRead; // todo(zhangyan04):???  
  50.     TranQueue qRead, qWrite; // 读写请求队列,push_back和pop_front需要加锁但是不用等待。  
  51.     NetRequest *pReadReq, *pWriteReq; // 当前读写请求  
  52.     BufHandle wHdl; // 写BufHandle,StartWrite里面多次写的话当前BufHandle就保存在这里。  
  53.     // nDelayRead表示是否已经发生了Delay操作,不允许多次发起Delay操作  
  54.     // nEnabled表示当前Context是否可用  
  55.     volatile int nDelayRead, nEnabled;  
  56.     uint32 nTimeout; // 超时时间  
  57.     uint64 tTimeoutCycle; // 超时时间转换成为的cycle,类似于一个绝对的超时时间  
  58.   
  59.     CAsyncClient *pClient; // 关联的client  
  60.     CEPoller* pPoller; // 底层poller  

你可能感兴趣的:(多线程,C++)