Почему это не создает много потоков, когда многие goroutines заблокированы в записи файла в golang?
Как мы знаем в go, поток может быть создан, когда программа должна выполнить блокирующий вызов, такой как системный вызов или вызов библиотеки C через cgo. Некоторый тестовый код:
package main
import (
"io/ioutil"
"os"
"runtime"
"strconv"
)
func main() {
runtime.GOMAXPROCS(2)
data, err := ioutil.ReadFile("./55555.log")
if err != nil {
println(err)
return
}
for i := 0; i < 200; i++ {
go func(n int) {
for {
err := ioutil.WriteFile("testxxx"+strconv.Itoa(n), []byte(data), os.ModePerm)
if err != nil {
println(err)
break
}
}
}(i)
}
select {}
}
Когда я запускал его, он не создавал много потоков.
➜ =99=[root /root]$ cat /proc/9616/status | grep -i thread
Threads: 5
Есть идеи?
3 ответа
Я немного изменил вашу программу, чтобы вывести гораздо больший блок
package main
import (
"io/ioutil"
"os"
"runtime"
"strconv"
)
func main() {
runtime.GOMAXPROCS(2)
data := make([]byte, 128*1024*1024)
for i := 0; i < 200; i++ {
go func(n int) {
for {
err := ioutil.WriteFile("testxxx"+strconv.Itoa(n), []byte(data), os.ModePerm)
if err != nil {
println(err)
break
}
}
}(i)
}
select {}
}
Это тогда показывает>200 потоков, как вы ожидали
$ cat /proc/17033/status | grep -i thread
Threads: 203
Поэтому я думаю, что системные вызовы выходили слишком быстро в исходном тесте, чтобы показать ожидаемый эффект.
Goroutine - это легкий поток, он не эквивалентен потоку операционной системы. Спецификация языка определяет его как "независимый параллельный поток управления в одном и том же адресном пространстве".
Цитирование из документации пакета runtime
:
Переменная GOMAXPROCS ограничивает количество потоков операционной системы, которые могут одновременно выполнять код Go на уровне пользователя. Нет ограничений на количество потоков, которые могут быть заблокированы в системных вызовах от имени кода Go; те не считаются с лимитом GOMAXPROCS.
То, что вы запускаете 200 процедур, не означает, что для них будет запущено 200 потоков. Ты устанавливаешь GOMAXPROCS
2, что означает, что одновременно могут выполняться 2 "активные" программы. Новые потоки могут быть порождены, если программа заблокирована (например, ожидание ввода / вывода). Вы не упомянули, насколько велик ваш тестовый файл. Запущенные вами подпрограммы могут закончить его написание слишком быстро.
Статья в блоге Effective Go определяет их как:
Их называют goroutines, потому что существующие термины - потоки, сопрограммы, процессы и т. Д. - дают неточные коннотации. Goroutine имеет простую модель: это функция, выполняемая одновременно с другими goroutines в том же адресном пространстве. Он легкий и стоит немного больше, чем выделение стекового пространства. И стеки начинаются с малого, поэтому они дешевы и растут, выделяя (и освобождая) хранилище кучи по мере необходимости.
Городуны мультиплексируются в несколько потоков ОС, поэтому, если один из них блокируется, например, во время ожидания ввода-вывода, другие продолжают работать. Их дизайн скрывает многие сложности создания потоков и управления ими.
В выпуске 4056 обсуждается, как ограничить количество создаваемых фактических потоков (не goroutine).
В Go 1.2 введено управление потоками в коммите 665feee.
Вы можете увидеть тест, чтобы проверить, действительно ли достигнуто количество созданных потоков в pkg/runtime/crash_test.go#L128-L134
:
func TestThreadExhaustion(t *testing.T) {
output := executeTest(t, threadExhaustionSource, nil)
want := "runtime: program exceeds 10-thread limit\nfatal error: thread exhaustion"
if !strings.HasPrefix(output, want) {
t.Fatalf("output does not start with %q:\n%s", want, output)
}
}
В этом же файле есть пример для создания фактического потока (для данной программы), используя runtime.LockOSThread()
:
func testInNewThread(name string) {
c := make(chan bool)
go func() {
runtime.LockOSThread()
test(name)
c <- true
}()
<-c
}