问题描述
不知你是否有过类似如下的需求:
有一些功能,它们足够单一,但又需要后台持续运行,以容器实现感觉太重了,以进程实现又太琐碎了,以线程实现可以接受但是又不好管理。
这类程序诸如:数据采集程序、可观测性程序、中间件、代理等等。
这一需求乍看之下倒是有点类似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的配置文件内容。
整个程序流程大致如下:
- 初始化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仓库。