英文:
How can I properly demonstrate concurrency AND parallelism in Go/Golang?
问题
为了演示如何使用Go编写并发且并行运行的程序,我制作了一个程序来进行演示。输出结果显示至少是并发运行的,但我不确定如何判断它是否是并行运行的。我已经阅读了很多关于如何使用goroutine和sync包中的WaitGroup来同步它们的资源,但是关于它们是否在单独的线程上运行存在很多混淆。作为一个对Go尤其新手的初学者,我非常希望能够得到一些澄清!
以下是要翻译的代码部分:
package main
import (
"fmt"
"sync"
"time"
)
//接订单
func takeOrder1(wg *sync.WaitGroup) {
s_sleep(1000)
fmt.Println("\n接订单...", t_time())
go takeOrder2(wg)
}
func takeOrder2(wg *sync.WaitGroup) {
s_sleep(1500)
fmt.Println("\n订单已接收!", t_time())
wg.Done()
}
//炸薯条
func makeFries1(wg *sync.WaitGroup) {
s_sleep(1500)
fmt.Println("\n正在炸薯条...", t_time())
go makeFries2(wg)
}
func makeFries2(wg *sync.WaitGroup) {
s_sleep(3000)
fmt.Println("\n薯条已炸好!", t_time())
wg.Done()
}
//煎汉堡
func makeBurger1(wg *sync.WaitGroup) {
s_sleep(2000)
fmt.Println("\n翻转汉堡...", t_time())
go makeBurger2(wg)
}
func makeBurger2(wg *sync.WaitGroup) {
s_sleep(5000)
fmt.Println("\n汉堡已煮好!", t_time())
wg.Done()
}
//制作饮料
func pourDrink1(wg *sync.WaitGroup) {
s_sleep(1000)
fmt.Println("\n将冰放入杯中...", t_time())
go pourDrink2(wg)
}
func pourDrink2(wg *sync.WaitGroup) {
s_sleep(3000)
fmt.Println("\n将汽水倒入杯中...", t_time())
go pourDrink3(wg)
}
func pourDrink3(wg *sync.WaitGroup) {
s_sleep(2500)
fmt.Println("\n饮料已倒好!", t_time())
wg.Done()
}
//擦桌子
func cleanTable1(wg *sync.WaitGroup) {
s_sleep(1000)
fmt.Println("\n'清洁'桌子中....", t_time())
go cleanTable2(wg)
}
func cleanTable2(wg *sync.WaitGroup) {
s_sleep(1500)
fmt.Println("\n桌子'清洁'完成!", t_time())
wg.Done()
}
//延时
func s_sleep(x int) { time.Sleep(time.Duration(x) * time.Millisecond) }
//打印时间
func t_time() string {
return time.Now().Format("15:04:05")
}
//创建任务数组
var McDolansTasks = []func(*sync.WaitGroup){
takeOrder1, makeFries1, makeBurger1, pourDrink1, cleanTable1}
//主函数
func main() {
var waitGroup sync.WaitGroup
//设置要等待的有效goroutine数量
waitGroup.Add(len(McDolansTasks))
for _, task := range McDolansTasks {
//传递WaitGroup实例的引用
//每个任务都应调用WaitGroup.Done()
go task(&waitGroup)
}
//等待所有goroutine执行完毕
waitGroup.Wait()
println("\n下班时间!")
}
你可以在这里查看完整代码和输出结果:链接
英文:
For a presentation, I made a program trying to illustrate how you can make a program that is concurrent and runs in parallel using Go. The output appears to show that it is at least running concurrently, but I'm not sure how to tell if it's running in parallel. I've read through a lot of resources on how to use goroutines and sync them together in a WaitGroup, but there seems to be a lot of confusion on whether these run on separate threads. As a novice programmer that's especially new to Go, I would greatly appreciate some clarification!
package main
import (
"fmt"
"sync"
"time"
)
//take order
func takeOrder1(wg *sync.WaitGroup) {
s_sleep(1000)
fmt.Println("\nTaking order...", t_time())
go takeOrder2(wg)
}
func takeOrder2(wg *sync.WaitGroup) {
s_sleep(1500)
fmt.Println("\nOrder tooken!", t_time())
wg.Done()
}
//make fires
func makeFries1(wg *sync.WaitGroup) {
s_sleep(1500)
fmt.Println("\nFrying fries...", t_time())
go makeFries2(wg)
}
func makeFries2(wg *sync.WaitGroup) {
s_sleep(3000)
fmt.Println("\nFries Fried!", t_time())
wg.Done()
}
//burn burger
func makeBurger1(wg *sync.WaitGroup) {
s_sleep(2000)
fmt.Println("\nFlipping burger...", t_time())
go makeBurger2(wg)
}
func makeBurger2(wg *sync.WaitGroup) {
s_sleep(5000)
fmt.Println("\nCooked a burger!", t_time())
wg.Done()
}
//cook drink
func pourDrink1(wg *sync.WaitGroup) {
s_sleep(1000)
fmt.Println("\nPutting ice in cup...", t_time())
go pourDrink2(wg)
}
func pourDrink2(wg *sync.WaitGroup) {
s_sleep(3000)
fmt.Println("\nPouring soda in cup...", t_time())
go pourDrink3(wg)
}
func pourDrink3(wg *sync.WaitGroup) {
s_sleep(2500)
fmt.Println("\nDrink poured!", t_time())
wg.Done()
}
//wipe table
func cleanTable1(wg *sync.WaitGroup) {
s_sleep(1000)
fmt.Println("\n'Cleaning' table....", t_time())
go cleanTable2(wg)
}
func cleanTable2(wg *sync.WaitGroup) {
s_sleep(1500)
fmt.Println("\nTable 'clean'!", t_time())
wg.Done()
}
//delay
func s_sleep(x int) { time.Sleep(time.Duration(x) * time.Millisecond) }
//just to print time
func t_time() string {
return time.Now().Format("15:04:05")
}
//create array of tasks to complete
var McDolansTasks = []func(*sync.WaitGroup){
takeOrder1, makeFries1, makeBurger1, pourDrink1, cleanTable1}
//main function
func main() {
var waitGroup sync.WaitGroup
// Set number of effective goroutines we want to wait upon
waitGroup.Add(len(McDolansTasks))
for _, task := range McDolansTasks {
// Pass reference to WaitGroup instance
// Each of the tasks should call on WaitGroup.Done()
go task(&waitGroup)
}
// Wait until all goroutines have completed execution.
waitGroup.Wait()
println("\nClock out for the day!")
}
答案1
得分: 4
简短回答是你无法展示并发性和并行性,因为它们可能并不是并发和并行的。例如,如果你只有一个执行器,在内部的Go运行时模型中只会同时运行一个goroutine。
关于是否在单独的线程上运行,似乎存在很多混淆。首先,Go并没有以“线程”为基础来定义,所以这个问题无法回答。除非你添加限定词,比如“在我的系统上”,否则你只能得到这个回答。所以也许你想要添加限定词“在我的系统上”。
规范允许goroutine并行运行,并定义了Go的并发模型,但并不要求任何特定的实现。这使得Go的实现者可以自由选择适合的实现方式。我上面提到了“一个内部的Go运行时模型”,但没有说具体是哪一个。我指的是“在你的系统上的那个”。
目前有一个主要的Go实现,尽管有许多版本的该实现,以满足不同的CPU和Go版本需求。这个实现有一个运行时(可以在runtime
包中调用一些控制它的函数),并且该运行时确实有一个内部模型。在这个内部模型中,系统(操作系统和/或其他底层Go实现)确实有线程。这些系统线程在内部被称为“M”:假设操作系统在运行时启动一个线程m0
,然后在适当的时候运行时会创建新的额外的操作系统线程。
Povilas Versockas在这里有一篇(现在稍微过时了)关于Go运行时调度的文章。https://povilasv.me/go-scheduler/ https://stackoverflow.com/q/68312082/1256452的答案描述了一些其他特性。
请注意,术语“进程”和“线程”已经相当模糊,尽管对于它们的区别有一些共识。参见https://stackoverflow.com/q/200469/1256452。goroutine非常类似于线程,但是大多数现代线程系统会为每个线程分配一个“线程ID”。goroutine特别没有ID,这意味着没有办法从任何其他goroutine有意义地与特定的goroutine交流或讨论。这有助于强制Go程序员使用通道进行通信。(它不能完全强制,但确实有帮助。)
要真正展示并行操作,你可以超越Go规范,进入未定义行为的领域。每当行为在Go规范中是未定义的时候,它可能由其他东西定义,并且这个其他东西可能允许你展示并行性。不幸的是,因为你已经超越了Go规范,你现在首先不再编写(可移植的)Go程序:你的演示只是展示了关于特定实现的一些东西。
你还可以使用调试器,比如delve。调试器通常必须以各种不太明确定义的方式与运行时和操作系统进行交互,以便能够显示程序中正在发生的情况,并通过这样做,它们往往显示出各种未由规范定义但实际上是这样工作的行为。除非它们由操作系统提供,否则你对这些行为没有任何保证。但你肯定会看到很多东西!
英文:
The short answer is that you can't show concurrency and parallelism, because maybe they aren't (concurrent and parallel). For instance, if you have just one executor—one "P", in an internal Go runtime model—you will only be running one goroutine at a time.
> ... there seems to be a lot of confusion on whether these run on separate threads ...
Go is not defined in terms of "threads" in the first place, so the question can't be answered. That's not very satisfactory, but unless you add qualifiers such as "on my system today", that is all you get! So perhaps you want to add the qualifier on my system today.
The spec allows goroutines to run in parallel, and defines Go's concurrency model, but does not require any particular implementation. This leaves a Go implementor free to use whatever seems suitable. I mentioned "an internal Go runtime model" above, but didn't say which one. I meant the one on your system today.
There's currently one main implementation of Go, albeit with many versions of that implementation, as needed for different CPUs and releases of Go. That one implementation has a runtime (with some things you can invoke in the runtime
package that control it to some extent), and that runtime does have an internal model. In that internal model, the system—the OS and/or other implementation that lies below the Go implementation—does have threads. These system threads are called "M" internally: the OS is assumed to start the runtime with one thread, m0
, after which the runtime will create new additional OS threads when that seems appropriate.
Povilas Versockas has a (now slightly out of date) write-up on scheduling in the Go runtime here. The answer to https://stackoverflow.com/q/68312082/1256452 describes a few other features.
Note that the terms process and thread are already rather vague, though there's some general agreement on the distinctions. See https://stackoverflow.com/q/200469/1256452 A goroutine is very much like a thread, but in general, most modern thread systems assign a "thread ID" to each thread. Goroutines specifically do not have IDs, which means there is no way to talk meaningfully to, or about, a specific goroutine from any other goroutine. This helps force the Go programmer to use channels for communication. (It can't completely force this, but it sure helps. 😀)
To truly demonstrate parallel operation, you can reach beyond the Go specification into the areas of undefined behavior. Whenever the behavior is undefined by the Go spec, it may be defined by something else, and that something-else might then allow you to demonstrate parallelism. Unfortunately, because you've gone beyond the Go spec, you're now no longer writing a (portable) Go program in the first place: your demo simply demonstrates something about the particular implementation.
You can also use debuggers, such as delve. Debuggers generally have to interact with the runtime and OS in various poorly-defined ways just to be able to show you what is going on in your program, and by so doing, they tend to show all kinds of not-defined-by-the-specification but-actually-works-like-this behaviors. You don't get any guarantees about those, unless they're provided by (e.g.) your OS. But you definitely see a lot of stuff!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论