这篇文章中提到了 tsched 的源码可以一读,所以去阅读了一下,总共220来行。
通过上文了解到这段程序实现的是一个任务队列,同时带有线程池。这段程序是计算机操作系统里经典的consumer-producer (生产者-消费者)问题的实现。凡是学过操作系统这门课的,都应该知道这个问题,做过习题。在阅读源码之前可以先尝试用伪代码实现上述生产者-消费者问题。
这是一个线程池,客户端可以提交任务,线程池按照顺序调度执行任务。通过阅读 tsched.h 头文件,知道主要有三个函数:
通过搜索上述三个函数的调用, 知道初始化了两个调度器,有三个地方会提交任务。
上面提到了,有三个生产者会提交任务给线程池:
了解了清楚使用方、使用场景后,就容易读懂逻辑了。这里是一个标准的操作系统中生产者消费者的问题,用的也是标准解法:使用一个互斥量,两个信号量。线程池使用 pthread 来创建。
SSchedQueue 里面就是上述问题中的核心数据结构,除了放置上述提到的互斥量,信号量,还需要一个队列来存储要具体执行的任务。
SSchedMsg 结构来表示线程池任务,包含要执行的具体函数及所需参数。
源码里注释并不多,只能通过看具体实现来了解上述支持的执行模式。看到支持两种模式:执行fp,或者执行 tfp(ahandle, thandle)。
上面提到了生产者,一直没有提到消费者。接着读 sched.c 里的源码,可以看到消费者就是线程池里每个线程的主框架逻辑: taosProcessSchedQueue。平常这些线程处于阻塞状态,等待任务。一旦生产者提交任务后,就会通知到消费者。消费者拿到提交的任务及参数,去执行。执行完之后继续进入上述阻塞的状态,这样周而复始。
这里有个疑问,消费者和生产者之间是异步的。消费完之后,总得有办法通知消费者,这一步在哪里做呢?读到这里可以花点时间翻翻源码,找找答案。
其实秘密也藏在当时提交任务的数据结构里。TDengine 里有样例代码,翻了翻,找到了这个 async demo。可以看到 taos_query_a 就是一个异步的query函数,里面带了 query语句异步执行完成后的回调函数:taos_insert_call_back)。
看的时候内心不断在思考、对比,比如优势、劣势是什么?我会怎么实现
为何使用线程池?
如果用 Go 语言实现,会很简单。使用 channel 来做任务分发,本身就是线程安全的。
使用 C 来写,个人觉得会限制 TDengine 的开源参与方。因为现在市场上会 C 的人比较少,而且主要集中在嵌入式领域。而且 C 的生态一般,语言的轮子比较少,所以很多工作都需要自己做,比如 http server,rpc 等。如果让我来设计实现 TDengine,我可能会优先考虑 Rust,既能精准控制内存,又有比较完善的社区,而且语言处于上升期,容易成为其中的明星项目,会有推广优势,比如能吸引一些本身对数据库不怎么关注,但是对 Rust 感兴趣的程序员。
通过搜索 pthread_create 可以发现系统中还有其他创建线程的地方,并没有用到上述的线程池,比如 dnodeMWrite, TcpPool,cache,sync等。这些地方为什么没有使用线程池呢?
原文:https://www.cnblogs.com/Jack47/p/threadpool-in-tdengine.html