借助 C++ 进行 Windows 开发

回到可恢复函数构筑的未来

Kenny Kerr

 

Kenny Kerr我结束我的最后一列 (msdn.microsoft.com/magazine/jj618294) 通过突出显示一些可能的改进,到 C + + 11 期货和承诺,将从很大程度上学术和简单,实际和有用建设的高效率和可组合的异步系统对其进行转换。在很大程度上这被鼓舞 Niklas Gustafsson 和阿图尔 · Laksberg 从 Visual c + + 团队的工作。

作为未来的期货的代表性,我给了这些线沿线的示例:

 

int main()
{
  uint8 b[1024];
  auto f = storage_read(b, sizeof(b), 0).then([&]()
  {
    return storage_write(b, sizeof(b), 1024);
  });
  f.wait();
}

Storage_read 和 storage_write 函数返回一个代表可能在未来某个时刻完成各自的 I/O 操作的未来。 这些函数模型 1 KB 页与一些存储子系统。 整个程序从存储中的第一页读取到缓冲区,然后将它复制回存储的第二页。 此示例的新奇是使用假设"然后"方法添加到未来类,允许读取和写入操作,然后可以对其无缝地等待为单个逻辑 I/O 操作组成。

这是一个巨大的改进,对世界的堆栈翻录我所述我最后一列,但本身就是我我 8 月 2012年列,c +"轻型合作多任务处理与 +"中描述的语言支持一个类似 coroutine 的设施仍不完全实现的乌托邦的梦想 (msdn.microsoft.com/magazine/jj553509)。 在该列中我成功地演示了一些戏剧性宏诡计如何才能实现这种设施 — — 但不是能没有重大的缺点,主要涉及到无法使用的本地变量。 这个月我想分享一些想法如何,这可能会实现在 c + + 语言本身。

我一定开始本系列文章探索替代技术可以实现并发与实际的解决办法,因为现实情况是我们需要的解决方案,今天的工作。 我们做的不过,需要,展望未来,迫使 c + + 社区转发,要求更多更自然和更富有成效的方式写入 I/O 密集型应用程序的支持。 当然写作高度可扩展的系统不应该的 JavaScript 和 C# 程序员和足够的意志力与罕见的 c + + 程序员的专属管辖范围。 另外,请记住这不只是方便和编程语法和样式的优雅。 有多个活动在任何给定时间的 I/O 请求的能力有可能显著地提高性能。 存储和网络驱动程序旨在规模以及在飞行中有更多的 I/O 请求。 在存储驱动程序的情况下请求可以组合以提高硬件缓冲并降低寻道时间。 在网络驱动程序的情况下更多的请求是指较大的网络数据包、 优化滑动窗口操作及更多。

我要切换齿轮略来说明如何快速的复杂性抬头。 而不从一个存储设备只需读取和写入和,如何提供文件的内容通过网络连接吗? 和以前一样,我从开始同步的方法,并从那里工作。 计算机可能从根本上是异步的但我们是凡人当然不是。 不了解你,但从来没有过着很多的出发点。 请考虑下面的类:

class file { uint32 read(void * b, uint32 s); };
class net { void write(void * b, uint32 s); };

使用你的想象力来填写其他。 我只被需要允许从一些文件读取的字节数的一定数量的文件类。 我会进一步认为文件对象将跟踪的偏移量。 同样,净类可能模型通过其滑动窗口的实现一定隐藏的调用方通过 TCP 处理数据偏移位置的 TCP 流。 由于各种原因,或许与相关的缓存或争用,文件阅读方法可能不会始终返回实际请求的字节数。 只有它将,然而,返回零时已到达文件的末尾。 净 write 方法是工作的更简单,因为 TCP 实现中,根据设计,幸好不庞大,以保持这简单的调用方。 这是基本的假想方案,但相当代表性的 OS I/O。 我现在可以编写以下简单的程序:

int main()
{
  file f = ...; net n = ...; uint8 b[4096];
  while (auto actual = f.read(b, sizeof(b)))
  {
    n.write(b, actual);
  }
}

鉴于 10 KB 的文件,你可以想象下面的循环耗尽之前的事件序列:

read 4096 bytes -> write 4096 bytes ->
read 4096 bytes -> write 4096 bytes ->
read 2048 bytes -> write 2048 bytes ->
read 0 bytes

像我的最后一列中的同步示例,不难弄清楚怎么在这里,因为 c + + 的顺序存取特性。 交换机成为异步组成是有点困难。 第一步是要转换的文件和净类返回期货:

class file { future<uint32> read(void * b, uint32 s); };
class net { future<void> write(void * b, uint32 s); };

这是比较容易的部分。 要充分利用这些方法中的任何不同步的主函数重写的几个挑战。 它不再足够使用未来的假设"然后"方法,因为我不再只在处理顺序组成。 是的真的写如下一读,但只,如果实际读取重新定位的东西。 若要使问题复杂化进一步,读也遵循写在所有情况下。 你可能会忍不住要想到封锁,但这一概念涵盖组成的状态和行为和不行为和其他行为的组成。

我可以首先创建关闭仅为读取和写入操作:

auto read = [&]() { return f.read(b, sizeof(b)); };
auto write = [&](uint32 actual) { n.write(b, actual); };

当然,这相当不能工作,因为未来的然后方法,不知道什么要传递给写函数:

read().then(write);

要解决这个问题,我需要某种将允许期货转发状态的公约 》。 一个明显的选择 (或许) 是转发本身的未来。 然后方法然后期望将采取适当的类型,允许我写这未来参数的表达式:

