Home > Back-end >  How can I properly demonstrate concurrency AND parallelism in Go/Golang?
How can I properly demonstrate concurrency AND parallelism in Go/Golang?

Time:11-15

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!")
}

(This is the output

CodePudding user response:

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 When will Go scheduler create a new M and P? 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 What is the difference between a process and a thread? 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.

  • Related