借助 C++ 进行 Windows 开发

与 c + + 的轻量级合作多任务处理

Kenny Kerr

 

Kenny Kerr如果你工作的公司已在其中一个编码将消灭整个雨林它曾有要打印的标准文档,但您将更好地停止现在读。机会是,我要显示您将违反上述史诗的圣牛的很多。我要告诉你们的最初开发,让我写完全异步代码有效,而无需复杂的状态机器的特殊技术。

除非你的名字是高德纳,它可能是你写的东西将类似于的任何代码都已经完成。事实上,我可以找到我在此处描述的技术的最古老的帐户是提及 Knuth 自己,而最近是由一位来自英国由西蒙 · 泰瑟姆,受欢迎的腻子终端仿真器闻名的名称。不过,套用在最近的 Oracle v 法官。谷歌纠纷,"你不能申请专利的 for 循环。"尽管如此,我们向我们的同伴在计算机编程为推动工艺领域所有重债穷国。

跳水,并描述了我的技术是什么以及它如何工作之前,我需要快速导流现状,它会给你更多角度看是什么来的希望。在"编译器:原则、 方法和工具,第二版"(普伦蒂斯厅 2006年) 由阿尔弗雷德 · V。阿霍莫尼卡 ·林、 Ravi Sethi 和杰弗里 d。尔曼 — — 更俗称龙书 — — 作者总结作为一个程序,可以读取一种语言中的程序,并将它翻译成另一种语言中的等效程序的编译器的目的。如果你要问什么语言的 c + + 设计器比亚内 Stroustrup 他用于编写 c + +,他会告诉你这是 c + +。这是什么意思就是他写预读取 c + + 和制作 c,C 标准的 C 编译器可能会再进一步转化为一些机器语言。如果你看起来不够密切,你可以看到这个想法在很多地方的变种。C# 编译器,例如,转化实现一个迭代器的常规类看似神奇产量的关键字。去年年底,我注意到,Visual c + + 编译器首先翻译部分的 C + + / CX 语法到标准 c + + 前进一步对其进行编译。这可能会有变化,但问题是,编译器从根本上是关于翻译。

有时,在使用特定语言时,您可能会认为一项功能就是可取的但,非常自然的语言禁止您实施它。因为语言的设计适合于不同的技术,由于其丰富的语法和元设施的表达,这也很少发生在 c + + 程序。然而,这事实上并未发生在我身上。

我花了我大部分的时间我从头开始创建的嵌入式操作系统这些天的时间。Linux、 Windows 和其他的操作系统都不在盖下运行。我依靠任何没有开放源码软件,的确会通常是不可能还是会变得清楚的原因。这已经睁开眼睛看到的是从传统的 PC 软件开发的世界完全不同的 C 和 c + + 编程的整个世界。大多数嵌入式的系统有非常不同的约束,从那些"正常"的程序。他们必须是非常可靠。失败可能代价高昂。用户很少围绕"重新启动"失败的程序。系统可能需要运行多年不间断和无需人为干预。想象一个没有 Windows 更新或类似的世界。这些系统还可能有相对稀缺的计算资源。正确性、 可靠性和特别是并发所有在此类系统的设计中发挥了核心作用。

为此,Visual c + +,其功能强大的图书馆、 毛绒世界很少是适当的。即使 Visual c + + 目标我嵌入式微控制器技术,随附的库不适合系统与这种稀缺的计算资源,通常,硬实时约束。我目前正在使用的微处理器之一具有只是 32 KB 的内存少于 50 兆赫,在运行,这是一些嵌入式社区仍豪华。它应该是明确的"嵌入式"我并不是你平均的智能手机,用半棒的 RAM 和 1 GHz 的处理器。

在"规划:原则和实践使用 c + +"(艾迪生 - 韦斯利专业,2008年),Stroustrup 援引免费存储分配 (示例中,新和删除)、 例外和 dynamic_cast 中的特性,必须最嵌入式系统中避免,因为他们不是可预测的简短列表。不幸的是,这排除的大多数标准的使用、 供应商提供并打开今天可用的源 c + + 库。

其结果是,大部分嵌入式的编程 — — 和内核模式编程在 Windows 上的那件事,— — 仍然采用 C,而不是 c + +。鉴于 C 是主要的 c + + 的一个子集,我倾向于使用但粘到的语言,是可预测的、 便携式和工作良好的嵌入式系统的严格子集的 c + + 编译器。

