8.1 异步工作简介

优质
小牛编辑
118浏览
2023-12-01

8.1 异步工作简介

异步机制是 vim8 版本引入的新机制,准确地说,是从 7.4 某个补丁开始引入,不过在 vim8 完善并正式发布。这一全新特性使得 vim 直接跳升一大版本号,可见意义非凡。

8.1.1 同步工作可能的问题

要理解异步的特性,不妨先回顾下在此之前只能同步工作的情况,会遭遇哪些不便。

比如要从一个目录下的文本文件中查找某个字符串,我们知道(在 unix 系统中)直接 有个 grep 工具可用。而在运行着的 vim 中,也可以通过 :!grep ... 命令调用系 统的 grep 工具。但是用 :! 执行外部命令的话,会临时切回启动当前 vim 的终端 ,外部命令的输出在该终端上;当外部命令经过或长或短的时间完成后,还需要等用户按 回车确认才回到 vim 正常的用户界面。如果是 windows 系统的 gVim ,:! 执行外部 命令则会弹出 cmd 黑框,展示外部命令的输出,也需要由用户确认关闭该黑窗才能回 到 gVim 编辑窗口。

显然按种方式,在运行外部命令的同时,在回到 vim 界面之前,vim 对用户而言是停止 工作的,比如用户暂时无法操纵 vim 进行编辑工作。vim 也有个类似的内部命令 :vimgrep 用于在多文件中搜索字符串,并将结果输出在 quickfix 窗口。运行该命 令不会切回 shell 终端,与 :!grep 很有些不同。但是,如果待搜索的文件很多,尤 其是类似 **/* 的递归所有子目录的文件搜索时,:vimgrep 命令完成搜索也可能很 慢,需要等待一段时间才能完成搜索。在等待的这段时间内,虽然仍然停留在 vim 界面 ,但 vim 也好像停止了与用户的交互工作,譬如按 j k 不见得会移动光标。事实上 vim 还是监测到你按了 j k 键,只不过要等 :vimgrep 这个慢命令完成后才会响 应后续按键。简单地说,就可能造成明显卡顿。

这就是旧版本 vim 按同步工作方式可能出现的问题。你可以将 vim 编辑器想象为一个单 线程的无限循环程序,等待着用户的按键,并立即根据按键命令处理工作。正常情况下 vim 响应用户按键命令是极快的,所以用户感觉很流畅。因为正常人类的击键速度在计算 机程序看来都太慢了,vim 在大部分时间里都是在等待用户击键的。但是当用户试图让 vim 执行某些“能感觉出来慢”的命令时,问题就浮现了,影响用户体验。

如果上面的 :vimgrep 命令没让你感觉到慢,可以用 VimL 定义如下的慢函数:

function! SlowWork()
    sleep 5
    echo "done!!"
endfunction

然后在命令行输入 :call SlowWork() 并回车,你应该就能感觉到 vim 明显卡顿了。 在此期间若按几次 j ,也要等该函数返回才能发现光标移动。此外,你也可以试试用 while 1 | endwhile 定义一个无限循环函数,调用时会令 vim 完全停止响应,如此 请用 Ctrl-C 强制结束当前命令,回到 vim 的正常工作状态来。

8.1.2 异步工作想解决的问题

显然,vim8 引入的异步机制,就是试图解决(或部分解决、缓和)上述同步模型中出现 的“慢命令卡顿”问题。当然它也不是直接重定义优化原来命令的工作方式,因为兼容旧习 惯也是 vim 的传统。所以,在 vim8 中,类似 :!grep:vimgrep 命令,该怎么 慢还怎么慢,它真正想优化的类似 system('grep') 函数的工作方式。

system('grep'):!grep 的相同之处在于都是调用外部命令(系统可执行程序) ,只不过调用 system() 函数不会切到 shell 终端,仍停留在 vim 界面。所调用的外 部命令的输出会被 system() 函数所捕获,可以保存在 VimL 变量中,供脚本后续使用 。如果该外部命令执行时间较长, vim 用户仍会感到停止响应或卡顿。

然后在 vim8 中,就提供了另一套不叫 system() 名字的函数,用于执行外部命令。 vim 不再等待外部命令结束,而是立即返回给用户,可以立即接着响应用户按键。等外部 命令终于结束了,vim 再调用一个回调函数处理结果。

开启异步工作的具体函数与用法,留待下一切详细介绍。不过你该能感觉与估计到,这个 异步编程模型比本书之前介绍的同步编程模型要复杂些。并且在监测外部命令结束时准备 回调也必然有其他开销,所以异步也不宜滥用,只适合在(可预期)比较耗时的外部命令 上。如果只是简单的可以快速完成的外部命令,仍用原来的 system() 函数完成工作即 可。

另外要提及的是,目前 vim8 版本的异步机制,也只能将外部命令以异步的方式开启,并 不能用异步的方式执行内部命令。也就是说,不论是 vim 内置的命令(及常规函数), 还是用 VimL 写的自定义命令(函数),都仍只能按原来的同步方式执行,暂无异步用法 。

8.1.3 异步机制带来的 vim 新特性简介

vim8 提供异步机制后,可以据此实现很多新特性。比如内置终端(从 vim8.1 版本开始 支持)。在命令行执行 :terminal 就能打开一个新窗口,体验一下内置终端。在这个 特殊的 vim 新窗口中,就相当于运行着一个 shell ,可以像系统 shell 一样执行任何 命令,甚至也可以在此又运行一个 vim (不过一般情况下不建议这么玩)。用窗口切换 快捷键 Ctrl-w 可以回到之前的普通 vim 窗口,正常操作 vim 进行编辑工作。

也就是说,内嵌终端正是异步运行的,并不中断 vim 本来的编辑工作。相比在这个功能 出现之前,用 :shell 命令打开的子终端,就会切出 vim 界面,只能在那个子终端中 工作,必须在那执行 $ exit 退出子终端,才能回到 vim 。

关于内置终端的详细用法请参考 :help terminal ,在那文档中还介绍了在 vim 中“ 嵌入”gdb 调试 vim 本身的示例。表明内置终端功能其实不止能执行一个 shell ,还 适于执行其他任何交互程序,例如 python 解释器,mysql 客户端,gdb 调试器等。

不过本章不是介绍 vim 的这类新特性,而是侧重介绍 VimL 脚本编程中如何使用这个异 步机制,据此可以完成之前的脚本无法完成的工作,或优化某些插件功能。

8.1.4 异步编程的简单运用:定时器

让我们先看一个简单的例子来体验下异步编程的风格,定时器(请确认 vim 编译包含 +timers 特性)。将上文按传统同步风格定义的 SlowWork() 函数重新改写如下:

function! SlowWork()
    call timer_start(5*1000, 'DoneWork')
endfunction

function! DoneWork(timer)
    echo "done!!"
endfunction

call SlowWork()

现在再调用 SlowWork() 函数时就不会“暂停” 5 秒了,该调用立即返回,用户可如常 操作 vim 。大约过了 5 秒后,函数 DoneWork() 被调用,显示 "done!!"

这里的关键是在 SlowWork() 中用 timer_start() 启用了一个定时器。参数一是时 间,单位毫秒;参数二叫回调函数,应该是函数引用,但也可用函数名代替。其意义就是 在指定时间后调用那个回调函数,而不影响现在 vim 对用户的正常响应。还可以指定可 选的第三参数,表示重复回调若干次,默认就只回调一次,然后自动关闭定时器。该函数 有返回值,表示定时器 ID,在 vim 内部就用该 ID 标记这个定时器。回调函数一般是自 定义函数,必须接收一个表示定时器 ID 的参数。不过在这个简单示例中,我们忽略未用 到这个定时器 ID 参数。定时器相关函数的详细用法请参考 :help timer-functions

由此可见,异步编程的基本思路是将原来在一个函数内的工作(一般是较费时的工作), 多拆出一个回调函数,用来在工作完成时处理“后事”,关键也就是回调函数的编写。在这 个例子中,我们用定时器来“模拟”了一件慢工作,当然定时器本身也另有用途场景。

定时器可以明确指定延时几秒,不过在实际的慢工作(外部命令)中,需要多长时间完成 工作是不确定的。这就需要另外的机制,根据其他条件来调用回调函数。这就是下一节准 备讲的“任务”,原文档术语叫 job 的话题了。