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

C 语言实用第三方库 Melon 之多线程开发

  •  
  •   monkeyNik · 2021-02-14 13:36:22 +08:00 · 1486 次点击
    这是一个创建于 1174 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文转载自本人头条号: https://www.toutiao.com/i6928959491274703371/

    转载请注明出处,感谢!

    在之前的文章中(开发利器——C 语言必备实用第三方库),笔者介绍了一款 Linux/UNIX 下 C 语言库 Melon 的基本功能,并给出了一个简单的多进程开箱即用的例子。

    本文将给大家介绍 Melon 中多线程的使用方法。

    之前的文章中提到过,在 Melon 中有两种多线程模式:

    • 模块化的多线程模式
    • 线程池

    我们将逐一给出实例。

    Melon 的 Github 仓库: https://github.com/Water-Melon/Melon

    模块化线程

    模块化线程是指,每一个线程都是一个独立的代码模块,都有各自对应的入口函数(类似于每一个 C 语言程序有一个 main 函数一样)。

    模块要存放于 Melon/threads/目录下。在现有的 Melon 代码中,包含了两个示例模块——haha 和 hello (名字起得有点随意)。下面,我们以这两个模块为例说明模块化线程的开发和使用流程。

    开发流程

    这里有几点注意事项:

    1. 模块的名字:模块的名字将被用于两个地方,一个是配置文件中,一个是模块入口函数名。前者将在使用流程中说明,后者我们马上将以 haha 为例进行说明。
    2. 模块的参数:参数是在配置文件中给出的,这一点我们在使用流程中将会说明。但是需要注意一点,最后一个参数并不是配置文件中给出的,而是框架自动追加的,是主线程与该线程模块通信的 socketpair 套接字。
    //haha 模块
    
    int haha_main(int argc, char **argv)
    { 
        int fd = atoi(argv[argc-1]);
        mln_thread_msg_t msg;
        int nfds;
        fd_set rdset;
        for (;;) {
            FD_ZERO(&rdset);
            FD_SET(fd, &rdset);
            nfds = select(fd+1, &rdset, NULL, NULL, NULL);
            if (nfds < 0) {
                if (errno == EINTR) continue;
                mln_log(error, "select error. %s\n", strerror(errno));
                return -1;
            }
            memset(&msg, 0, sizeof(msg));
            int n = read(fd, &msg, sizeof(msg));
            if (n != sizeof(msg)) {
                mln_log(debug, "write error. n=%d. %s\n", n, strerror(errno));
                return -1;
            }
            mln_log(debug, "!!!src:%S auto:%l char:%c\n", msg.src, msg.sauto, msg.c);
            mln_thread_clearMsg(&msg);
        }
        return 0;
    }
    

    可以看到,在这个例子中,模块的入口函数名为 haha_main 。对于每一个线程模块来说,他们的入口函数就是他们模块的名称(即文件名)+下划线+main组成的。

    这个例子也很简单,就是利用 select 持续关注主线程消息,当从主线程接收到消息后,就进行日志输出,然后释放资源。

    与之功能对应的就是 hello 这个模块:

    //hello 模块
    #include <assert.h>
      
    static void hello_cleanup(void *data)
    {
        mln_log(debug, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n");
    }
    
    int hello_main(int argc, char **argv)
    {
        mln_thread_setCleanup(hello_cleanup, NULL);
        int i;
        for (i = 0; i < 1; ++i)  {
            int fd = atoi(argv[argc-1]);
            mln_thread_msg_t msg;
            memset(&msg, 0, sizeof(msg));
            msg.dest = mln_string_new("haha");
            assert(msg.dest);
            msg.sauto = 9736;
            msg.c = 'N';
            msg.type = ITC_REQUEST;
            msg.need_clear = 1;
            int n = write(fd, &msg, sizeof(msg));
            if (n != sizeof(msg)) {
                mln_log(debug, "write error. n=%d. %s\n", n, strerror(errno));
                mln_string_free(msg.dest);
                return -1;
            }
        }
        usleep(100000);
        return 0;
    }
    

    这个模块的功能也很简单,就是向主线程发送消息,而消息的接收方是 haha 模块,即主线程是一个中转站,它将 hello 模块的消息转发给 haha 模块。

    在 hello 这个模块中,调用了 mln_thread_setCleanup 函数,这个函数的作用是:在从当前线程模块的入口函数返回至上层函数后,将会被调用,用于清理自定义资源。

    每一个线程模块的清理函数只能被设置一个,多次设置会被覆盖,清理函数是线程独立的,因此不会出现覆盖其他线程处理函数的情况(当然,你也可以故意这样来构造,比如传一个处理函数指针给别的模块,然后那个模块再进行设置)。

    使用流程

    使用流程遵循如下步骤:

    1. 编写框架启动器
    2. 编译链接生成可执行程序
    3. 修改配置文件
    4. 启动程序

    我们逐个步骤进行操作:

    关于如何安装库,可以参考 Github 仓库说明或者之前的文章。

    我们先编写启动器:

    //launcher.c
    
    #include "mln_core.h"
    
    int main(int argc, char *argv[])
    {
        struct mln_core_attr cattr;
        cattr.argc = argc;
        cattr.argv = argv;
        cattr.global_init = NULL;
        cattr.worker_process = NULL;
        return mln_core_init(&cattr);
    }
    

    这里,我们不初始化任何全局变量,也不需要工作进程,因此都置空即可。

    $ cc -o launcher launcher.c -I /usr/local/melon/include/ -L /usr/local/melon/lib/ -lmelon -lpthread
    

    生成名为 launcher 的可执行程序。

    此时,我们的线程尚不能执行,我们需要修改配置文件:

    log_level "none";
    //user "root";
    daemon off;
    core_file_size "unlimited";
    //max_nofile 1024;
    worker_proc 1;
    thread_mode off;
    framework off;
    log_path "/usr/local/melon/logs/melon.log";
    /*
     * Configurations in the 'exec_proc' are the
     * processes which are customized by user.
     *
     * Here is an example to show you how to
     * spawn a program.
     *     keepalive "/tmp/a.out" ["arg1" "arg2" ...]
     * The command in this example is 'keepalive' that
     * indicate master process to supervise this
     * process. If process is killed, master process
     * would restart this program.
     * If you don't want master to restart it, you can
     *     default "/tmp/a.out" ["arg1" "arg2" ...]
     *
     * But you should know that there is another
     * arugment after the last argument you write here.
     * That is the file descriptor which is used to
     * communicate with master process.
     */
    exec_proc {
       // keepalive "/tmp/a";
    }
    thread_exec {
    //    restart "hello" "hello" "world";
    //    default "haha";
    }
    

    上面是默认配置文件,我们要进行如下修改:

    • thread_mode off; -> thread_mode on;
    • framework off; -> framework on;
    • thread_exec 配置块中的两项注释去掉

    这里,需要额外说明一下:

    thread_exec 配置块专门用于模块化线程之用,其内部每一个配置项均为线程模块。

    以 hello 为例:

    restart "hello" "hello" "world";
    

    restart 或者 default 是指令,restart 表示线程退出主函数后,再次启动线程。而 default 则表示一旦退出便不再启动。其后的 hello 字符串就是模块的名称,其余则为模块参数,即入口函数的 argc 和 argv 的部分。而与主线程通信的套接字则不必写在此处,而是线程启动后进入入口函数前自动添加的。

    现在,就来启动程序吧。

    $ ./launcher
    
    Start up worker process No.1
    Start thread 'hello'
    Start thread 'haha'
    02/14/2021 04:07:48 GMT DEBUG: ./src/mln_thread_module.c:haha_main:42: PID:9309 !!!src:hello auto:9736 char:N
    02/14/2021 04:07:49 GMT DEBUG: ./src/mln_thread_module.c:hello_cleanup:53: PID:9309 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    02/14/2021 04:07:49 GMT REPORT: PID:9309 Thread 'hello' return 0.
    02/14/2021 04:07:49 GMT REPORT: PID:9309 Child thread 'hello' exit.
    02/14/2021 04:07:49 GMT REPORT: PID:9309 child thread pthread_join's exit code: 0
    02/14/2021 04:07:49 GMT DEBUG: ./src/mln_thread_module.c:haha_main:42: PID:9309 !!!src:hello auto:9736 char:N
    02/14/2021 04:07:49 GMT DEBUG: ./src/mln_thread_module.c:hello_cleanup:53: PID:9309 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    02/14/2021 04:07:49 GMT REPORT: PID:9309 Thread 'hello' return 0.
    02/14/2021 04:07:49 GMT REPORT: PID:9309 Child thread 'hello' exit.
    02/14/2021 04:07:49 GMT REPORT: PID:9309 child thread pthread_join's exit code: 0
    ...
    

    可以看到,事实上 Melon 中会启动工作进程来拉起其子线程,而工作进程数量由 worker_proc 配置项控制,如果多于一个,则每个工作进程都会拉起一组 haha 和 hello 线程。此外,我们也看到,hello 线程退出后,清理函数被调用。

    线程池

    线程池的使用则与框架基本无关,全部是对封装好的函数进行调用。

    这里我们将配置文件恢复为刚安装好时的默认配置。

    我们来看一个简单的例子:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include "mln_core.h"
    #include "mln_thread_pool.h"
    #include "mln_log.h"
    
    static int main_process_handler(void *data);
    static int child_process_handler(void *data);
    static void free_handler(void *data);
    
    int main(int argc, char *argv[])
    {
        struct mln_core_attr cattr;
        struct mln_thread_pool_attr tpattr;
    
        cattr.argc = argc;
        cattr.argv = argv;
        cattr.global_init = NULL;
        cattr.worker_process = NULL;
        if (mln_core_init(&cattr) < 0) {
            return -1;
        }
    
        tpattr.dataForMain = NULL;
        tpattr.child_process_handler = child_process_handler;
        tpattr.main_process_handler = main_process_handler;
        tpattr.free_handler = free_handler;
        tpattr.condTimeout = 10;
        tpattr.max = 10;
        tpattr.concurrency = 10;
        return mln_thread_pool_run(&tpattr);
    }
    
    static int child_process_handler(void *data)
    {
        mln_log(none, "%s\n", (char *)data);
        return 0;
    }
    
    static int main_process_handler(void *data)
    {
        int n;
        char *text;
    
        while (1) {
            if ((text = (char *)malloc(16)) == NULL) {
                return -1;
            }
            n = snprintf(text, 15, "hello world");
            text[n] = 0;
            mln_thread_pool_addResource(text);
            usleep(1000);
        }
    }
    
    static void free_handler(void *data)
    {
        free(data);
    }
    

    主函数中先对 Melon 框架做了初始化,主要是为了初始化日志,因为配置文件中将不启用框架。然后初始化线程池。

    程序功能比较简单,主线程创建资源然后分发资源,子线程拿到资源并日志输出。

    所有资源分发以及资源竞争全部封装在函数内部,回调函数只需要做功能逻辑处理即可。

    线程池被初始化为最大有 10 个子线程同时处理,若当前某一子线程闲置时间超过 10 秒,则会被回收。

    下面我们生成可执行程序并执行:

    $ cc -o hello hello.c -I /usr/local/melon/include/ -L /usr/local/melon/lib/ -lmelon -lpthread
    $ ./hello
    hello world
    hello world
    hello world
    hello world
    ...
    

    此时,执行

    $ top -d 1 -H -p PID
    

    PID 为 hello 程序的进程 ID,则会看到,偶尔会出现两个线程(如果机器性能较好可能看不到,那么就缩短 usleep 的时间即可)。


    感谢阅读,欢迎大家留言评论。

    再次给出 Melon 的官方 QQ 群:756582294

    Github 仓库: https://github.com/Water-Melon/Melon

    1 条回复    2021-02-16 21:46:52 +08:00
    monkeyNik
        1
    monkeyNik  
    OP
       2021-02-16 21:46:52 +08:00
    各位实在不好意思,之前 QQ 群设置有问题导致无法搜索到,感谢反馈,现在已经可以搜索到了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2216 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 11:59 · PVG 19:59 · LAX 04:59 · JFK 07:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.