这使我在寻找一种合适的技术,使我一点嵌入式操作系统中的并发的旅程。至此,我的 OS 了单个线程,如果你可以调用它。有无阻塞的操作,因此任何实施的东西所需的时间,可能需要花一些时间,例如存储 I/O 等待中断或网络重新传输超时时间,我会需要写一个精心构造的状态机。这是事件驱动的系统的标准做法,但结果在代码中的逻辑上,很难通过的原因是因为该代码不顺序。

想象一下,一个存储驱动程序。可能有一个 storage_read 函数来从设备的持久性闪存存储中读取的内存页。Storage_read 函数可能会首先检查是否外围设备或巴士正忙,,若然,只返回前队列读取的请求。在一些点外围设备,并可以继续成为免费巴士和要求。这可能需要禁用无线电收发机,适当的巴士,准备直接内存访问缓冲区,再次使收发器,然后再返回来允许调用方去做别的事情,而在传输完成硬件中的命令的格式。最终总线信号通过一些回调机制,通知其完成和调用方和任何其他排队的请求继续进行。不用说,它可以获得相当复杂的管理队列、 回调和状态机。验证一切无误的是更难。我甚至还没有描述如何非阻塞的文件系统可能会实现在这一抽象或 Web 服务器可能会如何使用文件系统来上菜的数据 — — 所有无过阻塞。另一种办法被需要减少的必然和日益增长的复杂性。

现在,想象一下 c + + 有几个关键字,使您可以传输到 mid-function 中的另一个调用堆栈中的处理器。想象一下以下假设 c + + 代码:

void storage_read(storage_args & args) async
{
  wait while (busy);
  busy = true;
  begin_transfer(args);
  yield until (done_transmitting());
  busy = false;
}

注意"异步"上下文关键字后的参数列表。 我还用两个假想的间隔的关键字,命名为"等待时"和"直到产量"。考虑它对 c + +,有这种关键字意味着什么。 编译器将以某种方式不得不表达概念的插曲,如果你愿意的话。 Knuth 称为这 coroutine。 异步关键字可能会出现让编译器知道函数可以异步运行,因此必须适当地调用函数声明。 实际点的功能不再执行同步和潜在可以返回到调用方,只是为了恢复它停止的位置在稍后阶段假设的等待和产量的关键字。 你也可以想象一个"等待"的条件关键字,以及无条件 yield 语句。

我见过这种合作形式的并发性替代品 — — 尤其是异步代理库包括 Visual c + + — — 但我发现的所有解决方案都取决于某些运行时调度引擎。 我的建议在这里,一会儿会说明是它是完全有可能 c + + 编译器实际上可能没有任何运行时,任何成本提供合作并发。 请记住我不说这将解决 manycore 可扩展性面临的挑战。 我的意思我们应该能够写快速和响应的事件驱动程序,而不涉及调度程序。 并与现有的 C 和 c + + 语言,一样没有什么应防止这些技术使用以及操作系统线程或其他并发运行时环境。

很明显,c + + 不支持我现在正在描述。 然而,我发现的是它可以模拟相当好使用标准 C 或 c + + 和无需依赖汇编程序弄虚作假。 使用这种方法,前面所述的 storage_read 函数可能会看,如下所示:

task_begin(storage_read, storage_args & args)
{
  task_wait_while(busy);
  busy = true;
  begin_transfer(args);
  task_yield_until(done_transmitting());
  busy = false;
}
task_end

很明显,我希望这里的宏。 叹息 ! 很显然,这违反了 c + + 编码标准中的 16 项 (bit.ly/8boiZ0),但其它选择更糟。 理想的解决办法是要直接支持这样的语言。 备选方案包括使用 longjmp,但我发现,更糟糕的是,有自己的陷阱。 另一种方法可能是使用汇编语言,但然后我会失去所有的可移植性。 这是值得商榷是否它甚至可以有效地在汇编语言,因为,最有可能会导致一种解决方案,使用更多的内存,由于丢失的上下文信息和不可避免的每个任务一个堆栈执行。 所以幽默我,我告诉你如何简单和有效这是,然后它是如何工作。

