V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
monkeyNik
V2EX  ›  C

开源 C 语言库 Melon:多线程治理

  •  
  •   monkeyNik · 2023-01-29 16:23:51 +08:00 · 927 次点击
    这是一个创建于 671 天前的主题,其中的信息可能已经有所发展或是发生改变。

    问题描述

    不知你是否有过类似如下的需求:

    有一些功能,它们足够单一,但又需要后台持续运行,以容器实现感觉太重了,以进程实现又太琐碎了,以线程实现可以接受但是又不好管理。

    这类程序诸如:数据采集程序、可观测性程序、中间件、代理等等。

    这一需求乍看之下倒是有点类似 supervisor 在做的事情,每个功能一个单一后台进程。诚然进程是一个选择,但是实际使用中则会面临是大量的可执行程序和因人而异的开发风格。

    当然,选择多线程还有另一个重要原因,这里先卖个关子,我们往下看。

    解决方案

    因此,笔者将介绍一个开源 C 语言库——Melon ,它实现了一套多线程框架。在这套框架之下,每一个线程是一个独立的功能模块,并且可以接受来自主线程的管理。

    关于 Melon 库,这是一个开源的 C 语言库,它具有:开箱即用、无第三方依赖、安装部署简单、中英文文档齐全等优势。

    Github repo

    对于上述的问题,我们可以使用这一框架来解决。除此之外,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 的配置文件内容。

    整个程序流程大致如下:

    1. 初始化 Melon 库并运行多线程框架
    2. 调用thread_create函数对主线程做部分初始化操作,其中:
      1. 构建子线程入口参数的字符指针数组
      2. 调用mln_thread_create创建子线程hello
      3. 设置定时器事件timer_handler,这个函数将每秒钟被调用一次
    3. 子线程hello被拉起,并 printf 输出函数名后,进入死循环调用mln_trace函数(我们后面马上说到这个函数)
    4. 主线程每秒钟进入一次timer_handler并执行如下事项:
      1. 如果sw为 0 ,则杀掉hello线程,并再次设置定时器事件
      2. 如果sw为 1 ,则调用thread_create创建hello线程,并再次设置定时器事件
      3. 反转sw的值,保持每秒关闭和启动hello线程

    我们可以看到,通过mln_thread_createmln_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 仓库

    6 条回复    2023-01-30 18:12:27 +08:00
    hankai17
        1
    hankai17  
       2023-01-29 20:44:59 +08:00
    已关注 咨询一下作者 Melang 项目里用的是协程实现的吗?
    monkeyNik
        2
    monkeyNik  
    OP
       2023-01-29 22:26:49 +08:00 via iPhone
    @hankai17 是的,Melang 的每一个脚本任务都是一个协程,这些任务可以在同一个线程下分时调度执行,且是由调度器自动切换 而不是调用了某些含有切换功能的函数才让出执行权限。
    LXGMAX
        3
    LXGMAX  
       2023-01-30 16:36:00 +08:00
    已 star ,有测试过跨平台( arm 、mips )运行吗?
    monkeyNik
        4
    monkeyNik  
    OP
       2023-01-30 16:56:31 +08:00 via iPhone
    @LXGMAX 感谢,曾经在同事的树莓派( arm )上适配了,但后来没有再试过。印象中后来增加的功能也没有对平台有什么特殊处理的地方。如果兄台手头有 arm 环境,还望能借用呀…😂
    LXGMAX
        5
    LXGMAX  
       2023-01-30 17:07:15 +08:00
    @monkeyNik 我这有 arm 和 mips 设备、运行 Linux 和 openwrt 系统,看了库的特性不错,我先交叉编译进去运行看看性能,需要设备的话可以提供一套给你
    monkeyNik
        6
    monkeyNik  
    OP
       2023-01-30 18:12:27 +08:00 via iPhone
    @LXGMAX 好呀,欢迎兄台加入 qq 群,有任何问题可以一起沟通交流呀
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2569 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 05:38 · PVG 13:38 · LAX 21:38 · JFK 00:38
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.