分类 C/C++/Rust 下的文章

最近一直再看 PHP 的底层源码,同时也对如何开发一门语言来了兴趣,看了《自制编程语言》这本书,在第一个计算机的例子上就栽了跟头。

按照书上的例子写完代码后报错:

y.tab.c:1049:16: error: implicit declaration of function 'yylex' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
      yychar = yylex ();
               ^
y.tab.c:1224:7: error: implicit declaration of function 'yyerror' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
      yyerror (YY_("syntax error"));
      ^
y.tab.c:1335:3: error: implicit declaration of function 'yyerror' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
  yyerror (YY_("memory exhausted"));
  ^

最终 google 了一番发现很多人卡在这里。

https://stackoverflow.com/questions/20106574/simple-yacc-grammars-give-an-error

最终找到了这个提问解决了这个问题,其实主要是找不到定义文件,我们在定义 yacc 文件的时候,需要在头部定义一下。

%{
#include <stdio.h>
#include <stdlib.h>
#define YYDEBUG 1
int yylex(void);
int yyerror(char const *str);
%}

定时器方案:红黑树、时间轮、最小堆

定时器的应用:

  • ⼼跳检测
  • 技能冷却
  • 武器冷却
  • 倒计时

定时器概述

对于服务端来说,驱动服务端逻辑的事件主要有两个,⼀个是⽹络事件,另⼀个是时间事件;

在不同框架中,这两种事件有不同的实现⽅式;

  • 第⼀种,⽹络事件和时间事件在⼀个线程当中配合使⽤;例如nginx、redis;
  • 第⼆种,⽹络事件和时间事件在不同线程当中处理;例如skynet;
// 第⼀种 ⽹络事件和时间事件在⼀个线程当中配合使⽤;
while (!quit) {
        int now = get_now_time();// 单位:ms
         int timeout = get_nearest_timer() - now;
         if (timeout < 0) {
      timeout = 0;
    }
         int nevent = epoll_wait(epfd, ev, nev, timeout);
         for (int i=0; i<nevent; i++) {
                 //... ⽹络事件处理
         }
         update_timer(); // 时间事件处理
}

// 第⼆种 ⽹络事件和时间事件在不同线程当中处理;
void* thread_timer(void * thread_param) {
        init_timer();
         while (!quit) {
                 update_timer(); // 更新检测定时器,并把定时事件发送到消息队列中
                 sleep(t); // 这⾥的 t 要⼩于 时间精度
         }
         clear_timer();
 return NULL; 
}

pthread_create(&pid, NULL, thread_timer, &thread_param);

定时器设计

接⼝设计

// 初始化定时器
void init_timer();
// 添加定时器
Node* add_timer(int expire, callback cb);
// 删除定时器
bool del_timer(Node* node);
// 找到最近要发⽣的定时任务
Node* find_nearest_timer();
// 更新检测定时器
void update_timer();
// 清除定时器
void clear_timer();

数据结构选择

  • 红⿊树:对于增删查,时间复杂度为$O(log_2n)$ ;对于红⿊树最⼩节点为最左侧节点,时间复杂度为 $O(log_2n) $;
  • 最⼩堆:对于增查,时间复杂度为 $O(log_2n)$ ;对于删时间复杂度为 $O(n)$,但是可以通过辅助数据结构(map或者hashtable来快速索引节点)来加快删除操作;对于最⼩节点为根节点,时间复杂度为 $O(1)$;
  • 跳表:对于增删查,时间复杂度为$O(log_2n)$ ;对于跳表最⼩节点为最左侧节点,时间复杂度为$O(1)$;但是空间复杂度⽐较⾼,为 $O(1.5n)$;
  • 时间轮:对于增删查,时间复杂度为 $O(1)$ ;查找最⼩节点也为 $O(1)$;

要点

  1. 有序的结构,且增加删除操作不影响该结构有序;
  2. 能快速查找最⼩节点;
  3. 时间轮增加操作只从单个定时任务触发,忽略定时任务之间的⼤⼩关系;⽽红⿊树、最⼩堆、跳表的有序性依赖定时任务之间的⼤⼩关系;

