ZIG是一种新的通用编程语言,它正在从头开始重新思考语言和相关工具的工作方式。我已经讨论了编译时代码执行,现在我将介绍该语言的另一个创新概念:异步/等待。
是的,但是Zig中的异步/等待与编译时执行相结合,允许函数隐式地成为异步的,并且整个库以一种非常独特的方式在阻塞和事件I/O模式下透明地工作。
虽然Zig非常创新,但它试图成为一种小而简单的语言。Zig从C语言的简单性中获得了很多灵感,并保留了元编程杂技,以备您真正需要时使用。一般来说,你应该能够在一个周末使用Zig变得高效。因此,虽然大多数Zig功能会让您立即感到愉快和熟悉,但异步/等待是打破熟悉规则的一个例子,但这是有充分理由的。
在使用事件I/O时,命令式编程语言往往有两个选择:回调或异步/等待。
第一种情况的优点是不需要对语言进行任何添加,但缺点是现在一切都必须基于回调和嵌套闭包。第二种情况基本上让编译器将函数分解成不同的“阶段”,使整个翻译对用户透明(即在您看来,它仍然是正常的、顺序的、命令性代码),但不幸的是,它有引入函数着色的副作用。
这篇博客文章很好地解释了函数着色的概念,但总而言之:因为你不能从非异步代码调用异步函数,你最终会有很多重复的工作,你需要重新实现你的标准库和所有与网络相关的库的一部分,以解决异步/等待。其中一个例子是Python,在Python3中引入异步/等待产生了像aio-libs这样的项目,其目标是在AsyncIO之上重新实现流行的网络库。
让我们通过查看几个示例来了解异步/等待在Zig中是如何工作的。要查看以下代码片段的运行情况,请在另一个终端中以侦听模式启动Netcat,如果一切正常,您应该会看到“Hello World!”每次运行后都会打印出来。
第一个示例是一个写入套接字的简单阻塞程序。这没什么好惊讶的。
const net=@import(";std&34;).net;pub FN main()!void{const addr=try net.Address.parseIp(";127.0.0.1";,7000);try send_message(Addr);}fn send_message(addr:net.Address)!void{var socket=try net.tcpConnectToAddress(Addr);defer socket.close();_=try socket.write(";Hello World!\n";);}。
const net=@import(";std";).net;pub const io_mode=.evented;pub FN main()!void{const addr=try net.Address.parseIp(";127.0.0.1";,7000);try send_message(Addr);}fn send_message(addr:net.Address)!void{var socket=try net.tcpConnectToAddress(Addr);defer socket.close();_=try socket.write(";Hello World!\n";);}。
该声明在后台引起了一些更改,其中之一是以非阻塞模式打开套接字。这会导致函数变成异步的,但是正如您可以看到的那样,它的调用方式并没有改变:它看起来仍然像一个普通的函数调用,尽管它不是。
import asyncio async def main():等待send_message(";127.0.0.1";,7000)async def send_message(addr,port):_,write=等待异步cio.open_connection(addr,port)writer.write(b";Hello World!\n";)writer.close()asyncio.run(main())。
现在您已经看到,在Zig中,启动协程并立即等待其完成不需要额外的关键字。那么,你如何开始一个协和程序,然后等待它呢?当然是通过使用异步!
const net=@import(";std";).net;pub const io_mode=.evented;pub FN main()!void{const addr=try net.Address.parseIp(";127.0.0.1";,7000);var sendFrame=异步发送消息(Addr);//.。在发送邮件时//执行其他操作.。尝试等待sendFrame;}//注意函数定义如何不需要任何静态//`async`标记。编译器可以根据`wait`的用法推断函数何时为//异步。FN SEND_MESSAGE(地址:net.Address)!void{//如果我们在此期间有其他想要做的事情,我们也可以推迟‘等待’要建立的连接。var socket=net.tcpConnectToAddress(Addr);defer socket.close();//在同一条语句中同时使用aWait和Async//是不必要的,也不是惯用的,但是它显示了//当`io_mode`//为`.evented`时,幕后正在发生什么。_=尝试等待异步socket.write(";Hello World!\n";);}。
通过使用Async关键字,您可以创建协同例程并运行它,直到它遇到挂起点(粗略地说,它必须等待I/O发生)。返回值是Zig所说的“异步框架”,在某种程度上等同于来自其他语言的Future、Task、Promise或Coroutine对象。
让我向您展示完成这个难题的最后一个技巧:在阻塞模式下使用异步/等待。要恢复到阻塞I/O,我们所要做的就是删除开头添加的特殊声明(或者使用相应的枚举大小写)。
const net=@import(";std";).net;pub const io_mode=.block;pub FN main()!void{const addr=try net.Address.parseIp(";127.0.0.1";,7000);//是的,var sendFrame=async send_message(Addr);try await sendFrame;}fn send_message(addr:net.Address)!void{var socket=net.tcpConnectToAddress(Addr);defer.socket.close();//this Too_=尝试等待异步socket.write(";Hello World!\n";);}。
是的,该程序可以按照预期进行编译和工作。该函数不再是异步的,实际上这两个关键字基本上都变成了no-ops,但重点是即使您不能利用它,也可以表示并发性。在我们的小示例中,这可能看起来不是什么大事,但这是允许库在单个代码库中提供阻塞和事件I/O功能的原则。
不久前,我开始开发OkRedis,这是一个用Zig编写的Redis客户端库,它试图在不影响效率的情况下为用户提供尽可能多的细节。除此之外,它还在单个代码库中完全支持阻塞和事件I/O。如果你想了解更多,请看一下GitHub上可用的文档,并观看这个演讲,Andrew Kelley(Zig的创建者)与我合著。在这篇文章中,Andrew解释了Zig中异步/等待的基础知识,在第二部分中,我演示了OkRedis。
";在Zig";中使用异步I/O建模数据并发虽然编译器非常聪明,并且击败函数着色有许多实际好处,但它不是灵丹妙药,所以让我立即揭开它的神秘面纱。
不,要使您的程序利用事件I/O,您需要在代码中表示并发性。如果您从未这样做过,启用事件I/O将不会提供任何明显的优势,但是如果您正在使用的某个库被正确地设计为异步/等待,那么您可能会体验到更好的性能。
也就是说,如果您的代码嵌入到一个使用异步/等待的较大项目中,那么自动转换到事件I/O将在某种程度上使您的代码与周围的上下文配合得很好。
不需要,有很多应用程序需要事件I/O才能正常运行。切换到阻塞模式,而不引入任何更改,可能会导致它们死锁。例如,可以考虑单个应用程序既充当其自身的服务器,又充当其自身的客户端。
也就是说,在编译时,可以检查整个程序是否处于事件模式,例如,正确设计的代码可能会在处于阻塞模式时决定移动到线程模型。
问:所以我甚至不需要考虑我的库中的普通函数和协程?
不,偶尔你会不得不这么做。例如,如果您允许用户在运行时将函数指针传递给您的库函数,则需要确保根据函数是否是异步的使用正确的调用约定。您通常不必考虑这一点,因为编译器能够在编译时为您完成工作,但对于运行时已知值则不能这样做。
好的一面是,您可以使用所有工具,以简单而清晰的方式说明所有的可能性。一旦您获得正确的细节,代码将不会比必须的更复杂,并且您的库将很容易使用。
虽然我在撰写简介时考虑到的是普通开发人员,但您需要知道Zig不是一种动态类型的语言,最重要的是,当涉及到资源管理时,它会给您带来很大的权力(和责任)。例如,如果您知道如何在C#、JavaScript或Python中执行异步/等待操作,您就不能立即知道如何在Zig中执行所有操作。
具体地说,垃圾收集的语言对您隐藏了内存的来源。这使得程序员的工作变得容易得多,但是这种额外的轻松的代价完全由机器承担。这不是什么新鲜事,这是一个非常值得做的权衡,但是,特别是在异步/等待方面,它是有问题的,因为你失去了对内存消耗的控制,最终会过量使用,并在超负荷时遇到问题(有关更多信息,请参阅这篇博客帖子)。
ZIG的要点之一就是使资源配置始终清晰、可管理。当涉及异步/等待时,这意味着运行协同例程所需的所有内存都由其底层异步帧表示。一旦您有了一个帧(无论是因为它是静态内存还是因为相应的动态分配成功了),那么您就知道协程将能够顺利运行到完成。例如,在HTTP服务器的上下文中,这意味着您将能够预先知道您是否有足够的资源来接受连接,而不会遇到不可恢复的错误条件。
到目前为止,我只讨论了实现协程的语言特性。我没有全部提到,但更重要的是,我没有讨论事件循环。在Zig中,事件循环是标准库的一部分,其想法是使其可交换。
在撰写本文的时候,事件循环还有很多工作要做,但是今天您已经可以尝试所有东西了。如果您想知道,当前的实现已经是多线程的。只需访问ziglang.org,下载最新版本,并查看文档即可。
啊,是的。事实上,Go-与其他几种语言一起使用-没有函数着色问题。安德鲁在前面的谈话中提到了GO,但我会在这里提出我的两点看法。
如果你以前看过我的博客,你就知道我喜欢围棋。我认为在应用程序级编程中,Goroutines通常比异步/等待更可取,特别是当涉及服务器端应用程序时,我认为这是Go的主要领域。
我认为Goroutines在这种情况下更可取,因为异步/等待是一个很容易误用的低级工具,但是当涉及到编写具有正确性和效率的关键要求的代码时,您需要异步/等待,您需要Zig的哲学,即我们都应该努力编写健壮、优化和可重用的软件。
非常感谢您的阅读!有什么意见吗?发推文:@croloris。另外,我开始定期在Twitch上直播编码,下一次直播时请关注频道通知我!