auto read = [&]() { return f.read(b, sizeof(b)); };
auto write = [&](future<uint32> previous) { n.write(b, 
  previous.get()); };
read().then(write);

这工作原理,以及我甚至可能希望改进可组合性进一步通过定义,然后方法预期的表达式还应该返回一个未来。 然而,这个问题依然是如何表达条件循环。 最终,这证明是更简单,相反,作为 do...while 循环重新考虑原始循环,因为这是迭代的方式表达更容易。 有条件地链接期货和使基于 <bool> 的未来的结果结束迭代组成可以然后制定要以异步方式,模仿这种模式的 do_while 算法 值,例如:

future<void> do_while(function<future<bool>()> body)
{
  auto done = make_shared<promise<void>>();
  iteration(body, done);
  return done->get_future();  
}

Do_while 函数首先创建一个引用计数的承诺,其最终的未来信号循环的终止。 这被传递到迭代功能和代表循环的主体的功能:

void iteration(function<future<bool>()> body, 
  shared_ptr<promise<void>> done)
{
  body().then([=](future<bool> previous)
  {
    if (previous.get()) { iteration(body, done); }
    else { done->set_value(); }
  });
}

此迭代函数是心脏的 do_while 算法,提供从一个调用链接到下一个,以及爆发和信号完成工作的能力。 虽然看上去可能递归,但请记住整点是单独从堆栈的异步操作,因此在循环并不实际增长堆栈。 使用 do_while 算法是相对容易,并且我现在可以编写中显示的程序图 1

图 1 使用 do_while 算法

int main()
{
  file f = ...; net n = ...; uint8 b[4096];
  auto loop = do_while([&]()
  {
    return f.read(b, sizeof(b)).then([&](future<uint32> previous)
    {
      return n.write(b, previous.get());
    }).then([&]()
    {
      promise<bool> p;
      p.set_value(!f.eof);
      return p.get_future();
    });
  });
  loop.wait();
}

Do_while 函数自然返回一个未来,和在这种情况下它等待后,但这很容易本来可以避免通过将主要函数的局部变量存储与 shared_ptrs 堆上。 内传递给 do_while 函数的 lambda 表达式,读取的操作开始,其次是写操作。 为使此示例简单,我假设这写将立即返回,如果它曾告诉写零字节。 当写入操作完成后时,我检查该文件的文件结束状态,并返回一个提供循环条件值的未来。 这可确保将重复循环的主体,直到用尽了该文件的内容。

尽管此代码不是特别令人讨厌 — — 事实上,是可以说比堆栈翻录很干净,并 — — 有点支持的语言会走很长的路。 Niklas Gustafsson 已建议这种设计并称之为"可恢复功能"。期货和承诺的基础上改进拟议并添加少许句法糖,我可以编写一个可恢复的函数来封装的令人惊讶的复杂的异步操作,如下所示:

future<void> file_to_net(shared_ptr<file> f, 
  shared_ptr<net> n) resumable
{
  uint8 b[4096];
  while (auto actual = await f->read(b, sizeof(b)))
  {
    await n->write(b, actual);
  }
}

这种设计的好处就是,代码具有相似,初始同步版本,并且属于什么我在寻找,毕竟。 请注意"可恢复"上下文关键字之后,该函数的参数列表。 这是类似于我我 8 月 2012年列中所描述的假想的"异步"关键字。 不像我在该列中说明的事情,但是,这将由执行编译器本身。 因此将无并发症和我面对宏执行的限制。 您可以使用 switch 语句以及本地变量 — — 和构造函数和析构函数将按预期工作 — — 但您功能现在可以暂停和继续以类似于我原型与宏的方式。 不但如此,但你将摆脱捕获局部变量只是为了让他们超出范围,一个常见的错误时使用 lambda 表达式的陷阱。 编译器会照顾为堆上的可恢复函数内的本地变量提供的存储。

在前面的示例还会通知"等待"关键字之前,读取和编写方法调用。 此关键字定义的恢复点,并期望未来类似的对象,它可用于确定是否要暂停和恢复以后是否只是继续执行,如果碰巧同步完成异步操作导致的表达式。 显然,为了实现最佳的性能,我需要处理异步操作的完成同步,也许由于高速缓存太常见方案或快速故障情形。

请注意我说,等待关键字预计未来类似的对象。 严格地说,没有任何理由,它需要一个实际的未来对象。 它只需要提供必要的行为,以支持异步完成和信号转导的检测。 这是今天的模板工作的方式类似。 这类似于未来的对象将需要支持我的最后一列所示的然后方法以及现有的 get 方法。 为了提高性能,结果立即可用的情况下,拟议的 try_get 和 is_done 方法也会很有用。 当然,编译器可以优化基于这种方法的可用性。

这并不是像有些牵强,也许看起来。 C# 已有形式的异步方法,可恢复功能的道德等效的几乎相同设施。 它甚至提供了等待的关键字,我已经说明了在相同的方式工作。 我的希望是 c + + 社区会拥抱可恢复的功能,或者类似,使到我们所有能够编写高效、 可组合的异步系统自然和轻松。

可恢复功能的详细分析,包括看看如何可能实现它们,请阅读 Niklas Gustafsson 纸张,"可恢复功能,"在 bit.ly/zvPr0a

Kenny Kerr 是一位热衷于本机 Windows 开发的软件专家。您可以通过 kennykerr.ca 与他联系。

衷心感谢以下技术专家对本文的审阅:Artur Laksberg