
不知你是否有过类似如下的需求:
有一些功能,它们足够单一,但又需要后台持续运行,以容器实现感觉太重了,以进程实现又太琐碎了,以线程实现可以接受但是又不好管理。
这类程序诸如:数据采集程序、可观测性程序、中间件、代理等等。
这一需求乍看之下倒是有点类似 supervisor 在做的事情,每个功能一个单一后台进程。诚然进程是一个选择,但是实际使用中则会面临是大量的可执行程序和因人而异的开风格。
当然,选择多线程还有另一个重要原因,这里先卖个关子,我们往下看。
因此,笔者将介绍一个开源 C 语言库Melon ,它实现了一套多线程框架。在这套框架之下,每一个线程是一个独立的功能模块,并且可以接受来自主线程的管理。
关于 Melon 库,这是一个开源的 C 语言库,它具有:开箱即用、无第三方依赖、安装部署简单、中英文文档齐全等优势。
对于上述的问题,我们可以使用这一框架来解决。除此之外,Melon 还支持了另一个功能,这也是选择多线程的原因之一,谜底将在示例中揭晓。
在 Melon 的多线程框架中,有两种方式可以启动不同的线程模块,下面的示例将以动态创建和杀掉线程的方式进行演示。
#include <stdio.h> #include <errno.h> #include <unistd.h> #include "mln_core.h" #include "mln_log.h" #include "mln_thread.h" #include "mln_trace.h" int sw = 0; //开关 switch 缩写 char name[] = "hello"; static void thread_create(mln_event_t *ev); static int hello_entrance(int argc, char *argv[]) { printf("%s\n", __FUNCTION__); while (1) { mln_trace("s", "Hello"); usleep(10); } return 0; } static void timer_handler(mln_event_t *ev, void *data) { if (!sw) { mln_string_t alias = mln_string("hello"); mln_thread_kill(&alias); mln_event_timer_set(ev, 1000, NULL, timer_handler); } else { thread_create(ev); } sw = !sw; } static void thread_create(mln_event_t *ev) { char **argv = (char **)calloc(3, sizeof(char *)); if (argv != NULL) { argv[0] = name; argv[1] = NULL; argv[2] = NULL; mln_thread_create(ev, "hello", THREAD_DEFAULT, hello_entrance, 1, argv); mln_event_timer_set(ev, 1000, NULL, timer_handler); } } int main(int argc, char *argv[]) { struct mln_core_attr cattr; cattr.argc = argc; cattr.argv = argv; cattr.global_init = NULL; cattr.main_thread = thread_create; cattr.worker_process = NULL; cattr.master_process = NULL; if (mln_core_init(&cattr) < 0) { fprintf(stderr, "Melon init failed.\n"); return -1; } return 0; } 可以看到,main函数中只初始化了 Melon 库。而多线程框架也正是在库初始化时启动的。
我们先对程序做大致的描述,然后给出 Melon 的配置文件内容。
整个程序流程大致如下:
thread_create函数对主线程做部分初始化操作,其中:mln_thread_create创建子线程hellotimer_handler,这个函数将每秒钟被调用一次hello被拉起,并 printf 输出函数名后,进入死循环调用mln_trace函数(我们后面马上说到这个函数)timer_handler并执行如下事项:sw为 0 ,则杀掉hello线程,并再次设置定时器事件sw为 1 ,则调用thread_create创建hello线程,并再次设置定时器事件sw的值,保持每秒关闭和启动hello线程我们可以看到,通过mln_thread_create和mln_thread_kill我们可以让主线程动态的拉起和杀掉子线程。
因为我们使用了mln_trace,这个宏函数是将 C 代码中数据投递到脚本层。这么做的好处是,这些数据不需要被写入日志文件,然后再启动另一个程序处理日志文件。也不需要手写 C 代码来将这些数据发送给远端。脚本层有内置的库函数可以轻松完成这些数据的处理、传输、入库等操作。
说了很多关于程序功能的问题,但想要正常启动这个程序还需要正确配置 Melon ,配置文件内容如下:
log_level "none"; //user "root"; daemon off; core_file_size "unlimited"; //max_nofile 1024; worker_proc 1; thread_mode on; framework on; log_path "/usr/local/melon/logs/melon.log"; trace_mode "trace/trace.m"; /* path or off */ 这里主要关注四个配置:
framework必须是onthread_mode必须是ontrace_mode如果想启用mln_trace的功能,这里要给出脚本代码路径,否则给出off表示关闭该功能worker_proc是工作进程数,我们的多线程都是跑在工作进程上的,这样一旦线程有 bug 造成工作进程崩溃,主进程依旧可以拉起新的工作进程继续运行本例的脚本代码使用的就是 Melon 库中自带的默认脚本trace/trace.m。
/* * Copyright (C) Niklaus F.Schen. */ sys = Import('sys'); if (MASTER) sys.print('master process'); else sys.print('worker process'); Pipe('subscribe'); while (1) { ret = Pipe('recv'); if (ret) { for (i = 0; i < sys.size(ret); ++i) { sys.print(ret[i]); } } fi sys.msleep(1000); } Pipe('unsubscribe'); 脚本主要工作就是死循环调用Pipe函数接收mln_trace投递来的数据,并向终端输出。
... [Hello, ] [Hello, ] [Hello, ] 01/29/2023 07:38:23 GMT REPORT: PID:15708 Child thread 'hello' exit. 01/29/2023 07:38:23 GMT REPORT: PID:15708 child thread pthread_join's exit code: 1 hello_entrance [Hello, ] [Hello, ] [Hello, ] ... 可以看到终端上会输出大量[Hello, ],这是脚本层输出的mln_trace投递来的数据。中间会穿插着一些线程退出和启动的打印信息。
感谢阅读!欢迎各位对 Melon 感兴趣的读者访问其Github 仓库。
1 hankai17 2023-01-29 20:44:59 +08:00 已关注 咨询一下作者 Melang 项目里用的是协程实现的吗? |
2 monkeyNik OP @hankai17 是的,Melang 的每一个脚本任务都是一个协程,这些任务可以在同一个线程下分时调度执行,且是由调度器自动切换 而不是调用了某些含有切换功能的函数才让出执行权限。 |
3 LXGMAX 2023-01-30 16:36:00 +08:00 已 star ,有测试过跨平台( arm 、mips )运行吗? |
4 monkeyNik OP @LXGMAX 感谢,曾经在同事的树莓派( arm )上适配了,但后来没有再试过。印象中后来增加的功能也没有对平台有什么特殊处理的地方。如果兄台手头有 arm 环境,还望能借用呀… |