不知你是否有过类似如下的需求:
有一些功能,它们足够单一,但又需要后台持续运行,以容器实现感觉太重了,以进程实现又太琐碎了,以线程实现可以接受但是又不好管理。
这类程序诸如:数据采集程序、可观测性程序、中间件、代理等等。
这一需求乍看之下倒是有点类似 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
创建子线程hello
timer_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
必须是on
thread_mode
必须是on
trace_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 仓库。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.