发布于 2026-01-06 7 阅读
0

Go 与 Erlang 并发 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

Go 与 Erlang 中的并发性

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

夜幕下的桥上汽车

我使用 Go 语言编写代码已经一年多了,并且成功编写了一个相当大的库

不过,我最近对函数式编程很感兴趣,偶尔会用 Go 语言快速搭建一些 API 服务器。我开始学习Erlang,并比较了这两种语言在编写并发程序方面的差异。

对于许多人来说,Erlang 是一种相当古怪的语言,尤其是对于更熟悉 C 风格命令式编程的程序员而言。

我们都知道 Go 语言非常流行,不仅是因为它的简洁性,还因为它拥有两个远胜 Node.js 的并发结构——通道 (channel) 和 goroutine。在 Go 中,创建并行例程并与之交互非常简单,只需以下方式即可:

func main() {
        numchan := make(chan int, 1)
        donechan := make(chan struct{})

        go func() {
                fmt.Println(<-numchan + 1)
                donechan <- struct{}{}  
        }() 
        numchan <- 8
        <-donechan
}
Enter fullscreen mode Exit fullscreen mode

关键在于,通道(channel)是一个绝佳的通道,你可以从任何 goroutine 向其发送值,并且许多其他 goroutine 可以安全地使用这些值,而不会发生数据竞争。Go 通道会确保每个请求其值的 goroutine 都受到约束并有序执行(它在底层实现了互斥锁和信号量)。作为程序员,你只需要信任通道即可。你无法控制或了解哪个 goroutine 先访问了通道。

仔细想想,这并不意外。每个例程(理想情况下)都是并行执行的,它们之间不应该相互干扰。举个例子,假设你需要将几个 URL 分配给多个 goroutine,让它们像工作进程一样运行,每个 goroutine 在完成 HTTP 请求后,打印从 URL 收到的响应状态码:

func printStatusCode(url string) {
        res, _ := http.Get(url)
        fmt.Printf("%d: %d\n", pid, res.StatusCode)
}

func main() {
        var wg sync.WaitGroup
        urls := [...]string{
                "https://google.com",
                "https://facebook.com",
                "https://dev.to",
                // ... thousands more
        }
        for i, url := range urls {
                wg.Add(1)
                go func(pid int, url string) {
                        printStatusCode(url)
                        wg.Done()
                }(i, url)
        }
        wg.Wait()
}
Enter fullscreen mode Exit fullscreen mode

每个 goroutine 都会打印出它分配的索引pid(即 URL 在urls数组中的索引)以及收到的响应的状态码。打印索引很有意思,因为它能让你看到 goroutine 是并行执行的,而不是同步执行的。sync.WaitGroup这里使用了这种方式,而不是像前面的例子那样使用信号通道。这使得等待多个 goroutine 变得更加容易。

这里的关键在于 Go 通过通道抽象了并发。你可以使用通道向一个或多个例程发送数据,也可以在不同的例程之间进行同步/信号传递。

让我们来看看如何在 Erlang 中实现同样的功能。

在 Erlang 中,并行执行被称为进程。它们与 Go 中的例程相同(至少就本文而言)。

Erlang 没有像 Go Channel 那样的中间层,但它采用了一种非常强大的概念,称为 Actor 模型。在这个模型中,进程是一个独立的 Actor,它不关心外部世界。它就像一个囚犯,独自消化着自己的东西,等待着被送到监狱门口,更确切地说,是送到邮箱里

进程邮箱类似于 Go 语言中的通道,但它是私有的。它是一个私有的数据缓冲区,供特定进程同步地逐个访问。当一个进程向另一个进程的邮箱发送数据时,数据会被存储在那里,直到目标进程需要使用它为止。

以下是一个与第一个 Go 示例功能相同的简单示例:

main() ->
    P1 = spawn(fun() ->
        receive
            Num -> io:format("~p~n", [Num + 1])
        end
    end),

    P1 ! 8,
    ok.
Enter fullscreen mode Exit fullscreen mode

这是怎么回事?怎么能用这么少的代码实现这个功能?首先,函数式语言的设计层次比大多数命令式语言高一个层次。在命令式编程中(比如 Go 语言的结构化编程,它本身就是一种命令式编程),你编写代码主要是为了改变某些状态,因此你必须关心每一行代码的执行时机A(先改变状态X,然后B读取数据,与先读取数据,然后改变状态,两者在逻辑X上是不同的)。而在声明式编程中,你可以直接表达逻辑,而无需关心控制流,因为每个表达式/函数都不会产生副作用,也就是不会改变自身之外的状态。BXAX

这就是 Erlang 的 Actor 模型如此简单而强大的原因。它无需担心数据竞争或同步问题,因为每个进程都无法访问任何外部资源。

每个 Erlang 进程占用内存很小,并且可以动态增长/收缩。它们不共享内存,仅通过消息传递进行通信。这与 Go 的并发解决方案非常相似。以下是与第二个 Go 代码等效的另一个 Erlang 示例:

printStatusCode(Url) ->
    {_, Res} = httpc:request(Url),
    {{_, StatusCode, _}, _, _} = Res,
    io:format("~p: ~p~n", [self(), Code]).

main() ->
    inets:start(),
    Urls = [ 
        "https://google.com", 
        "http://facebook.com", 
        "https://dev.to" 
    ],
    lists:foreach(fun(Url) -> spawn(?MODULE, printStatusCode, [Url]) end, Urls).
Enter fullscreen mode Exit fullscreen mode

现在情况变得有点复杂了,但这并非 Erlang 本身的问题,而是它的括号元组字面量的问题。这里最突出的是_括号内重复出现的下划线。这被称为模式匹配,是函数式编程中常用的一种技巧。这是因为=在 Erlang 中,`\` 表示“绑定”,而在 Go 中则表示“赋值”。因此,你可以像这样将左侧的数据结构与右侧的数据结构进行匹配:

{X, Y, _} = {"Joe", "Jim", "Jam"},
%% `X` is bound to "Joe" and `Y` is bound to "Jim"
[F | _ | T | _] = [1, 5, 2, 10, 54, 0, 98].
%% `F` is bound to 1 and T is bound to 2 
Enter fullscreen mode Exit fullscreen mode

Go 语言有类似的特性,叫做多重返回。

// Aw, not even close :(
a, b := func() (string, string) {
        return "Joe", "Jim"
}()
Enter fullscreen mode Exit fullscreen mode

由于 Go 语言不支持元组表达式,因此需要将多个返回值封装在函数表达式中。而且,你不能对数组或切片进行模式匹配。

需要注意的一点是,Go 是一种静态类型编译型语言,而 Erlang 是一种动态类型字节码编译型语言。Go 在算术运算(例如图像处理)方面当然比 Erlang 更胜一筹。此外,由于 Go 程序可以编译成可执行文件,因此无需虚拟机即可移植,比 Erlang 更具可移植性。

欢迎提出问题、反馈和建议。请在评论区留言。

文章来源:https://dev.to/pancy/concurrency-in-go-vs-erlang-595a