C++通过bind,lambda表达式等实现简单的异步函数调用模型

用C++写代码的时候,有很多的场景需要关注一种情况,就是当需要调用一个可能引起长时间阻塞的函数(例如阻塞式的io操作)。大家遇到这种情况,代码的写法一般是这样的:

1.调用需要阻塞以等待操作Func完成的接口

 -> 成功 调用 FcncDone

这样的调用方式会导致线程阻塞,尤其是在主线程中,应该避免这样的调用方式,以免造成用户体验上的卡顿,转化为如下写法:

2.使用异步的方式(创建线程或者使用线程池任务的方式)调用需要阻塞以等待操作Func完成的接口

 -> 等待一小段时间

-> Done ? 调用 FuncDone

-> Pend ? 调用 FuncPend

 ...

 -> (另一部分代码)异步通知或者轮询或者回调的方式,得知Func接口调用完成

-> 调用 FuncDone


第二种方法是程序里面经常使用的,但是写起来并不容易,特别是在c11之前。


好在现在有c11,c14的新特性,bind,auto,decltype等等,可以很方便的写出类似下面结构的代码:

3.使用异步的方式(创建线程或者使用线程池任务的方式)调用需要阻塞以等待操作Func完成的接口

 -> 等待一小段时间(或者不等待)

-> Done(立刻或者未来某个时间) ? 调用 FuncDone

-> Pend ? 调用 FuncPend


从以上的三种代码的结构来看,3的代码更清晰,并且如果能够达到的预期的调用效果,那就很完美了。


下面介绍一个简单的实现这种调用的一个封装,代码如下:

template<typename FuncCallAsync, typename FuncCallDone, typename FuncCallPend>
bool asynccall(FuncCallAsync&& funcasync, int waitmillisec, FuncCallDone&& funcdone, FuncCallPend&& funcpend)
{
	std::shared_ptr<std::atomic_bool> done(new std::atomic_bool(false));
	std::shared_ptr<std::promise<void>> wait(new std::promise<void>());
	std::thread t([](FuncCallAsync&& funcasync, FuncCallDone&& funcdone, std::shared_ptr<std::atomic_bool> done, std::shared_ptr<std::promise<void>> wait)
		{funcasync(); funcdone(); (*done) = true; wait->set_value(); }
		, std::forward<FuncCallAsync>(funcasync), std::forward<FuncCallDone>(funcdone), done, wait);
	t.detach();
	if (!(*done))
	{
		if (waitmillisec > 0)
		{
			wait->get_future().wait_for(std::chrono::milliseconds(waitmillisec));
		}
		else
		{
			std::this_thread::yield();
		}
	}
	if (!(*done))
	{
		std::forward<FuncCallPend>(funcpend)();
	}
	return (*done);
}

使用方式如下:

void testasyncassync()
{
	for (int i = 0; i < 100; ++i)
	{
		{
			VAR* var = new VAR(new int(102));
			bool res2 = asynccall(
				std::bind([](VAR* var)
				{
					std::this_thread::sleep_for(std::chrono::milliseconds(1000));
					*(var->m_ptr) += 32;
				}, var)
				, 0
				, std::([&](const VAR* var)
				{
					std::cout << *var->m_ptr << std::endl;
					delete var;
					std::cout << "done" << std::endl;
				}, var)
				, []()
				{
					std::cout << "pend" << std::endl; 
				});
		}
		std::this_thread::sleep_for(std::chrono::milliseconds(1));
	}
	_getch();
}

接下来分析代码,以及介绍其中存在的坑。

1.可以看到asynccall是一个模板函数,但是其实这个函数的其中三个函数类型的参数都是无参数函数,不过这里依然使用模板是因为在使用中很可能用到了std::bind,所得到的结果不是一个无参函数,而是一个函数对象,当然也可能是lambda表达式。

2.为什么三个函数都要是无参数的?其实在c++新特性中有个变长参数的模版可以使用,如下(类似std::invoke):

template<typename Func, typename... Args>
constexpr auto funccall(Func&& func, Args&&... args)
// noexcept(noexcept(std::forward<Func>(func)(std::forward<Args>(args)...)))
-> typename std::result_of<Func(Args...)>::type
{
	return std::forward<Func>(func)(std::forward<Args>(args)...);
}

在只涉及到一个参数是函数类型参数的时候是可以做到的,但是asynccall有三个函数类参数,如果都可以是任意长度参数的函数,则没有办法区别每个函数的参数具体有多长。而之所以都定义成无参数的函数形式,是因为可以通过std::bind将不同个数参数的函数都转化成无参函数。

3.坑1)使用lambda表达式作为参数调用asynccall时,如果在lambda表达式中需要使用外部变量,请最好使用参数而不要使用捕获,尤其是[=]或者[&]这种所有变量都捕获的使用方式,,如果使用参数,需要使用std::bind绑定参数,使之最终变成无参形式。

4.坑2)出现在funcasync和funcdone中的捕获变量或者参数,请务必保证他们的生命期限,因为这两个函数是异步方式在其他线程中执行的,有可能调用asynccall的语句块已经离开了作用域但是新线程还没有运行完,那么在新线程中的lambda表达式所使用到的变量将失效,引起内存错误。这也就是为什么在3中提到的尽量不要使用[=]和[&]的捕获方式的原因,即你需要知道自己在干嘛,不要被语言提供的便利给摆一道。

5.坑3)其实还是第四点,关于lambda表达式中使用的变量的生命周期问题,一般在调用asynccall之前new,然后在funcdone结尾的地方delete。

6.大家看到这个asynccall很容易想到std::async,其实两个是不同的概念,或者有人想asynccall中的线程创建是不是可以使用std::async代替,其实可以,当然这里也有个坑,在于判断是否执行完成也应该用std::atomic_bool,如果使用std::future,则因为std::future在析构时会等待到ready状态,所以看似异步其实还是同步的。

7.这个函数是最简单的实现,没有考虑线程并发数量的问题,其实自己看代码或者实验可以知道在std::async中是有线程管理策略的(虽然有些让人难以理解),所以如果想要实现更高级的线程管理策略,可以改写asynccall中的第5~7行,使用自己的线程创建管理策略(如线程池)来处理可能存在的线程数量并发问题。






  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值