聊聊使用 Libuv 实现的 Watchdog,你学会了吗?

开发 前端
本文主要是介绍一下使用 Libuv 实现的 watchdog,背景主要是因为 Node.js 是单线程的,一旦主线程繁忙或者陷入死循环,那么整个进程都无法工作了。

watchdog 的概念大概是定时去做一些事情,具体的概念可以网上搜索,本文主要是介绍一下使用 Libuv 实现的 watchdog,背景主要是因为 Node.js 是单线程的,一旦主线程繁忙或者陷入死循环,那么整个进程都无法工作了。

虽然 Node.js 在 JS 层实现了子线程模块,但是因为子线程持有单独的 V8 Isolate 和 loop,所以存在很多限制。比如我们希望在主线程繁忙时还能收集到主线程的一些数据(进程 / V8 内存、CPU 使用情况等)。这时候就需要使用 addon 去实现,addon 虽然是 C、C++ 等语言写的,但是依然跑在主线程里。所以我们需要使用底层提供的原生子线程加上 addon 实现我们的需求。本文正是基于这种场景的 watchdog。

首先看一下整体的类结构。

下面看一下每个类的作用和实现。

1.TaskManager

因为存在主线程和子线程,线程间会互相向对方提交任务,这就涉及到多线程互斥访问的问题。TaskManager 是负责管理任务的类,是线程安全的。

class TaskManager {
public:
TaskManager() {
uv_mutex_init(&tasks_mutex);
};
void add_task(task_cb cb, void * data) {
struct task* t = new task;
t->cb = cb;
t->data = data;
uv_mutex_lock(&tasks_mutex);
tasks.push_back(t);
uv_mutex_unlock(&tasks_mutex);
};
void handle_task() {
std::vector<struct task*> task_list;
uv_mutex_lock(&tasks_mutex);
task_list.swap(tasks);
uv_mutex_unlock(&tasks_mutex);
std::vector<struct task*>::iterator iter;
for(iter = task_list.begin(); task_list.end() != iter;)
{
struct task* t = (*iter);
t->cb(t->data);
iter = task_list.erase(iter);
delete t;
}
};
private:
std::vector<struct task*> tasks;
uv_mutex_t tasks_mutex;
};

实现很简单,主要是使用 uv_mutex_t 解决互斥问题,提供 add_task 和 handle_task 两个方法。add_task 是追加任务,handle_task 是处理任务。handle_task 具体的调用时机由 TaskManager 的持有者决定。

2.WatchDog

WatchDog 是对 Libuv 定时器的封装,核心数据结构是 watch_dog_ctx。

typedef void (*watch_dog_cb)(void* ctx);
struct watch_dog_ctx{

void * data; // 上下文
watch_dog_cb work; // 任务函数
int poll_interval; // 定时时间
};

watch_dog_ctx 是对一个定时任务的封装。watch_dog_ctx 的任务会在子线程中定时被执行,这就解决了主线程陷入繁忙时无法工作的问题。

NodeWatchDog::WatchDog::WatchDog(uv_loop_t* loop, struct watch_dog_ctx* ctx) {
this->ctx = ctx;
uv_timer_init(loop, &timer);
timer.data = this;
is_stop = false;
}

void NodeWatchDog::WatchDog::start() {
if (is_stop) {
return;
}
uv_timer_start(
&timer,
[](uv_timer_t* timer) {
NodeWatchDog::WatchDog* watch_dog = (NodeWatchDog::WatchDog*)timer->data;
struct watch_dog_ctx* ctx = watch_dog->get_ctx();
ctx->work(ctx);
},
ctx->poll_interval,
ctx->poll_interval
);
};

void NodeWatchDog::WatchDog::stop() {
is_stop = true;
uv_timer_stop(&timer);
}

3.WatchDogWorker

WatchDogWorker 是负责管理子线程和 watchdog 的类。首先看一下定义。

class WatchDogWorker {
public:
WatchDogWorker();
~WatchDogWorker() {};
void start();
void stop();
void add_watchdog(struct watch_dog_ctx* watchdog);
void add_task(task_cb cb, void * data);
void handle_task();
uv_loop_t* get_event_loop() {
return &loop;
}
uv_sem_t * get_thread_sem() {
return &sem;
}
private:
uv_loop_t loop;
uv_thread_t tid;
uv_sem_t sem;
TaskManager task_manager;
uv_async_t notify_async;
std::vector<WatchDog*> watch_dogs;
};

1)add_watchdog 用于新增 watchdog。

void NodeWatchDog::WatchDogWorker::add_watchdog(struct watch_dog_ctx* watchdog_ctx) {
NodeWatchDog::WatchDog *watchdog = new NodeWatchDog::WatchDog(&loop, watchdog_ctx);
watch_dogs.push_back(watchdog);
}

2)start 函数用于创建子线程和启动 watchdog。

void NodeWatchDog::WatchDogWorker::start() {
std::vector<NodeWatchDog::WatchDog*>::iterator iter;
for(iter = watch_dogs.begin(); watch_dogs.end() != iter; iter++)
{
(*iter)->start();
}
int r = uv_thread_create(&tid, [](void *data) {
NodeWatchDog::WatchDogWorker * worker = (NodeWatchDog::WatchDogWorker *)data;
uv_sem_post((uv_sem_t*)worker->get_thread_sem());
uv_loop_t * loop = worker->get_event_loop();
uv_run(loop, UV_RUN_DEFAULT);
uv_loop_close(loop);
}, (void *)this);
if (!r) {
uv_sem_wait(&sem);
}
uv_sem_destroy(&sem);
}

启动 watchdog 时会往子线程的 loop 里插入一个定时器。然后创建子线程,在子线程里开启一个新的 loop。在这个 loop 里就会不断执行 watchdog 的任务。

3)add_task 用于其他线程外另一个线程插入一个任务。下面是任务的定义。

typedef void (*task_cb)(void *data);
struct task{
task()
{
cb = nullptr;
data = nullptr;
}
task_cb cb;
void *data;
};

定义很简单,一个工作函数和对应的上下文。接着看 add_task。

void NodeWatchDog::WatchDogWorker::add_task(task_cb cb, void * data) {
task_manager.add_task(cb, data);
uv_async_send(&notify_async);
}

add_task 直接调用 task_manager 的 add_task 函数插入任务,因为它保证了线程安全。接着通过 Libuv 提供的 async 线程间通信机制通知另一个线程有新任务。如果是在 Node.js 里的话,还需要通过另一种方式进行通知,这里就不具体展开。

此处,我们就实现了在子线程里定时执行任务,并实现主线程和子线程互相提交任务的能力。最终再实现一个 WatchDogManager 用于管理多个 worker。

4.WatchDogManager

class WatchDogManager
{
public:
WatchDogManager(uv_loop_t *loop);
~WatchDogManager() {};
void start();
void stop();

void add_task(task_cb cb, void *data);
void handle_task();
void add_worker(WatchDogWorker* worker);
WatchDogWorker* get_worker(int index) { return workers[index]; };
private:
uv_async_t notify_async;
TaskManager task_manager;
std::vector<WatchDogWorker*> workers;
};

通过 WatchDogManager 的类定义,我们大概就知道它的功能。首先看一下 start 方法。

void NodeWatchDog::WatchDogManager::start(){
std::vector<NodeWatchDog::WatchDogWorker*>::iterator iter;
for(iter = workers.begin(); workers.end() != iter; iter++)
{
(*iter)->start();
}
}

start 函数就是启动多个 worker,刚才已经介绍过,worker 会创建一个子线程定时执行任务。其他方法就没有太多逻辑,就不一一介绍。

5.使用

接下来看看使用方式。

#include "src/watch_dog_manager.h"
#include "uv.h"
#include "stdio.h"

using namespace NodeWatchDog;int main() {
setbuf(stdout, NULL);
uv_loop_t loop;
uv_loop_init(&loop);
WatchDogWorker *worker = new WatchDogWorker();
struct watch_dog_ctx* ctx = new watch_dog_ctx;
ctx->work = [](void *ctx) {
printf("worker task execute in thread id => %ld\n", (long)pthread_self());
};
ctx->poll_interval = 1000;
worker->add_watchdog(ctx);
worker->start();
uv_idle_t idle;
uv_idle_init(&loop, &idle);
uv_idle_start(&idle, [](uv_idle_t *) {});
printf("main thread id => %ld\n", (long)pthread_self());
uv_run(&loop, UV_RUN_DEFAULT);
delete worker;
delete ctx;
return 0;
}

新建一个 watchdog 和 worker,并把 watchdog 插入到 worker 中,最后启动 worker。同时主线程也进入自己的 loop。执行输出如下。

main thread id => 4338490880
worker task execute in thread id => 123145480077312
worker task execute in thread id => 123145480077312

我们看到,printf 分别执行在不同的线程中。接下来再看一下复杂的场景。

#include "src/watch_dog_manager.h"
#include "uv.h"
#include "stdio.h"

using namespace NodeWatchDog;
int main() {
setbuf(stdout, NULL);
uv_loop_t loop;
uv_loop_init(&loop);
WatchDogManager *manager = new WatchDogManager(&loop);
WatchDogWorker *worker = new WatchDogWorker();
struct watch_dog_ctx* ctx = new watch_dog_ctx;
ctx->data = (void *)manager;
ctx->work = [](void *ctx) {
struct watch_dog_ctx* watchdog_ctx = (struct watch_dog_ctx*)ctx;
WatchDogManager* manager = (WatchDogManager*)watchdog_ctx->data;
// 提交任务给主线程
manager->add_task([](void *data) {
printf("manager task execute in thread id => %ld\n", (long)pthread_self());
WatchDogManager* manager = (WatchDogManager*)data;
// 提交任务给某个子线程
manager->get_worker(0)->add_task([](void *data) {
printf("worker task execute in thread id => %ld\n", (long)pthread_self());
}, nullptr);
}, manager);
};
ctx->poll_interval = 1000;
worker->add_watchdog(ctx);
manager->add_worker(worker);
manager->start();
uv_idle_t idle;
uv_idle_init(&loop, &idle);
uv_idle_start(&idle, [](uv_idle_t *) {
//
});
printf("main thread id => %ld\n", (long)pthread_self());
uv_run(&loop, UV_RUN_DEFAULT);
delete manager;
delete worker;
delete ctx;
return 0;
}

上面的例子中,当在子线程中执行回调时,通过 manager->add_task 往主线程提及一个任务。然后在主线程执行这个任务时,首先输出当前线程 id,再通过 manager->get_worker(0)->add_task 给子线程提交一个任务。比如我们希望定时收集主线程的 CPU 数据,那么就可以给子线程插入一个 watchdog,然后在 watchdog 回调里给主线程提交一个任务,当主线程执行这个任务的时候,我们就可以拿到主线程的 CPU 数据。当然还有更复杂的场景,比如获取 CPU Profile 时,主线程和子线程会进行多次交互。最后再看一个多个 worker 线程的例子。

 WatchDogManager *manager = new WatchDogManager(&loop);
WatchDogWorker *worker1 = new WatchDogWorker();
WatchDogWorker *worker2 = new WatchDogWorker();
struct watch_dog_ctx* ctx = new watch_dog_ctx;
ctx->work = [](void *ctx) {
printf("manager task execute in thread id => %ld\n", (long)pthread_self());
};
ctx->poll_interval = 1000;
worker1->add_watchdog(ctx);
worker2->add_watchdog(ctx);
manager->add_worker(worker1);
manager->add_worker(worker2);
manager->start();

以上代码输出如下。

main thread id => 4469550592
manager task execute in thread id => 123145483321344
manager task execute in thread id => 123145491722240
manager task execute in thread id => 123145483321344
manager task execute in thread id => 123145491722240

我们看到存在多个子线程,并且成功执行了任务。

后记:单线程使得编码变得简单,但是也导致了一些限制,这时候我们就需要使用额外的子线程来解决单线程存在的限制。引入多线程后,就需要解决多线程互斥和通信问题。以上代码只是介绍了一个大致的思路,还有地方需要完善,如果有想法的同学也可以交流。

仓库:https://github.com/theanarkh/uv-watchdog

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2024-08-19 10:24:14

2023-03-26 22:02:53

APMPR监控

2022-12-14 08:31:43

#error编译命令

2023-01-29 08:08:34

并发库conc通用库

2023-07-10 08:36:21

工具pptword

2022-12-26 07:48:04

敏捷项目

2024-03-05 10:09:16

restfulHTTPAPI

2024-06-12 08:36:25

2024-02-02 11:03:11

React数据Ref

2024-04-29 06:55:34

RustMIDI应用程序

2023-06-05 08:36:04

SQL函数RANK()

2023-03-07 07:50:15

Transactio事务代码

2024-11-08 08:56:01

2022-12-27 08:45:00

绘制菜单符号

2024-03-04 07:41:18

SpringAOPOOP​

2022-09-26 08:49:11

Java架构CPU

2022-07-11 09:00:37

依赖配置文件Mybati

2022-12-08 10:49:43

2022-04-13 09:01:45

SASSCSS处理器

2022-06-16 07:50:35

数据结构链表
点赞
收藏

51CTO技术栈公众号