1 回答

TA貢獻1831條經(jīng)驗 獲得超4個贊
在 Go 中,如果到達main
方法的末尾(在包中)?,程序?qū)⑼V埂?code>main此行為在 Go 語言規(guī)范中有關(guān)程序執(zhí)行的部分(強調(diào)我自己的)下進行了描述:
程序執(zhí)行從初始化
main
包開始,然后調(diào)用函數(shù)main
。當該函數(shù)調(diào)用返回時,程序退出。它不會等待其他(非主)goroutines 完成。
缺陷
我將考慮您的每個示例及其相關(guān)的控制流缺陷。您會在下面找到指向 Go playground 的鏈接,但這些示例中的代碼不會在限制性 playground 沙箱中執(zhí)行,因為sleep
找不到可執(zhí)行文件。復制粘貼到自己的環(huán)境進行測試。
多個 goroutine 示例
case?<-time.After(3?*?time.Second): ????????log.Println("closing?via?ctx") ????????ch?<-?struct{}{}
在計時器觸發(fā)并且您向 goroutine 發(fā)出信號是時候殺死孩子并停止工作之后,沒有什么可以導致方法main
阻塞并等待它完成,所以它返回。根據(jù)語言規(guī)范,程序退出。
main
調(diào)度程序可能會在通道傳輸后觸發(fā),因此在退出和其他 goroutine 醒來接收來自之間可能存在競爭ch
。然而,假設(shè)任何特定的行為交錯是不安全的——而且,出于實際目的,在main
退出之前不太可能發(fā)生任何有用的工作。子sleep
進程將成為孤兒;在 Unix 系統(tǒng)上,操作系統(tǒng)通常會將進程的父進程重放到進程上init
。
單個 goroutine 示例
在這里,你有相反的問題:main
不返回,所以子進程沒有被殺死。這種情況只有在子進程退出時(5 分鐘后)才能解決。發(fā)生這種情況是因為:
cmd.Wait
方法中的調(diào)用Run
是阻塞調(diào)用 (?docs?)。該select
語句被阻塞等待cmd.Wait
返回錯誤值,因此無法從quit
通道接收。通道
quit
(聲明為ch
)main
是一個無緩沖通道。無緩沖通道上的發(fā)送操作將阻塞,直到接收方準備好接收數(shù)據(jù)。來自頻道的語言規(guī)范(再次強調(diào)我自己的):以元素數(shù)量表示的容量設(shè)置通道中緩沖區(qū)的大小。如果容量為零或不存在,則通道是無緩沖的,只有當發(fā)送方和接收方都準備好時,通信才會成功。
由于
Run
在 中被阻塞cmd.Wait
,沒有準備好的接收器來接收方法ch <- struct{}{}
中的語句在通道上傳輸?shù)闹?code>main。main
塊等待傳輸此數(shù)據(jù),從而阻止進程返回。
我們可以通過較小的代碼調(diào)整來演示這兩個問題。
cmd.Wait
正在阻塞
要公開 的阻塞性質(zhì)cmd.Wait
,請聲明以下函數(shù)并使用它代替調(diào)用Wait
。此函數(shù)是一個包裝器,具有與 相同的行為cmd.Wait
,但有額外的副作用來打印 STDOUT 發(fā)生的情況。(游樂場鏈接):
func waitOn(cmd *exec.Cmd) error {
? ? fmt.Printf("Waiting on command %p\n", cmd)
? ? err := cmd.Wait()
? ? fmt.Printf("Returning from waitOn %p\n", cmd)
? ? return err
}
// Change the select statement call to cmd.Wait to use the wrapper
case e <- waitOn(cmd):
Waiting on command <pointer>
運行此修改后的程序后,您將觀察到控制臺的輸出。計時器啟動后,您將觀察到輸出calling ctx cancel
,但沒有相應(yīng)的Returning from waitOn <pointer>
文本。這只會在子進程返回時發(fā)生,您可以通過將睡眠持續(xù)時間減少到更小的秒數(shù)(我選擇了 5 秒)來快速觀察到這一點。
在退出頻道上發(fā)送,,ch
塊
main
無法返回,因為用于傳播退出請求的信號通道是無緩沖的,并且沒有相應(yīng)的偵聽器。通過更改行:
????ch?:=?make(chan?struct{})
到
????ch?:=?make(chan?struct{},?1)
通道中的發(fā)送main
將繼續(xù)(到通道的緩沖區(qū))并main
退出——與多 goroutine 示例相同的行為。然而,這個實現(xiàn)仍然是錯誤的:在返回之前,不會從通道的緩沖區(qū)中讀取值來真正開始停止子進程main
,所以子進程仍然是孤立的。
固定版
我已經(jīng)為你制作了一個固定版本,代碼如下。還有一些風格上的改進可以將您的示例轉(zhuǎn)換為更慣用的 go:
不需要通過通道間接發(fā)出停止時間的信號。相反,我們可以通過將上下文和取消函數(shù)的聲明提升到方法來避免聲明通道
main
。上下文可以在適當?shù)臅r候直接取消。我保留了單獨的
Run
函數(shù)來演示以這種方式傳遞上下文,但在許多情況下,它的邏輯可以嵌入到方法中main
,并生成一個 goroutine 來執(zhí)行cmd.Wait
阻塞調(diào)用。select
方法中的語句是main
不必要的,因為它只有一個case
語句。sync.WaitGroup
main
引入是為了明確解決在子進程(在單獨的 goroutine 中等待)被殺死之前退出的問題。等待組實現(xiàn)了一個計數(shù)器;對塊的調(diào)用,Wait
直到所有 goroutines 完成工作并調(diào)用Done
.
package main
import (
? ? "context"
? ? "log"
? ? "os/exec"
? ? "sync"
? ? "time"
)
func Run(ctx context.Context) {
? ? cmd := exec.CommandContext(ctx, "sleep", "300")
? ? err := cmd.Start()
? ? if err != nil {
? ? ? ? // Run could also return this error and push the program
? ? ? ? // termination decision to the `main` method.
? ? ? ? log.Fatal(err)
? ? }
? ? err = cmd.Wait()
? ? if err != nil {
? ? ? ? log.Println("waiting on cmd:", err)
? ? }
}
func main() {
? ? var wg sync.WaitGroup
? ? ctx, cancel := context.WithCancel(context.Background())
? ? // Increment the WaitGroup synchronously in the main method, to avoid
? ? // racing with the goroutine starting.
? ? wg.Add(1)
? ? go func() {
? ? ? ? Run(ctx)
? ? ? ? // Signal the goroutine has completed
? ? ? ? wg.Done()
? ? }()
? ? <-time.After(3 * time.Second)
? ? log.Println("closing via ctx")
? ? cancel()
? ? // Wait for the child goroutine to finish, which will only occur when
? ? // the child process has stopped and the call to cmd.Wait has returned.
? ? // This prevents main() exiting prematurely.
? ? wg.Wait()
}
- 1 回答
- 0 關(guān)注
- 223 瀏覽
添加回答
舉報