Contents

在Coni中构建原生并行下载

构建自己的语言和工具的绝佳之处之一,是能够重新思考操作的执行方式。在最近的提交中,我们决定解决构建过程中的一个主要瓶颈:下载Maven依赖项。

特定平台脚本存在的问题

以前,Coni中的 download-url-to-file 函数依赖于调用特定平台的外部工具:

  • 在Linux/macOS上,它会生成一个 curl 进程。
  • 在Windows上,它会调用一个使用 System.Net.WebClient 的庞大 powershell 命令。

虽然这种方法可行,但它有几个缺点。首先,调用外部进程既缓慢又消耗资源。其次,依赖外部工具意味着 curl 版本的微妙差异或Windows安全协议可能会导致意想不到的失败。最重要的是,使用这些shell命令顺序下载工件意味着解析大型Maven项目会花费太长时间。

原生解决方案

在提交 9ac76d12 中,我们引入了 sys-http-download,这是一个直接在Go评估器中实现的原生内置函数。这个新的内置函数使用了Go标准的 net/http 客户端。

因为它原生内置在评估器中,所以我们完全避免了进程创建的开销。我们甚至在Go实现中直接加入了一些健壮的重试逻辑,以处理来自Maven Central的速率限制(如果您在没有可识别的User-Agent的情况下请求过快,它经常会抛出429 Too Many Requests错误)。

使用Goroutine进行超级加速

在原生内置函数准备就绪后,真正的魔力发生在我们应用Coni的并发原语时。Coni使用 gochan<!(从通道读取)和 >!(写入通道)原生支持Go风格的并发。

我们没有逐个下载工件,而是直接在标准库中设置了一个工作池 (worker pool):

(let [total-count (count missing)
      result-ch (chan total-count)
      task-ch (chan total-count)]
  ;; 将所有任务推送到任务通道
  (loop [rem missing]
    (if (not (empty? rem))
      (do 
        (>! task-ch (first rem))
        (recur (rest rem)))))
  (close! task-ch)
  
  ;; 生成8个工作goroutine并发处理任务
  (loop [i 0]
    (if (< i 8)
      (do
        (go 
          (loop []
            (let [item (<! task-ch)]
              (if (not (nil? item))
                (do
                  (>! result-ch (download-file-single item repos))
                  (recur))))))
        (recur (+ i 1)))))

  ;; 等待结果并显示进度
  ...)

我们将工作池的上限限制为8个goroutine,以防止网络不堪重负或触发激进的速率限制。

结果

提速是巨大的。过去漫长、串行化的shell调用序列,现在变成了完全在Coni运行时内部处理的快速、并行化的操作。

通过在我们自己的项目中使用我们自己的并发原语 (dogfooding),我们证明了Coni基于通道的架构不仅仅是一个有趣的玩具——它足够健壮,可以高效地处理该语言自己的核心I/O任务。