为了保持清晰的东西,我就从此叫这些异步功能任务。 鉴于前面所述的任务,它可以安排只需通过调用它作为函数:

storage_args = { ...
};
storage_read(args);

作为任务决定它不能继续进行,它将只返回给调用方。 任务使用 bool 的返回类型,以指示向呼叫者是否他们已经完成。 因此,您可以连续安排任务,直到它完成,如下所示:

while (storage_read(args));

当然,这会阻止调用方,直到任务完成。 这实际上可能适当情况下,也许当你的程序第一次启动以加载配置文件或类似。 除了该异常,你很少想要阻止这种方式。 你需要的是合作的方式等待任务的方式:

task_wait_for(storage_read, args);

这是假定调用方本身就是一项任务和嵌套的任务完成,此时它将继续之前将然后屈服于它的调用方。 现在让我松散定义的任务关键字或 pseudo-functions,然后通过一个示例或实际上可以尝试为自己的两个:

  • task_declare (名称、 参数) 宣布一项任务,通常在头文件中。
  • task_begin (名称、 参数) 开始一项任务,通常在 c + + 源代码文件中的定义。
  • task_endEnds 定义的任务。
  • task_return () 终止执行的任务,并将控制权返回给调用方。
  • task_wait_until (表达式) 一直等待,直到表达式为真才能继续。
  • 表达式为真才能继续等待 task_wait_while (表达式)。
  • task_wait_for (名称、 参数) 执行该任务,并等待其完成才继续。
  • task_yield () 收益率无条件地控制时的重排任务的继续。
  • task_yield_until (表达式) 的收益率至少一次,控制继续当表达式为非零值。

它是重要的是要记住没有这些例程阻止任何方式。 它们被旨在实现并发的高度合作的方式。 让我举一个简单的例子来说明。 此示例使用两个任务,一来提示用户输入数目和其他计算的数字的简单算术平均值,当他们到达。 首先是平均任务,所示图 1

图 1 的平均任务

struct average_args
{
  int * source;
  int sum;
  int count;
  int average;
  int task_;
};
task_begin(average, average_args & args)
{
  args.sum = 0;
  args.count = 0;
  args.average = 0;
  while (true)
  {
    args.sum += *args.source;
    ++args.count;
    args.average = args.sum / args.count;
    task_yield();
  }
}
task_end

任务接受一个参数,必须通过引用传递,并必须包括一个称为 task_ 的整数成员变量。 很明显,这是编译器会隐藏从调用方给予一流语言的支持,理想的方案。 但是,为此模拟,需要一个变量来跟踪任务的进展情况。 调用方需要做的就是将其初始化为零。

其中包含无限 while 循环内循环体 task_yield 调用任务本身很有趣。 任务进入这个循环之前初始化某个状态。 它然后更新其聚合和收益率,使要无限期地重复之前执行其他任务。

下一步是输入的任务,如中所示图 2

图 2,输入任务

struct input_args
{
  int * target;
  int task_;
};
task_begin(input, input_args & args)
{
  while (true)
  {
    printf("number: ");
    if (!scanf_s("%d", args.target))
    {
      task_return();
    }
    task_yield();
  }
}
task_end

这项任务很有趣,因为它说明了任务实际上可能会阻止,作为 scanf_s 函数将做等待输入时。 虽然不理想的一种事件驱动的系统。 此任务还演示了使用 task_return 调用来完成 mid-function 中的任务,而不是使用条件表达式中,当声明。 任务完成通过调用 task_return 或从该函数,结束掉下来这么说。 不管怎样,呼叫者会认为这是任务的完成,并将能够恢复。

要给生活带来这些任务,你需要的一切是简单的主要功能,作为一个调度程序:

int main()
{
  int share = -1;
  average_args aa = { &share };
  input_args ia = { &share };
  while (input(ia))
  {
    average(aa);
    printf("sum=%d count=%d average=%d\n",
      aa.sum, aa.count, aa.average);
  }
}

着无穷无尽的可能性。 您可以编写表示计时器、 生产者与消费者、 TCP 连接处理程序和更多的任务。

它是怎么工作的? 第一次请记住理想的解决方案是,编译器执行此,在这种情况下它可以使用各种巧妙的把戏来有效地实现它和我将要描述实际上并不会在附近复杂或复杂。