红⿊树

void ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *temp,
ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel) {
    ngx_rbtree_node_t **p;
     for ( ;; ) {
         // 这⾥是重点
         p = ((ngx_rbtree_key_int_t) (node->key - temp->key) < 0) ? &temp->left : &temp->right;
         
        if (*p == sentinel) {
             break;
         }
         temp = *p;
     }
 
    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}

STL中 map 结构采⽤的是红⿊树来实现,但是定时器不要使⽤ map 结构来实现,因为可能多个定时任务需要同时被触发, map 中的key是惟⼀的;

红⿊树的节点同时包含 key 和 val ,红⿊树节点的有序由 key 来决定的;插⼊节点的时候,通过⽐较key来决定节点存储位置;红⿊树的实现并没有要求 key 唯⼀;如上代码示例, for 循环中 (node->key - temp->key) < 0)? &temp->left : &temp->right; 当key相同的时候取值为 temp->right ;

思考: map 结构中插⼊节点这⾥如何操作?

最⼩堆

概述

满⼆叉树:所有的层节点数都是该层所能容纳节点的最⼤数量(满⾜ );

完全⼆叉树:若⼆叉树的深度为 h ,除了 h 层外,其他层的节点数都是该层所能容纳节点的最⼤数量(满⾜ $2^n;n >= 0$),且 h 层都集中在最左侧;

最⼩堆

  1. 是⼀颗完全⼆叉树;
  2. 某⼀个节点的值总是⼩于等于它的⼦节点的值;
  3. 堆中每个节点的⼦树都是最⼩堆;

 title=

增加操作

为了满⾜完全⼆叉树定义,往⼆叉树最⾼层沿着最左侧添加⼀个节点;然后考虑是否能上升操作;

如果此时添加值为 4 的节点, 4 节点是5节点的左⼦树; 4 ⽐ 5 ⼩, 4 和 5 需要交换位置;

删除操作

删除操作需要先查找是否包含这个节点,最⼩堆的查找效率是 $O(n)$;查找之后,交换最后⼀个节点,先考虑下降操作,如果操作失败则上升操作;最后删除最后⼀个节点;

例如:假设删除 1 号节点,则需要下沉操作;假设删除 9 号节点,则需要上升操作;

时间轮

 title=

从时钟表盘出发,如何⽤数据结构来描述秒表的运转;

int seconds[60]; // 数组来描述表盘刻度;

++tick 60;每秒钟 ++tick 来描述秒针移动;对让秒针永远在间$[0,59]$移动;

对于时钟来说,它的时间精度(最⼩运⾏单元)是1秒;

单层级时间轮

背景

⼼跳检测:客户端每 5 秒钟发送⼼跳包;服务端若 10 秒内没收到⼼跳数据,则清除连接;

实际在开发过程中,若收到除了⼼跳包的其他数据,⼼跳检测也算通过,在这⾥为了简化流程,只判断⼼跳包;

作为对⽐:我们假设使⽤ map<int, conn*> 来存储所有连接数;每秒检测 map 结构,那么每秒需要遍历所有的连接,如果这个map结构包含⼏万条连接,那么我们做了很多⽆效检测;考虑极端情况,刚添加进来的连接,下⼀秒就需要去检测,实际上只需要10秒后检测就⾏了;那么我们考虑使⽤时间轮来检测;

注意:这个例⼦只是⽤来帮助理解时间轮,不代表实际解决⽅案;

设计

1. 准备⼀个数组存储连接数据;那么数组⻓度设置为多少?

2. 考虑⼀秒内添加了多条连接,那么可以参考 hash 结构处理冲突的⽅式,⽤链表链接起来;

3. 回到 1 中的问题,如果想 2 中链表稀疏,将数组⻓度设置⼤⼀些;如果想紧凑些,则将数组⻓度设置⼩些(但是必须⼤于10);

4. 假设我们设置数组⻓度为 11;那么检测指针的移动可描述为 ++point % 11;

