理解Bash中的协程
引子
过去做并发编程的开发时,接触得比较多的概念,就是“进程(procedure)”和“线程(thread)”。然而,不知道从什么时候起,“协程(co-processes)”这个概念突然就遍布整个世界了。偶然翻看bash的man帮助信息时,发现竟然连bash都已经自带了关键词coproc
的支持,于是抽空做了点研究学习。
预备知识
1. 管道
Unix/Linux系统的一个设计思想,是把每个简单功能都拆分开来,单独实现成为独立的(内部或外部)命令,然后通过管道等方式将它们组合起来,灵活地完成各种任务。
管道的常规使用形式如下:
|
|
用管道(|
)串联起来的各命令,每个命令都以前一个命令的“标准输出(stdout)”,作为自身的“标准输入(stdin)”。
例如,下面的命令排序展示了最近一周内登录到系统的用户及其登录次数:
|
|
解释如下:
|
|
2. 重定向
每个命令都允许从“标准输入”读取数据,并将结果输出到“标准输出”。此外,为便于调试和检查问题,程序还能把日志或报错信息输出到“标准错误(stderr)”。
在Linux系统中,“标准输入”、“标准输出”和“标准错误”都被封装成为设备文件的形式,分别是“/dev/stdin
”、“/dev/stdout
”和“/dev/stderr
”。这样,应用程序可以很方便地以文件读写的方式,完成数据输入输出的操作。对于文件的处理,在操作系统层面,会为每个打开的文件分配一个文件描述符(file descriptor,简称fd),通常是一个整数。上述三个设备文件,因为每个应用程序都会带有,其实被固定为0、1、2。
有了这些知识,我们就可以通过“重定向(redirection)”来深度定制bash命令的执行了,例如:
|
|
这个技巧很常用,可以在写bash脚本时,用于日志输出:
|
|
反过来,经常有些命令的帮助信息被输出到了“标准错误”,而我们又想从篇幅较长的该帮助中查找(grep
)到某项内容,则可以:
|
|
3. awk命令
GNU awk也许是Linux下最强大的基础命令。在我看来,要做数据分析,这个命令几乎是必学的。它有很多操作技巧甚至是不可替代的,或者说,用其他语言或工具,即使能实现同样功能,效率或便利程度都总有所不及。
比如,对一个非常非常巨大的文件,排除其重复行:
|
|
稍微调整一下,选取其中出现过三次或三次以上的行,不重复地输出出来:
|
|
此外,本文会用到的一个awk用法,给每一行内容增加一个当前时间的前缀:
|
|
4. bash数组
Bash支持变量操作,这些变量定义了各个命令的执行环境,所以通常也称为环境变量(environment variables)。
|
|
|
|
除了单值变量外,bash也支持数组变量:
|
|
协程
接下来,进入正题。首先看下man bash
中关于协程的介绍:
Coprocesses
A coprocess is a shell command preceded by the coproc reserved word. A coprocess is executed asynchronously in a subshell, as if the command had been terminated with the & control operator, with a two-way pipe established between the executing shell and the coprocess.
The format for a coprocess is:
coproc [NAME] command [redirections]
This creates a coprocess named NAME. If NAME is not supplied, the default name is COPROC. NAME must not be supplied if command is a simple command (see above); otherwise, it is interpreted as the first word of the simple command. When the coprocess is executed, the shell creates an array variable (see Arrays below) named NAME in the context of the executing shell. The standard output of command is connected via a pipe to a file descriptor in the executing shell, and that file descriptor is assigned to NAME[0]. The standard input of command is connected via a pipe to a file descriptor in the executing shell, and that file descriptor is assigned to NAME[1]. This pipe is established before any redirections specified by the command (see REDIRECTION below). The file descriptors can be utilized as arguments to shell commands and redirections using standard word expansions. The file descriptors are not available in subshells. The process ID of the shell spawned to execute the coprocess is available as the value of the variable NAME_PID. The wait builtin command may be used to wait for the coprocess to terminate.
Since the coprocess is created as an asynchronous command, the coproc command always returns success. The return status of a coprocess is the exit status of command.
可以了解到,bash里的协程,与其他编程语言中的协程(诸如Python等语言中,协程区别于线程的,至少还有资源分配是否轻量级等问题,等以后再仔细做研究学习),应该还是有所区别的。
bash的协程,其实是类似于“&”后台命令的方式,帮助用户在运行一个命令时,自动创建管道。
下面看下创建协程的过程,相应的环境变量与(管道)文件描述符的变化:
|
|
|
|
|
|
|
|
示例
综上,可以看到,bash的协程,其实是一个预定义了输入输出文件描述符的后台进程。
那么协程有什么实际用途呢?
下面展示一个例子,给每行输出文字增加一个时间字符串前缀:
|
|
这里有两个细节值得注意下:
- awk中加了一个“fflush()”语句,用于确保结果被实时输出,避免缓存导致阻塞。
- 在重定向协程的输出时,使用了Ctrl+Z的方式,来将程序放入后台,这个过程并不能直接使用“&”实现,推测原因应该与“&”的实现机制有关(可能是创建了一个bash子环境,待后续确认)。
小结
- 协程,是协作进程的简称,这是并发编程领域里一个相对比较新的概念。
- 在bash中,协程其实是一个后台进程,预定义了输入输出管道的文件描述符,方便后续其他命令与之进行交互。
参考链接
- Bash manual: Coprocesses
- StackExchange: How do you use the command coproc in various shells?
- StackExchange: Is coproc <command> the same as <command> &?
- Bash百宝箱:协作进程coproc
- 小议bash中的COPROC
扩展阅读
关于文件描述符255的介绍:
下面这篇帖子的回答中,论述了bash协程存在的问题,以及不被推荐的原因:
相较于bash协程,更被推荐使用的expect
命令:
其他相关链接: