Libuv应用(二)

本文深入探讨Libuv的TCP/UDP通信、人工通知事件(uv_async_t)以及线程池支持。示例展示了如何使用uv_async_t干预事件循环,以及利用线程池执行计算任务。此外,还简要提及了进程与进程间通信、动态链接库支持和文件监控功能。

三、TCP/UDP

Libuv最常用的应用是TCP和UDP通信。它们是libuv的主要功能,但有很多文章都描述了,本文就不涉及了。

四、人工通知事件

一般情况下,一个线程运行一个事件循环,线程阻塞,等待事件的发生,如果我们想“干预”一下这个事件循环,例如,停止它,向某个socket通道发点信息等,就需要人工“制造”一个消息触发一次,此时就需要用到uv_async_t。由于事件循环的线程处于阻塞状况,所以发消息的函数应该在另一个线程。下面是一个示例。

int test_async()
{
	int res;
	uv_loop_t* loop = uv_default_loop();

	auto async_cb = [](uv_async_t* handle)
	{
		printf("stop loop now\n");
		uv_stop(uv_handle_get_loop((uv_handle_t *)handle));
	};
	uv_async_t hasync;
	if (res = uv_async_init(loop, &hasync, async_cb); res)
	{
		fprintf(stderr, "uv_async_init error %s\n", uv_strerror(res));
		return -1;
	}
	std::thread otherthread([](uv_async_t* p) {
			std::this_thread::sleep_for(std::chrono::seconds(5));
			uv_async_send(p);
		}, &hasync); 

	uv_timer_t htimer;
	if (res = uv_timer_init(loop, &htimer); res)
	{
		fprintf(stderr, "uv_timer_init error %s\n", uv_strerror(res));
		return -2;
	}
	auto timer_cb = [](uv_timer_t* handle)
	{
		uint64_t timestamp = uv_hrtime();
		printf("timer time out at [%I64u ms]\n",(timestamp / 1000000) % 100000);
	};
	res = uv_timer_start(&htimer, timer_cb, 0, 1000);
	res = uv_run(loop, UV_RUN_DEFAULT);
	uv_close((uv_handle_t*)& hasync, NULL);
	uv_close((uv_handle_t*)& htimer, NULL);  //uv_close must be called to avoid memory leakage

	otherthread.join();

	printf("end of loop\n");
	return res;
}

此例中,有一个定时器,每秒触发一次,无限循环下去,但设置了一个人工事件,在另一个线程5秒之后通知这个线程触发,在事件中停止事件循环,整个程序结束。屏幕输出如下所示。

需要注意的是,除创建线程的那个lamda函数在另一线程中运行外,所有代码是在主线程中运行的。

五、线程、锁与任务(线程)池支持

libuv提供了对线程相关操作的支持,不过,如果采用C++ 11以上标准的话,已经对线程操作(包括锁)有相当好的支持,可以不必使用,这里就不涉及了。但对纯C风格,倒是可以考虑采用libuv中的函数。

线程池对于高并发程序是必不可少的。Libuv中的线程池是一个全局资源,即一个进程只有一个线程池,程序运行后已经被初始化好,线程数量的缺省值是4,最大是 1024,它们都在threadpool.c中定义,如果需要修改线程数,方法是设置环境变量UV_THREADPOOL_SIZE的值,在init_threads()中有如下代码:

  nthreads = ARRAY_SIZE(default_threads); //缺省值4
  val = getenv("UV_THREADPOOL_SIZE");
  if (val != NULL)  nthreads = atoi(val);

如果感觉设置环境变量比较麻烦,可自行修改上面的代码,从某个全局变量(函数)中读取,然后赋给nthreads就可以了。

线程池设置好之后,将计算任务分配给某个线程是通过uv_work_t来完成的。如下例所示。主线程运行事件循环,并设置了一个500ms触发一次的定时器,触发6次后停止,定时器例程中,创建一个新任务(随机延时0-1000ms),然后将任务交由线程池运行(uv_queue_work),任务完成后,释放资源(uv_work_t)。

