【问题标题】:Can Go spawn and communicate with external processes without starting one OS-thread per external process?Go 是否可以在不为每个外部进程启动一个 OS 线程的情况下生成并与外部进程通信?
【发布时间】:2015-11-27 00:19:50
【问题描述】:

短版:

Golang 是否可以并行生成多个外部进程(shell 命令),这样它就不会为每个外部进程启动一个操作系统线程。 .. 完成后仍然能够接收到它的输出吗?

加长版:

在 Elixir 中,如果您使用端口,则可以生成数千个外部进程,而不会真正增加 Erlang 虚拟机中的线程数。

例如以下代码 sn-p 启动了 2500 个外部 sleep 进程,仅由 Erlang VM 下的 20 个操作系统线程管理:

defmodule Exmultiproc do
  for _ <- 1..2500 do
    cmd = "sleep 3600"
    IO.puts "Starting another process ..."
    Port.open({:spawn, cmd}, [:exit_status, :stderr_to_stdout])
  end
  System.cmd("sleep", ["3600"])
end

(前提是您将ulimit -n 设置为较大的数字,例如10000)

另一方面,Go 中的以下代码应该做同样的事情 - 启动 2500 个外部 sleep 进程 - 也启动 2500 个操作系统线程。因此,它显然会为每个(阻塞?)系统调用启动一个操作系统线程(以免阻塞整个 CPU 或类似的,如果我理解正确的话):

package main

import (
    "fmt"
    "os/exec"
    "sync"
)

func main() {
    wg := new(sync.WaitGroup)
    for i := 0; i < 2500; i++ {
        wg.Add(1)
        go func(i int) {
            fmt.Println("Starting sleep ", i, "...")
            cmd := exec.Command("sleep", "3600")
            _, err := cmd.Output()
            if err != nil {
                panic(err)
            }
            fmt.Println("Finishing sleep ", i, "...")
            wg.Done()
        }(i)
    }
    fmt.Println("Waiting for WaitGroup ...")
    wg.Wait()
    fmt.Println("WaitGroup finished!")
}

因此,我想知道是否有一种方法可以编写 Go 代码,使其执行与 Elixir 代码类似的事情,而不是为每个外部进程打开一个操作系统线程

我基本上是在寻找一种方法来管理至少几千个外部长期运行(最多 10 天)的进程,以尽可能减少操作系统中任何虚拟或物理限制的问题.

(对于代码中的任何错误,我深表歉意,因为我是 Elixir 和 Go 的新手。我很想知道我正在做的任何错误。)

编辑:澄清了并行运行长时间运行的进程的要求。

【问题讨论】:

  • 这绝对不是对所提出的 q 的回答,但 1)我知道 Go 可以启动 OS 线程以让代码在阻塞系统调用期间继续运行;我不知道这些线程是否具有,例如,您倾向于与 pthreads 线程或类似线程相关联的大堆栈分配,以及 2)我认为进程的 fork+exec 将主导成本似乎是合理的这个操作,和你程序这边的线程比较小。
  • @twotwotwo:确实,Go 线程似乎并不是非常重。但是,根据我的实验,它们似乎仍然限制了可以实际管理的外部进程的数量,至少比 Elixir 更多,因为(小)额外成本。
  • 线程是 OS 线程,也称为 pthread,与 goroutine 完全分离。
  • 生成一个子进程可能会消耗多达三个在读取/写入常用文件描述符时阻塞的 goroutine。调度程序还将产生三个操作系统线程来替换那些因阻塞读/写操作而消耗的线程。这是多大的问题?我不知道,但这取决于有多少线程太多,而有多少打开的文件描述符太多了。
  • 我在Ubuntu amd64上测试了go1.5下的10线程和100线程。 90 个额外打开的进程中的每一个在驻留大小(大致占用的物理内存)中的增量成本计算为 30.4kb。虚拟内存大小增加了 108kb/进程,但根据定义,这并不意味着占用了 108kb 物理 RAM。你可以运行更好的测试,但this &gt; nothing 我想。

标签: multithreading unix go concurrency elixir


【解决方案1】:

我发现如果我们没有wait 进程,Go 运行时将不会启动2500 operating system threads。所以请使用 cmd.Start() 而不是 cmd.Output()。

但是,如果不使用 golang os 包消耗操作系统线程,似乎不可能读取进程的stdout。我认为是因为 os 包没有使用非阻塞 io 来读取管道。

下面的程序在我的 Linux 上运行良好,虽然它像 @JimB 在评论中所说的那样阻塞了进程的标准输出,可能是因为我们的输出很小并且它适合系统缓冲区。

func main() {
    concurrentProcessCount := 50
    wtChan := make(chan *result, concurrentProcessCount)
    for i := 0; i < concurrentProcessCount; i++ {
        go func(i int) {
            fmt.Println("Starting process ", i, "...")
            cmd := exec.Command("bash", "-c", "for i in 1 2 3 4 5; do echo to sleep $i seconds;sleep $i;echo done;done;")
            outPipe,_ := cmd.StdoutPipe()
            err := cmd.Start()
            if err != nil {
                panic(err)
            }
            <-time.Tick(time.Second)
            fmt.Println("Finishing process ", i, "...")
            wtChan <- &result{cmd.Process, outPipe}
        }(i)
    }

    fmt.Println("root:",os.Getpid());

    waitDone := 0
    forLoop:
    for{
        select{
        case r:=<-wtChan:
            r.p.Wait()
            waitDone++
            output := &bytes.Buffer{}
            io.Copy(output, r.b)
            fmt.Println(waitDone, output.String())
            if waitDone == concurrentProcessCount{
                break forLoop
            }
        }
    }
}

【讨论】:

  • 谢谢江,我也在尝试这种方法,但它也启动了同样多的线程。在您的代码中,您必须进行大量秒的睡眠(例如 3600,即一个小时),而不仅仅是回声。另外,我认为你必须在 go-routine 中,在 wg.Done() 之前有一个 cmd.Wait(),以防止它完成,直到命令完成。当我解决这个问题时,我得到了相同的结果,请参阅:gist.github.com/samuell/630467732703feefcc81
  • 但是,我只是说不要等待 :( 我认为等待会消耗一个线程并且不能与其他 goroutines 重用。
  • 是的,但是无法获得这些过程的输出,这对我来说是非常重要的一点,并且在问题中提到了。
  • 当然我们可以读取进程的输出,即使没有等待。这是另一个问题,例如how to read output of process without wait process finish
  • 这仍然不正确。首先,在通过 StdoutPipe 读取所有内容之前调用 Wait 是不正确的,因为 Wait 隐式地​​尝试关闭管道。其次,您正在阻止进程的输出,如果命令没有充分缓冲其输出,这将死锁。与命令无关:您正在泄漏 time.Tickers - 如果您需要选择时间间隔,请使用 time.Aftertime.Ticker 您可以停止,但在这种情况下只需使用 time.Sleep,甚至最好不要使用任何东西,因为您不必为了参加比赛而必须睡觉。
猜你喜欢
  • 2010-11-05
  • 2020-02-24
  • 2013-08-23
  • 2018-04-09
  • 2017-08-07
  • 1970-01-01
  • 2015-07-16
  • 1970-01-01
  • 2017-09-12
相关资源
最近更新 更多