定时器
这是一篇启发式的文章,重在介绍设计一个功能库的步骤。
我们在开发一个功能库的时候,不能一开始就胡乱的开始编码。最基本的必须要想清楚,你想让库的使用者如何使用这个库。所以在开始编码前,必须尝试来写一个定时器的应用。
定时器语义
定时器的作用是在未来某个时刻执行某项任务,在程序里就是运行一个函数;然而当我们启动定时器后,由于各方面的条件影响,需要取消定时,或修改定时(提前或延后)。所以我们的定时器至少有如下2个语义:
- 启动定时器对象,暂时定义为
timer_start(loop, timer, expires)
。 - 取消定时器对象,暂时定义为
timer_stop(timer)
。
定时器作为一个对象,在启动后需要一个容器来管理它,所以定时器的组成还包括定时器容器。很显然我们在判断哪些定时器已超时,只有遍历这个容器,并检查容器中定时器。既然是遍历,那么就有一个循环语义:
- 定时器系统循环,暂时定义为
loop_start(loop)
。
定时结构
根据上面的说明,定时器至少需要一个超时时间和一个回调函数。 定时器对象:
struct timer {
uint64_t expires;
void (*func)(struct timer*);
};
我们一如既往的使用 侵入式
数据结构来构造任何需要扩展的对象。典型的使用如下:
struct connector {
int fd;
struct timer timedout_checker;
};
我们将定时器所处的上下文都集中在一个内存块中,这样减少了内存碎片,提高了缓存命中,提高效率。
如果我们定义成:
struct timer {
uint64_t expires;
void *data;
void (*func)(void *data);
};
那么定时器和其上下文基本就会被分开分配,内存碎片增加,缓存未命中提高,效率降低。
我们需要一个有序定时器队列,来管理已启动的定时器,定时器容器:
struct timer_loop {
bool running;
struct timer_queue queue;
};
尝试使用API
现在我们将实现一个简单的应用,简单的使用上面给出的 定时器API,
struct hello_timer {
char words[64];
struct timer timer;
struct timer_loop *loop;
};
/*我们使用定时器反复的调用此函数,打印一些信息*/
void say_hello_callback(struct timer *timer_ptr)
{
struct hello_timer *say_hello =
container_of(timer_ptr, strut hello_timer, timer);
printf("say : %s\n", say_hello->words);
/*重启定时*/
timer_start(say_hello.loop, timer_ptr, 10);
}
int main(void)
{
struct timer_loop loop;
struct hello_timer say_hello;
/*loop初始化,略*/
/*初始化*/
say_hellp.loop = &loop;
say_hello.timer.func = say_hello_callback;
snprintf(say_hello.words, sizeof(say_hello.words), "hello timer");
/*启动定时器*/
timer_start(&loop, &say_hello.timer, 10);
/*启动循环*/
loop_start(&loop);
return 0;
}
增加新功能
- 从上面的简单例子可以看到,我们的示例可以在启动循环后,就会一直运行,无法停止。现在我们增加一个新的接口,可以停止循环
loop_stop()
。
现在我们可以实现优雅退出的代码了,我们通过信号来优雅退出循环:
/*信号只能从全局获取上下文*/
struct timer_loop g_loop;
void handle_intr(int signo)
{
loop_stop(&g_loop);
}
/*省略定时器回调*/
...
int main(void)
{
struct hello_timer say_hello;
/*loop初始化,略*/
/*初始化*/
say_hellp.loop = &g_loop;
...
/*安装信号*/
signal(SIGINT, handle_intr);
/*启动定时器,略*/
/*启动循环*/
loop_start(&loop);
/*反初始化 loop 对象*/
...
return 0;
}
现在我们可以通过从终端 kill -2 <pid>
发送一个信号来优雅的停止程序了。
- 我们需要一个能够完全确定定时器没有处于排队或调度的停止函数,因为如果只是简单的停止(从容器中删除),如果此时定时器正在被调度,那么释放掉定时器持有的资源,将会导致系统的不一致性。所以我们增加一个接口,同步删除定时器
timer_stop_sync()
,如果定时器正在被调度,他会等待直到完成。这个函数必须要等待回调函数完成,所以不能在超时回调函数中执行,否则死锁。
现在动态分配一个定时器,然后在另一个线程同步停止、释放这个定时器,这里仅仅为了做一个演示,代码没有任何实际的意义:
void *thread_cb(void *ptr)
{
struct hello_timer *say_timer = ptr;
/*同步删除定时器后,才能释放*/
timer_stop_sync(&say_timer->timer);
/*如果我们使用 timer_stop() 来仅仅从容器中删除就释放,将可能造成程序发生不可预期的错误*/
free(say_timer);
return NULL;
}
int main(void)
{
pthread tid;
struct hello_timer *say_hello;
/*loop初始化,略*/
say_hello = malloc(sizeof(*say_hello));
/*初始化*/
say_hello->loop = &g_loop;
...
/*安装信号,略*/
...
/*启动定时器,略*/
...
/*创建线程*/
pthread_create(&tid, NULL, thread_cb, say_hello);
/*启动循环,略*/
...
/*反初始化 loop 对象*/
...
pthread_join(tid, NULL);
return 0;
}
结束语
我们一步一步丰富一个库,有理有据。没有一股脑的实现一些没有必要的接口,比如自动重启,因为我们在回调再重新调用 timer_start()
就可以了,没有增加库接口的复杂性,简单明了才是最好的,这个是Unix提倡的编程风格。