$\ m\%n = m - n \times floor(m/n)$

优化:将 n 替换为 ,这⾥ 恰好⼤于 n;这样以来 可以转化为;所以我们可以选择 16( ),那么检测指针移动可优化为 ++point ;

5. 考虑到正常情况下 5 秒钟发送⼀次⼼跳包,10 秒才检测⼀次,如下图到索引为 10 的时候并不能踢掉连接;所以需要每收到⼀次⼼跳包则 used++ ,每检测⼀次 used-- ;当检测used == 0 则踢掉连接;

 title=

多层级时间轮

背景

参照时钟表盘的运转规律,可以将定时任务根据触发的紧急程度,分布到不同层级的时间轮中;假设时间精度为 10ms ;在第 1 层级每 10ms 移动⼀格;每移动⼀格执⾏该格⼦当中所有的定时任务;

当第 1 层指针从 255 格开始移动,此时层级 2 移动⼀格;层级 2 移动⼀格的⾏为定义为,将该格当中的定时任务重新映射到层级 1 当中;同理,层级 2 当中从 63 格开始移动,层级 3 格⼦中的定时任务重新映射到层级 2 ; 以此类推层级 4 往层级 3 映射,层级 5 往层级 4 映射;

如何重新映射?定时任务的过期时间对上⼀层级的⻓度取余分布在上⼀层级不同格⼦当中;

 title=

添加节点