int test_threadpool()
{
	int res;

	uv_timer_t timer;
	uv_timer_init(uv_default_loop(), &timer);
	int timercount = 0;
	timer.data = &timercount;
	auto timer_cb = [](uv_timer_t* timer)
	{
		uint64_t timestamp = uv_hrtime();
		int* p = (int*)timer->data;
		if (*p > 5)
		{
			uv_timer_stop(timer);
			return;
		}
		else (*p)++;

		int taskid = *p;
		printf("New Task[%d] at: %I64u ms\n",taskid, (timestamp / 1000000) % 100000);

		uv_work_t* worker = (uv_work_t*)malloc(sizeof(uv_work_t));
		memcpy(&(worker->data), &taskid, sizeof(int));
		uv_queue_work(uv_handle_get_loop((uv_handle_t *)timer), worker,
		//on_work, exec on different threads
		[](uv_work_t * worker)
		{
			std::thread::id tid = std::this_thread::get_id();
			size_t th = std::hash<std::thread::id>{}(tid);
			int elapse = th % 1000;
			int taskid = (int)worker->data;
			printf("Task[%d] works on  thread[%u] for %u milliseconds...\n", taskid, th, elapse);
			std::this_thread::sleep_for(std::chrono::milliseconds(elapse));
		},
		//on_after_work, exec on the main thread
		[](uv_work_t * worker, int status)
		{
			int taskid = (int)worker->data;
			printf("Main Thread :Task[%u] Done\n", taskid);
			free(worker);
		}
		);
	};

	uv_timer_start(&timer, timer_cb, 500, 500);
	res = uv_run(uv_default_loop(), UV_RUN_DEFAULT);
	uv_close((uv_handle_t*)& timer, NULL);

	printf("end of loop\n");
	return res;
}

程序运行的输出屏幕如下图,可以看出,确实有4个线程在运行任务,第5,6个任务线程与第1,2个任务的线程相同。如果线程池有空闲的线程,任务以队列的方式运行,我们不能设定任务的优先级和选择特定的线程。

完成任务分配主要的函数是uv_queue_work,而没有uv_work_init,因此uv_work_t分配后不必初始化,uv_queue_work参数中两个回调函数,一个是任务函数(计算任务本体,在各线程中运行),另一个是任务完成后的“收尾”函数(完成资源释放等收尾任务,它在事件循环线程中运行,使原本处于阻塞的事件循环线程也有机会响应计算结果的处理)。注意uv_work_t->data的赋值,上例中直接将taskid赋给data不是常规的做法,仅为简单起见。

六、进程与进程间通信支持

libuv对进程的支持,包括两方面内容,一是进程的创建,libuv主要通过uv_process_t、uv_process_options_t结构以及uv_spawn函数来完成;二是进程间的通信,通过socket或pipe完成。进程间通信也可以通过内存完成,同时需要进程间锁,这是libuv不支持的。

以下是一个启动其他进程的例子,uv_spawn参数比较多,也比较复杂,各平台不尽相同,具体场景还需查阅相关文档,这里仅是简单的一个示例,其中另一个进程为e:\demo.exe,就是输出一段文字,通过重定向stdout,将其输出到out.log中。

int test_process()
{
	int res=0;
	int fd = open("out.log", O_WRONLY|O_CREAT|O_TEXT,S_IWRITE);
	if (fd == -1) {
		printf("\n file open error\n");   return -1;
	}
	auto exit_cb = [](uv_process_t* pp, int64_t exit_status, int term_signal) {
		printf("process[%d] exits[%lld]:%d", pp->pid, exit_status, term_signal);
		uv_close((uv_handle_t*)pp, NULL);
	};
	uv_stdio_container_t stdio[3];
	stdio[0].flags = UV_IGNORE;
	stdio[1].flags = UV_INHERIT_FD;
	stdio[1].data.fd = fd;
	stdio[2].flags = UV_IGNORE;

	const char* args[3] = { "demo.exe" , nullptr, nullptr };
	uv_process_t  hprocess;
	uv_process_options_t options = {
		exit_cb,
		"demo.exe",
		(char **)args,
		nullptr,
		"e\\",
		0,
		3,
		stdio,  
		0,
		0,
	};
	res = uv_spawn(uv_default_loop(), &hprocess, &options);
	if(res!=0) printf("subprocess error[0x%X]:%s\n", res, uv_strerror(res));
	else  printf("subprocess starts\n");

	res = uv_run(uv_default_loop(), UV_RUN_DEFAULT);

	close(fd);

	printf("end of loop\n");
	return res;
}

