How do goroutines work? (or: goroutines and OS threads relation)

huangapple go评论151阅读模式
英文:

How do goroutines work? (or: goroutines and OS threads relation)

问题

在使用GOMAXPROCS=1时,其他goroutine如何在调用系统调用时继续执行?
据我所知,当调用系统调用时,线程会放弃控制权,直到系统调用返回。
Go语言如何在不为每个阻塞在系统调用上的goroutine创建系统线程的情况下实现并发?

根据文档

Goroutines(协程)

它们被称为goroutines,因为现有的术语(线程、协程、进程等)传达了不准确的内涵。goroutine具有简单的模型:它是在同一地址空间中与其他goroutine并发执行的函数。它是轻量级的,几乎只需要分配堆栈空间。堆栈的初始大小很小,所以它们很廉价,并且通过根据需要分配(和释放)堆存储来增长。

Goroutine被复用到多个操作系统线程上,因此如果一个goroutine阻塞,例如在等待I/O时,其他goroutine将继续运行。它们的设计隐藏了许多线程创建和管理的复杂性。

英文:

How can other goroutines keep executing whilst invoking a syscall? (when using GOMAXPROCS=1)
As far as I'm aware of, when invoking a syscall the thread gives up control until the syscall returns.
How can Go achieve this concurrency without creating a system thread per blocking-on-syscall goroutine?

From the documentation:

> Goroutines
>
> They're called goroutines because the existing terms—threads,
> coroutines, processes, and so on—convey inaccurate connotations. A
> goroutine has a simple model: it is a function executing concurrently
> with other goroutines in the same address space. It is lightweight,
> costing little more than the allocation of stack space. And the stacks
> start small, so they are cheap, and grow by allocating (and freeing)
> heap storage as required.
>
> Goroutines are multiplexed onto multiple OS threads so if one should
> block, such as while waiting for I/O, others continue to run. Their
> design hides many of the complexities of thread creation and
> management.

答案1

得分: 53

如果一个 goroutine 正在阻塞,运行时将会启动一个新的操作系统线程来处理其他的 goroutine,直到阻塞的 goroutine 停止阻塞。

参考链接:https://groups.google.com/forum/#!topic/golang-nuts/2IdA34yR8gQ

英文:

If a goroutine is blocking, the runtime will start a new OS thread to handle the other goroutines until the blocking one stops blocking.

Reference : https://groups.google.com/forum/#!topic/golang-nuts/2IdA34yR8gQ

答案2

得分: 35

好的,下面是我学到的内容:

当你使用原始系统调用时,Go确实为每个阻塞的goroutine创建一个线程。例如,考虑以下代码:

package main

import (
    "fmt"
    "syscall"
)

func block(c chan bool) {
    fmt.Println("block() enter")
    buf := make([]byte, 1024)
    _, _ = syscall.Read(0, buf) // 在标准输入上进行无缓冲读取时阻塞
    fmt.Println("block() exit")
    c <- true // 告诉main()我们完成了
}

func main() {
    c := make(chan bool)
    for i := 0; i < 1000; i++ {
        go block(c)
    }
    for i := 0; i < 1000; i++ {
        _ = <-c
    }
}

在运行该代码时,Ubuntu 12.04 报告该进程有1004个线程。

另一方面,当使用Go的HTTP服务器并向其打开1000个套接字时,只创建了4个操作系统线程:

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

因此,这是一个IOLoop和每个阻塞系统调用一个线程的混合体。

英文:

Ok, so here's what I've learned:
When you're doing raw syscalls, Go indeed creates a thread per blocking goroutine. For example, consider the following code:

package main

import (
        &quot;fmt&quot;
        &quot;syscall&quot;
)

func block(c chan bool) {
        fmt.Println(&quot;block() enter&quot;)
        buf := make([]byte, 1024)
        _, _ = syscall.Read(0, buf) // block on doing an unbuffered read on STDIN
        fmt.Println(&quot;block() exit&quot;)
        c &lt;- true // main() we&#39;re done
}

func main() {
        c := make(chan bool)
        for i := 0; i &lt; 1000; i++ {
                go block(c)
        }
        for i := 0; i &lt; 1000; i++ {
                _ = &lt;-c
        }
}

When running it, Ubuntu 12.04 reported 1004 threads for that process.

On the other hand, when utilizing Go's HTTP server and opening 1000 sockets to it, only 4 operating system threads were created:

package main

import (
        &quot;fmt&quot;
        &quot;net/http&quot;
)

func handler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, &quot;Hi there, I love %s!&quot;, r.URL.Path[1:])
}

func main() {
        http.HandleFunc(&quot;/&quot;, handler)
        http.ListenAndServe(&quot;:8080&quot;, nil)
}

So, it's a mix between an IOLoop and a thread per blocking system call.

答案3

得分: 23

它不能。当GOMAXPROCS=1时,只能同时运行一个goroutine,无论这个goroutine是在执行系统调用还是其他操作。

然而,大多数阻塞的系统调用(如套接字I/O、等待计时器)在Go中执行时并不会被阻塞在系统调用上。它们由Go运行时通过epoll、kqueue或类似的操作系统提供的多路复用I/O机制进行复用。