作为最佳,我可以说,这归结为一项发现由一个名叫汤姆夫,发现了你可以玩诡计使用 switch 语句的程序员。 只要它是有效的语法,您可以嵌套各种选择或内 switch 语句来有效地跳进一个函数在迭代语句会。 达夫发表应用手动循环展开,一种技术,泰瑟姆这时才意识到它可以用来模拟无穷无尽。 我把这些想法,并执行任务,如下所示。

Task_begin 和 task_end 宏定义周围的 switch 语句:

#define task_begin(name, parameter) \
                                    \
  bool name(parameter)              \
  {                                 \
    bool yield_ = false;            \
    switch (args.task_)             \
    {                               \
      case 0:
#define task_end                    \
                                    \
    }                               \
    args.task_ = 0;                 \
    return false;                   \
  }

它现在应该明显,单 task_ 变量是什么以及它是如何工作。 初始化为零的 task_,确保执行跳到该任务的开始。 任务结束时,它已再次设置回以零为方便,因此该任务可以轻松地重新启动。 鉴于此,task_wait_until 宏提供的必要跳位置和合作返回设施:

#define task_wait_until(expression)      \
                                         \
  args.task_ = __LINE__; case __LINE__:  \
  if (!(expression))                     \
  {                                      \
    return true;                         \
  }

Task_ 变量设置为预定义的行数的宏,和 case 语句获取相同的行数,从而确保,如果任务的收益率,它已排定代码在下一次将会恢复它停止的位置的权利。 其余的宏所示图 3

图 3 的剩余的宏

#define task_return()                    \
                                         \
  args.task_ = 0;                        \
  return false;
#define task_wait_while(expression)      \
                                         \
  task_wait_until(!(expression))
#define task_wait_for(name, parameter)   \
                                         \
  task_wait_while(name(parameter))
#define task_yield_until(expression)     \
                                         \
  yield_ = true;                         \
  args.task_ = __LINE__; case __LINE__:  \
  if (yield_ || !(expression))           \
  {                                      \
    return true;                         \
  }
#define task_yield()                     \
                                         \
  task_yield_until(true)

这些都应明显。 也许只有城府值得一提的是 task_yield_until,因为它是类似于 task_wait_until,但事实,即它将总是产生至少一次。 task_yield,反过来,将只有过一次,高产深信任何受人尊敬的编译器将优化掉我速记。 我应该提及,task_wait_until 也是出的内存条件处理的好方法。 而不是在某些与可疑的可恢复性的深层嵌套操作失败,你可以简单地屈服,直到内存分配成功,给予其他任务完成,并希望释放一些内存,急需的机会。 再次,这是关键的嵌入式系统位置失败不是选项。

既然我模仿无穷无尽,有一些陷阱。 您不能可靠地使用本地变量内的任务,和任何违反有效性的隐藏的 switch 语句的代码将会带来麻烦。 不过,既然我可以定义自己的 task_args — — 并考虑多么简单我的代码多亏了这种技术 — — 感激它像它一样工作。

我发现有用来禁用以下的 Visual c + + 编译器警告:

#pragma warning(disable: 4127) // Conditional expression is constant
#pragma warning(disable: 4189) // Local variable is initialized but not referenced
#pragma warning(disable: 4706) // Assignment within conditional expression

最后,如果您正在使用 Visual c + + IDE,一定要使用 /Zi 代替 /ZI 禁用"编辑并继续调试"。

当我结束此列,我看了看四周为任何类似的举措 Web 和发现新的异步,等待出台的 Visual Studio 2012 C# 编译器的关键字。 在许多方面,这是试图解决类似的问题。 我期望,c + + 社会效仿。 问题是,这些解决方案将 C 和 c + + 来会产生可预见代码适合嵌入式系统,如我在本专栏中描述过或是否他们会依靠一个并发运行时,作为当前 Visual c + + 库做的方式。 我的希望是有朝一日能扔掉这些宏,但直到那一天到来,我会继续生产此轻量化、 合作、 多任务处理的技术。

敬请与 c + + 中,我将给你们一些新技术尼古拉斯尔斯 · 古斯塔夫松和阿尔图尔 · Laksberg 从 Visual c + + 团队一直对 c + + 将可恢复功能的 Windows 的下一部分。

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

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