定时器 —— 库的设计

定时器

这是一篇启发式的文章,重在介绍设计一个功能库的步骤。

我们在开发一个功能库的时候,不能一开始就胡乱的开始编码。最基本的必须要想清楚,你想让库的使用者如何使用这个库。所以在开始编码前,必须尝试来写一个定时器的应用。

定时器语义

定时器的作用是在未来某个时刻执行某项任务,在程序里就是运行一个函数;然而当我们启动定时器后,由于各方面的条件影响,需要取消定时,或修改定时(提前或延后)。所以我们的定时器至少有如下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;
}

增加新功能

  1. 从上面的简单例子可以看到,我们的示例可以在启动循环后,就会一直运行,无法停止。现在我们增加一个新的接口,可以停止循环 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> 发送一个信号来优雅的停止程序了。

  1. 我们需要一个能够完全确定定时器没有处于排队或调度的停止函数,因为如果只是简单的停止(从容器中删除),如果此时定时器正在被调度,那么释放掉定时器持有的资源,将会导致系统的不一致性。所以我们增加一个接口,同步删除定时器 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提倡的编程风格。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值