对于无法像epoll那样进行多路复用的其他类型的阻塞系统调用,Go会生成一个新的操作系统线程,而不管GOMAXPROCS的设置(尽管这是Go 1.1的状态,我不确定情况是否有所改变)。

英文:

It can't. There's only 1 goroutine that can be running at a time when GOMAXPROCS=1, whether that one goroutine is doing a system call or something else.

However, most blocking system calls, such as socket I/O, waiting for a timer are not blocked on a system call when performed from Go. They're multiplexed by the Go runtime onto epoll, kqueue or similar facilities the OS provides for multiplexing I/O.

For other kinds of blocking system calls that cannot be multiplexed with something like epoll, Go does spawn a new OS thread, regardless of the GOMAXPROCS setting (albeit that was the state in Go 1.1, I'm not sure if the situation is changed)

答案4

得分: 6

你必须区分处理器编号和线程编号:你可以拥有比物理处理器更多的线程,因此多线程进程仍然可以在单核处理器上执行。

正如你引用的文档所解释的那样,goroutine并不是一个线程:它只是在专用的一块堆栈空间中执行的函数。如果你的进程有多个线程,这个函数可以由任何一个线程执行。因此,一个由于某种原因(系统调用、I/O、同步)而阻塞的goroutine可以在它的线程中继续执行,而其他的goroutine可以由另一个线程执行。

英文:

You have to differentiate processor number and thread number: you can have more threads than physical processors, so a multi-thread process can still execute on a single core processor.

As the documentation you quoted explain, a goroutine isn't a thread: it's merely a function executed in a thread that is dedicated a chunk of stack space. If your process have more than one thread, this function can be executed by either thread. So a goroutine that is blocking for a reason or another (syscall, I/O, synchronization) can be let in its thread while other routines can be executed by another.

答案5

得分: 5

我已经写了一篇文章,用例子和图表解释了它们的工作原理。请随意在这里查看:https://osmh.dev/posts/goroutines-under-the-hood

英文:

I’ve written an article explaining how they work with examples and diagrams. Please feel free to take a look at it here: https://osmh.dev/posts/goroutines-under-the-hood

答案6

得分: 2

根据runtime文档的说明:

GOMAXPROCS变量限制了可以同时执行用户级Go代码的操作系统线程的数量。在代表Go代码进行系统调用时,被阻塞的线程数量没有限制;它们不计入GOMAXPROCS的限制。

因此,当一个操作系统线程因为系统调用而被阻塞时,可以启动另一个线程,并且仍然满足GOMAXPROCS = 1。这两个线程不会同时运行。

英文:

From documentation of runtime:

> The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.

so when one OS thread is blocked for syscall, another thread can be started - and GOMAXPROCS = 1 is still satisfied. The two threads are NOT running simultaneously.

答案7

得分: 0

希望这个求和数字的例子对你有所帮助。

package main

import "fmt"

func put(number chan<- int, count int) {
    i := 0
    for ; i <= (5 * count); i++ {
        number <- i
    }
    number <- -1
}

func subs(number chan<- int) {
    i := 10
    for ; i <= 19; i++ {
        number <- i
    }
}

func main() {
    channel1 := make(chan int)
    channel2 := make(chan int)
    done := 0
    sum := 0

    //go subs(channel2)
    go put(channel1, 1)
    go put(channel1, 2)
    go put(channel1, 3)
    go put(channel1, 4)
    go put(channel1, 5)

    for done != 5 {
        select {
        case elem := <-channel1:
            if elem < 0 {
                done++
            } else {
                sum += elem
                fmt.Println(sum)
            }
        case sub := <-channel2:
            sum -= sub
            fmt.Printf("atimta : %d\n", sub)
            fmt.Println(sum)
        }
    }
    close(channel1)
    close(channel2)
}

希望对你有所帮助!

英文:

Hope this example of sum numbers helps you.

package main

import &quot;fmt&quot;

func put(number chan&lt;- int, count int) {
	i := 0
	for ; i &lt;= (5 * count); i++ {
		number &lt;- i
	}
	number &lt;- -1
}

func subs(number chan&lt;- int) {
	i := 10
	for ; i &lt;= 19; i++ {
		number &lt;- i
	}
}

func main() {
	channel1 := make(chan int)
	channel2 := make(chan int)
	done := 0
	sum := 0

	//go subs(channel2)
	go put(channel1, 1)
	go put(channel1, 2)
	go put(channel1, 3)
	go put(channel1, 4)
	go put(channel1, 5)

	for done != 5 {
		select {
		case elem := &lt;-channel1:
			if elem &lt; 0 {
				done++
			} else {
				sum += elem
				fmt.Println(sum)
			}
		case sub := &lt;-channel2:
			sum -= sub
			fmt.Printf(&quot;atimta : %d\n&quot;, sub)
			fmt.Println(sum)
		}
	}
	close(channel1)
	close(channel2)
}

huangapple
  • 本文由 发表于 2014年7月7日 03:40:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/24599645.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定