示例中,stdio[0],[1],[2]分别代表stdin,stdout和stderr,事先打开重定向的文档out.log,然后将文件句柄传给stdio[1],则demo.exe的输出就保存到了文件out.log中。

七、对动态链接库的支持

Libuv提供了的对动态链接库的支持,例如用于动态载入协议解析器。加载动态链接库应该是比较简单的,大值的过程如下:

  1. 调用uv_dlopen(动态库文件名[in], uv_lib_t 句柄[out]),得到uv_lib_t类型的动态库句柄;
  2. 调用uv_dlsym(动态库句柄[in], 函数名[in], 指向函数的指针[out]),得到动态库中某一指向函数的指针(需强行转换,因此需要知道函数的类型声明);
  3. 按函数声明传递参数,调用动态库中该函数。
  4. 完成后,调用uv_dlclose(动态库句柄[in])关闭该动态库。

以上调用发生错误时,可通过uv_dlerror(动态库句柄[in])得到最近一次操作的错误描述。

八、对文件的监控

uv_fs_event_t和uv_fs_poll_t结构都是用来监控文件(路径)变化的,二者之间的区别,文档中已比较明确:

uv_fs_event_t使用每个平台上最佳的解决方案,即它是一种跨平台的抽象,在不同的平台实现各不相同。(FS Event handles allow the user to monitor a given path for changes, for example, if the file was renamed or there was a generic change in it. This handle uses the best backend for the job on each platform.);uv_fs_poll_t使用了stat来检测文件是否发生了更改,这样它的适用范围就广些,但它是轮询方式的,效率上要差些。(FS Poll handles allow the user to monitor a given path for changes. Unlike uv_fs_event_t, fs poll handles use stat to detect when a file has changed so they can work on file systems where fs event handles can’t)。因而,能用uv_fs_event_t的场合先用它。

它们的应用同样比较简单,大致的过程如下(标准的libuv过程):

  1. 调用uv_fs_event_init/ uv_fs_poll_init进行初始化
  2. 调用uv_fs_event_start / uv_fs_poll_start进行监控选项配置,并设置回调函数,如果监控对象有变化,则回调函数被调用。期间可用uv_fs_event_stop/uv_fs_poll_stop停止监控
  3. 最后调用uv_close关闭句柄

其实,对文件系统的修改还是比较复杂的,各操作系统还不尽相同,因而libuv仅提供了类似“hook”的功能,告述使用者文件系统发生变化了,至于发生了什么,libuv的功能就显得比较粗糙了(这并不是libuv的核心功能,所以比较简单)。例如采用uv_fs_poll_t方法时,回调函数只接受一个stat结构,如果同时有多个文件发生变化,则只有第一个文件被传入;采用uv_fs_event_t方法时,windows平台对文件的修改有时是分步的,因此会产生几次回调,而libuv所提供的信息过于简单,需要采用额外的手段才能分析出到底发生了什么。

总结

与libevent,boost.asio的总体对比

Libevent与Libuv都是一个异步网络操作C库,不提供同步功能,在linux下,二者就核心功能来讲基本相同,但libuv的辅助函数要丰富得多,此时Libevent只能用标准库或第三方库来弥补,但Libevent的buffer功能比较强大,libuv相对弱很多,Libevent也附带提供了一个http解析功能,很少一些代码就可以构建出一个简单的http server或client,这是libuv所没有的。boost.asio是一个IO操作C++库,既有同步操作,也有异步操作,由于有Boost其他库的支撑,它没有提供过多的辅助功能,但核心功能应该是最强大的,不过由于采用C++模板编写, Boost.asio的源码看起来相当困难,而Libevent与Libuv的源码努力一下,还能看明白一些,template+lamda的大量应用,使得查错也比较困难(相对C),因此学习成本很高。传说asio会进入C++标准,目前已发布的C++20中还未看到,应该是争议较大,笔者感觉主要是学习成本相对太高了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值