void add_node(timer_t *T, timer_node_t *node) {
    uint32_t time=node->expire;
    uint32_t current_time=T->time;
    uint32_t msec = time - current_time;
     if (msec < TIME_NEAR) { //[0, 0x100)
         // time % 256
         link(&T->near[time&TIME_NEAR_MASK],node);
    } else if (msec < (1 << (TIME_NEAR_SHIFT + TIME_LEVEL_SHIFT))) {
        //[0x100, 0x4000)
        // floor(time/2^8) % 64
         link(&T->t[0][((time >> TIME_NEAR_SHIFT) & TIME_LEVEL_MASK)],node); 
    } else if (msec < (1 << (TIME_NEAR_SHIFT + 2 *TIME_LEVEL_SHIFT))) {
        //[0x4000, 0x100000)
         // floor(time/2^14) % 64
         link(&T->t[1][((time >> (TIME_NEAR_SHIFT + TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node); 
    } else if (msec < (1 << (TIME_NEAR_SHIFT + 3 *TIME_LEVEL_SHIFT))) {
        //[0x100000, 0x4000000)
         // floor(time/2^20) % 64
         link(&T->t[2][((time >> (TIME_NEAR_SHIFT + 2 * TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node);  
    } else {
        //[0x4000000, 0xffffffff]
         // floor(time/2^26) % 64
         link(&T->t[3][((time >> (TIME_NEAR_SHIFT + 3 * TIME_LEVEL_SHIFT)) & TIME_LEVEL_MASK)],node); 
 } }

重新映射

void timer_shift(timer_t *T) {
    int mask = TIME_NEAR;
     uint32_t ct = ++T->time; // 第⼀层级指针移动 ++ ⼀次代表10ms
     if (ct == 0) {
         move_list(T, 3, 0);
    } else {
         // floor(ct / 256)
         uint32_t time = ct >> TIME_NEAR_SHIFT;
         int i=0;
         // ct % 256 == 0 说明是否移动到了 不同层级的 最后⼀格
         while ((ct & (mask-1))==0) {
             int idx=time & TIME_LEVEL_MASK;
             if (idx!=0) {
                 move_list(T, i, idx); // 这⾥发⽣重新映射,将i+1层级idx格⼦中的定时任务重新映射到i层级中
             }
             mask <<= TIME_LEVEL_SHIFT;
             time >>= TIME_LEVEL_SHIFT;
             ++i;
         }
     } 
}

协程存在的原因?协程能够解决哪些问题?

在我们现在CS,BS开发模式下,服务器的吞吐量是一个很重要的参数。其实吞吐量是 IO 处理时间加上业务处理。

为了简单起见,比如,客户端与服务器之间是长连接的,客户端定期给服务器发送心跳包数据。客户端发送一次心跳包到服务器,服务器更新该新客户端状态的。心跳包发送的过程,业务处理时长等于IO读取(RECV系统调用)加上业务处理(更新客户状态)。吞吐量等于1s业务处理次数。

2023-03-25T13:13:29.png

业务处理(更新客户端状态)时间,业务不一样的,处理时间不一样,我们就不做讨论。那如何提升recv的性能。

若只有一个客户端,recv的性能也没有必要提升,也不能提升。若在有百万计的客户端长连接的情况,我们该如何提升。以Linux为例,在这里需要介绍一个“网红”就是epoll。服务器使用epoll管理百万计的客户端长连接,代码框架如下:

while (1) {
    int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);
    for (i = 0;i < nready;i ++) {
        int sockfd = events[i].data.fd;
        if (sockfd == listenfd) {
            int connfd = accept(listenfd, xxx, xxxx);
 
            setnonblock(connfd);
            ev.events = EPOLLIN | EPOLLET;
            ev.data.fd = connfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
        } else {
            handle(sockfd);
        }
    } 
}

对于响应式服务器,所有的客户端的操作驱动都是来源于这个大循环。来源于epoll_wait的反馈结果。对于服务器处理百万计的IO。Handle(sockfd)实现方式有两种。

第一种,handle(sockfd) 函数内部对 sockfd 进行读写动作。代码如下:

int handle(int sockfd) {

 recv(sockfd, rbuffer, length, 0);
 
 parser_proto(rbuffer, length);

 send(sockfd, sbuffer, length, 0);
 
}

handle 的 io 操作(send,recv)与 epoll_wait 是在同一个处理流程里面的。这就是 IO 同步操作。
优点:

  1. sockfd 管理方便。
  2. 操作逻辑清晰。

缺点:

  1. 服务器程序依赖 epoll_wait 的循环响应速度慢。
  2. 程序性能差

第二种,handle(sockfd)函数内部将 sockfd 的操作,push 到线程池中,代码如下:

int thread_cb(int sockfd) {
 // 此函数是在线程池创建的线程中运行。
 // 与 handle 不在一个线程上下文中运行
 recv(sockfd, rbuffer, length, 0);
 parser_proto(rbuffer, length);
 send(sockfd, sbuffer, length, 0);
}
int handle(int sockfd) {
 //此函数在主线程 main_thread 中运行
 //在此处之前,确保线程池已经启动。
 push_thread(sockfd, thread_cb); //将 sockfd 放到其他线程中运行。
}

Handle 函数是将 sockfd 处理方式放到另一个已经其他的线程中运行,如此做法,将 io 操作(recv,send)与 epoll_wait 不在一个处理流程里面,使得 io 操作(recv,send)与 epoll_wait 实现解耦。这就叫做 IO 异步操作。

优点:

  1. 子模块好规划。
  2. 程序性能高。

缺点:

正因为子模块好规划,使得模块之间的 sockfd 的管理异常麻烦。每一个子线程都需要管理好 sockfd,避免在 IO 操作的时候,sockfd 出现关闭或其他异常。

上文有提到 IO 同步操作,程序响应慢,IO 异步操作,程序响应快。

C 语言实现协程,最困难的部分就是上下文信息的保存和还原。这样才能够做到,让协程在任意位置让出执行权限,稍后再恢复到中断位置继续执行。C 实现协程一般有几个方案。

使用第三方库来保存恢复上下文数据,比如ucontext

使用汇编来保存上下文信息

使用setjmp / longjmp 保存恢复上下文信息

使用switch case的特性来做上下文断点继续,上下文信息需要用static变量保存。比如Protothreads

使用线程来保存上下文信息

本文,使用了switch case的特性来保存中断位置,使用数据结构和static变量来保存上下文信息,使用宏来构建API调用。由于我使用过lua和unity c#协程进行了产品开发。所以,这套实现会贴近unity中C#的使用习惯。完成了一下功能:

在协程执行的任意位置暂停,让出执行权限

恢复协程继续上次中断的地方继续执行

通过static变量和数据结构保存协程数据

协程让出执行后,等待特定的帧数,时间,和其它协程完成

开始看代码:

typedef enum
{
/**
* Coroutine wait for frame count to waitValue
*/
coroutine_wait_frame,
 
/**
* Coroutine wait for second count to waitValue
*/
coroutine_wait_second,
 
/**
* Coroutine wait for other Coroutine to finish
*/
coroutine_wait_coroutine,
 
/**
* Coroutine just run forward
*/
coroutine_wait_none,
}
CoroutineWaitType;

先定义协程让出执行后,等待的类型。可以看到这里定义了几种类型,可以等待帧数,时间,其它协程。

typedef enum
{
/**
* Coroutine enter queue ready to running
*/
coroutine_state_ready,
 
/**
* Coroutine has started to execute
*/
coroutine_state_running,
 
/**
* Coroutine already finished and waiting for reuse
*/
coroutine_state_finish,
}
CoroutineState;

这里定义协程的状态。等待执行,正在执行包括中断的也算在执行的,还有执行完成的。我们后面会介绍,有一个协程管理器。所有的协程进入管理器,被轮询检测。完成后的协程会被缓存起来,下次请求协程的时候会先检查缓存的协程可否使用。

typedef struct Coroutine Coroutine;
typedef void (*CoroutineRun)(Coroutine* coroutine);
 
struct Coroutine
{
/**
* Record coroutine run step
*/
int step;
 
/**
* Coroutine implement function
*/
CoroutineRun Run;
 
/**
* Coroutine current state
*/
CoroutineState state;
 
/**
* Coroutine wait value to execute
*/
float waitValue;
 
/**
* Record wait progress
*/
float curWaitValue;
 
/**
* Coroutine wait types
*/
CoroutineWaitType waitType;
 
/**
* Hold params for CoroutineRun to get
* when coroutine finish clear but the param create memory control yourself
*/
ArrayList(void*) params[1];
 
/**
* Hold Coroutines wait for this Coroutine to finish
*/
ArrayList(Coroutine*) waits [1];
};

这里定以了一个协程的数据结构。CoroutineRun 就是一个C语言的函数,真正执行的协程函数。

step 用来保存CoroutineRun执行到哪一行了。下次继续这一行执行。后面会介绍,使用宏定义 __LINE__来捕获函数执行的函数,保存到step。

Run 就是执行的函数指针。

state 用来标示协程处在什么状态。

waitValue 表示协程等待的数值,帧数还是时间。

curWaitValue 就是当前等待了多少数值,这个值抵达waitValue表示协程等待结束了。

waitType 表示等待的类型。是等待帧数,还是时间,还是其它协程完成。

params 是绑定的一个动态数组,存放需要在协程函数里使用的参数。ArrayList是自定义类型,可以替换为其它相同实现。后面的()仅仅是一个空参数的宏定义。

waits 也是一个动态数组,存放的是等待当前协程的其它协程。也就是说有多个协程在等待这个协程,当这个协程完成的时候会释放等待队列的其它协程。这里并没有使用一个指针保存等待的协程,而是选择了保存等待自己的协程数组。因为协程使用了缓存系统,一个协程结束,就要进入缓存队列,依赖它的协程需要立马得到通知。

接下来,我们提供一组宏定义,用在 CoroutineRun 中,来完成协程的功能。

#define ACoroutineAddParam(coroutine, value) \
AArrayListAdd(coroutine->params, value)
 
/**
* return value
*/
#define ACoroutineGetParam(coroutine, index, type) \
AArrayListGet(coroutine->params, index, type)
 
/**
* return valuePtr
*/
#define ACoroutineGetPtrParam(coroutine, index, type) \
AArrayListGetPtr(coroutine->params, index, type)

这是在协程对象上绑定和获取数据,为了在协程函数内使用外部数据。就是使用协程对象的params数组。

#define ACoroutineBegin() \
switch (coroutine->step) \
{ \
case 0: \
coroutine->state = coroutine_state_running
 
#define ACoroutineEnd() \
} \
coroutine->state = coroutine_state_finish \

这两个宏是协程主体功能的开始和结束。在这两段之内的代码,可以通过后面提供的宏进行中断。这里是建立了一个switch case代码段,协程的代码处在这个代码段中,就可以利用case任意跳转。每次跳转的位置由step标识。

#define ACoroutineYieldFrame(waitFrameCount) \
coroutine->waitValue = waitFrameCount; \
coroutine->curWaitValue = 0.0f; \
coroutine->waitType = coroutine_wait_frame; \
coroutine->step = __LINE__; \
return; \
case __LINE__: \
 
#define ACoroutineYieldSecond(waitSecond) \
coroutine->waitValue = waitSecond; \
coroutine->curWaitValue = 0.0f; \
coroutine->waitType = coroutine_wait_second; \
coroutine->step = __LINE__; \
return; \
case __LINE__: \
 
#define ACoroutineYieldCoroutine(waitCoroutine) \
coroutine->waitValue = 0.0f; \
coroutine->curWaitValue = 0.0f; \
coroutine->waitType = coroutine_wait_coroutine; \
AArrayListAdd((waitCoroutine)->waits, coroutine); \
coroutine->step = __LINE__; \
return; \
case __LINE__: \

这里提供了,在begin和end之间中断的功能,等待帧数,等待时间,等待其它协程。原理是,使用这几个宏的时候,会用__LINE__赋值step,这样step就持有了当前行数变量。先return结束函数,在添加了case __LINE__,这样下次再次执行这个函数的时候,就会直接跳到上次return后的一个case上,继续执行。保存状态的变量需要使用static local变量保存,或是利用params传入。

#define ACoroutineYieldBreak() \
coroutine->state = coroutine_state_finish; \
return \

中断协程就是设置状态直接跳出。由于在begin和end中可能嵌套有循环,所以不能break,要直接return。
那看看怎么使用:

static void CRun(Coroutine* coroutine)
{
ACoroutineBegin();
ALogD("### begin");
 
ACoroutineYieldSecond(5.0f);
ALogD("### yield second 5");
 
ACoroutineYieldSecond(10.0f);
ALogD("### yield second 10");
 
ACoroutineYieldFrame(100.0f);
ALogD("### yield frame 100");
 
ACoroutineEnd();
}
 
void main()
{
ACoroutine->StartCoroutine(CRun);
}

只要在begin和end之间,使用Yield就可以让出执行流程,然后在返回接着执行。再次强调,需要保存进度的变量,需要使用params保存或是static local变量。那么,让出执行流程,是如何恢复的呢。那是因为所有协程都在一个协程管理器。协程管理器每帧都会执行控制协程的流程。代码如下。

struct ACoroutine
{
/**
* Bind CoroutineRun with Coroutine and enter queue ready to run
*/
Coroutine* (*StartCoroutine)(CoroutineRun Run);
 
/**
* Update on every frame
*/
void (*Update) (float deltaTime);
};

extern struct ACoroutine ACoroutine[1];

协程管理器,需要一个CoroutineRun函数就可以启动,然后在 CoroutineRun 中使用协程的功能。协程管理器的完整实现如下。


static ArrayIntMap(Coroutine*) coroutineMap [1] = AArrayIntMapInit(Coroutine*, 20);
static ArrayList (Coroutine*) coroutineList[1] = AArrayListInit (Coroutine*, 20);
 
static Coroutine* StartCoroutine(CoroutineRun Run)
{
    Coroutine* coroutine = AArrayListPop(coroutineList, Coroutine*);
 
    if (coroutine == NULL) {
        coroutine = (Coroutine*) malloc(sizeof(Coroutine));
 
        AArrayList->Init(sizeof(void*), coroutine->params);
        coroutine->params->increase = 4;
        AArrayList->Init(sizeof(Coroutine*), coroutine->waits);
        coroutine->waits->increase = 4;
    } else {
        AArrayList->Clear(coroutine->params);
        AArrayList->Clear(coroutine->waits);
    }
 
    coroutine->Run = Run;
    coroutine->step = 0;
    coroutine->waitValue = 0.0f;
    coroutine->curWaitValue = 0.0f;
    coroutine->waitType = coroutine_wait_none;
    coroutine->state = coroutine_state_ready;
 
    AArrayIntMapPut(coroutineMap, coroutine, coroutine);
 
    return coroutine;
}
 
static void Update(float deltaTime)
{
for (int i = coroutineMap->arrayList->size - 1; i > -1; i--)
{
Coroutine* coroutine = AArrayIntMapGetAt(coroutineMap, i, Coroutine*);
 
if (coroutine->waitType == coroutine_wait_coroutine)
{
continue;
}
else if (coroutine->curWaitValue >= coroutine->waitValue)
{
coroutine->Run(coroutine);
 
if (coroutine->state == coroutine_state_finish)
{
AArrayIntMap->RemoveAt(coroutineMap, i);
 
// add to cache
AArrayListAdd(coroutineList, coroutine);
 
// set waiting coroutines execute forward
for (int j = 0; j < coroutine->waits->size; j++)
{
Coroutine* wait = AArrayListGet(coroutine->waits, j, Coroutine*);
 
ALogA
(
wait->state != coroutine_state_finish,
"Coroutine [%p] can not finish before wait coroutine [%p] finish",
wait, coroutine
);
 
wait->waitType = coroutine_wait_none;
}
 
continue;
}
}
else
{
switch (coroutine->waitType)
{
case coroutine_wait_frame:
coroutine->curWaitValue += 1.0f;
break;
 
case coroutine_wait_second:
coroutine->curWaitValue += deltaTime;
break;
}
}
}
}
 
struct ACoroutine ACoroutine[1] =
{
    StartCoroutine,
    Update,
};

代码量很少,Update函数需要每帧都调用。ArrayIntMap 和ArrayList 就是自定义的字典映射和动态数组。我在开发游戏中使用过lua和unity的C#中的协程。这套实现也是模拟了unity里面协程的接口。最后,说一下个人理解的协程的好处。

协程,能够把一个计算或是操作,分解成若干步,并且可以再任何一步停下来,并在需要的时候继续执行剩下的步骤。

这样的模型,给予了更细粒度的控制一个操作或是功能。

比如,一个非常耗时间的操作,被分步执行可以更好的控制程序响应。

比如,一个操作需要依赖各种条件,可以更好的处理条件不满足时候的情况。

也能够更好的把操作或是计算过程中的状态变化,与其它的状态变化交互。而然,程序运行的过程就是抽象数据和结构不断变化的过程,协程能够优雅自然的进行这个变化过程的需求。

WebAssembly

通过Web执行一种类似于机器码的程序,简称wasm。相对于JS解析执行性能大大提升,目前主流浏览器都已支持。
wasm 本身是一种字节码标准,一般通过c/c++、GO、Rust来进行开发,并编译成wasm。其中Rust在这块相对更活跃一些
场景:如图像处理、视觉效果、3D游戏、其他需要CPU高性能计算的场景

编译出第一个wasm模块

安装 wasm-pack,它是 Rust-Wasm 官方工作组开发,用于构建 wasm 应用程序的工具。

cargo install wasm-pack

创建项目,因为编写的是 wasm 所以我们选择 lib 。

cargo new --lib  mywasm

修改 Cargo.toml 增加依赖项。

[package]
name = "mywasm"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

修改 lib.rs

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn echo() -> String {
    format!("{}", "maksim")
}

如果我们想要对外暴露函数可以在函数上方加上 #[wasm_bindgen] 注解,在上面的额代码中,我们暴露了一个 echo 函数,用来打印输出一段字符。

接下来,我们在命令行中执行:

$ wasm-pack build -t nodejs

$ ls pkg/
mywasm.d.ts         mywasm.js           mywasm_bg.js        mywasm_bg.wasm      mywasm_bg.wasm.d.ts package.json

wasm-pack build 是用来构建代码,-t nodejs 是用于指定生成 nodejs 可引入的 wasm 代码。pkg是我们生成的 wasm 文件。

使用 node 调用我们的 wasm 模块

我们在根目录中建立一个 test.js 的文件。

let {echo} =  require("./pkg/mywasm");

console.log(echo());

执行结果如下:

$  node test.js
maksim

从 js 中导入函数到 Rust

我们如果想要使用js 中的函数只需要将 js 中的函数原型引入到 rust 中即可,wasm_bindgen 中提了该功能。

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    #[wasm_bindgen(js_namespace = console)]
    fn log(str: &str);
}

#[wasm_bindgen]
pub fn echo()  {
    log("hello maksim")
}

执行结果如下:

hello maksim
undefined

支持参数打印

现在我们对 echo 增加支持传递参数。

#[wasm_bindgen]
pub fn echo(s: &str)  {
    log(s)
}

修改test.js 的调用。

let {echo} =  require("./pkg/mywasm");

console.log(echo('abc'));

// 运行结果
//abc
//undefined

用宏来简化 echo

针对于这类的输出函数,其实我们使用宏要更方便一些。

macro_rules! echo {
    ($expr:expr) => {
        log(format!("{}", $expr).as_str());
    };
}

#[wasm_bindgen]
pub fn echo(s: &str)  {
    echo!(s)
}

在 JS 中使用 Rust 的结构体

我们在 lib.rs 下增加一个 UserModel 的结构体,并且增加两个函数。

#[wasm_bindgen]
pub struct UserModel {
    user_id: i32,
}

#[wasm_bindgen]
impl UserModel {
    pub  fn get_user_id(&self) -> i32 { 
        self.user_id 
    }
}

#[wasm_bindgen]
pub fn new_user(id: i32) -> UserModel {
    UserModel { user_id: id }
}
  • get_user_id 用来返回 user_id。
  • new_user 用来创建 UserModel 结构体

修改 test.js

let {echo, new_user} =  require("./pkg/mywasm");

let user = new_user(12);
echo(user.get_user_id().toString());

由于目前 echo 仅支持字符串,我们需要对 get_user_id() 的返回值进行类型转换。

运行结果如下:

$ node test.js             
12

接下来,我们对这段代码进行优化,因为我们的代码不可能只写在一个文件里面。我们在 src 目录下创建一个名叫 models 的文件夹,用来存放我们的 model 代码。

首先是创建 mod.rs 这是 Rust 的基础,用来导出模块。

pub mod user_model;

然后创建 user_model.rs。

extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct UserModel {
    user_id: i32,
}

#[wasm_bindgen]
impl UserModel {
    pub  fn get_user_id(&self) -> i32 { 
        self.user_id 
    }

    #[wasm_bindgen(constructor)]
    pub fn new() -> UserModel {
        UserModel { user_id: -1 }
    }
}

#[wasm_bindgen]
pub fn new_user(id: i32) -> UserModel {
    UserModel { user_id: id }
}

一定要注意最上面的导入,由于已经拆分成独立的文件了,如果没有导入会报错,同时我们在代码中增加了下面这段代码

#[wasm_bindgen(constructor)]
pub fn new() -> UserModel {
    UserModel { user_id: -1 }
}

对应到 js 中就是构造方法。

let {echo, UserModel} =  require("./pkg/mywasm");

let user = new UserModel();
echo(user.get_user_id().toString());

执行代码会输出-1。

接下来我们增加 getter 方法。

#[wasm_bindgen(getter)]
pub fn uid(&self) -> i32 { 
        self.user_id 
}

#[wasm_bindgen(setter)]
pub fn set_uid(&mut self, value: i32) { 
        self.user_id = value 
}

其中 setter 的命名是固定写法,必须以 set_ 开头,代码中还需要注意 &mut self。