Apa sebenarnya yang dilakukan runtime.Gosched?

86

Dalam versi sebelum rilis go 1.5 dari situs web Tour of Go , ada potongan kode yang terlihat seperti ini.

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

Outputnya terlihat seperti ini:

hello
world
hello
world
hello
world
hello
world
hello

Yang mengganggu saya adalah ketika runtime.Gosched()dihapus, program tidak lagi mencetak "dunia".

hello
hello
hello
hello
hello

Mengapa demikian? Bagaimana runtime.Gosched()pengaruhnya terhadap eksekusi?

Jason Yeo
sumber

Jawaban:

143

catatan:

Mulai Go 1.5, GOMAXPROCS disetel ke jumlah inti perangkat keras: golang.org/doc/go1.5#runtime , di bawah jawaban asli sebelum 1.5.


Saat Anda menjalankan program Go tanpa menentukan variabel lingkungan GOMAXPROCS, goroutine Go dijadwalkan untuk dieksekusi di thread OS tunggal. Namun, untuk membuat program tampak multithread (itulah gunanya goroutine, bukan?), Penjadwal Go terkadang harus mengganti konteks eksekusi, sehingga setiap goroutine dapat melakukan tugasnya.

Seperti yang saya katakan, ketika variabel GOMAXPROCS tidak ditentukan, runtime Go hanya diperbolehkan untuk menggunakan satu utas, jadi tidak mungkin untuk mengganti konteks eksekusi saat goroutine melakukan beberapa pekerjaan konvensional, seperti komputasi atau bahkan IO (yang dipetakan ke fungsi C biasa ). Konteks dapat dialihkan hanya ketika Go concurrency primitif digunakan, misalnya ketika Anda mengaktifkan beberapa chans, atau (ini kasus Anda) ketika Anda secara eksplisit memberi tahu penjadwal untuk mengganti konteks - runtime.Goscheduntuk itulah.

Jadi, singkatnya, ketika konteks eksekusi dalam satu goroutine mencapai Goschedpanggilan, penjadwal diinstruksikan untuk mengalihkan eksekusi ke goroutine lain. Dalam kasus Anda, ada dua goroutine, main (yang mewakili utas 'main' dari program) dan tambahan, yang telah Anda buat go say. Jika Anda menghapus Goschedpanggilan, konteks eksekusi tidak akan pernah ditransfer dari goroutine pertama ke goroutine kedua, karenanya tidak ada 'dunia' untuk Anda. Saat Goschedada, penjadwal mentransfer eksekusi pada setiap iterasi loop dari goroutine pertama ke yang kedua dan sebaliknya, jadi Anda memiliki 'hello' dan 'world' interleaved.

FYI, ini disebut 'multitasking kooperatif': goroutine harus secara eksplisit menghasilkan kontrol ke goroutine lain. Pendekatan yang digunakan di sebagian besar OS kontemporer disebut 'multitasking preemptive': thread eksekusi tidak peduli dengan transfer kontrol; penjadwal mengalihkan konteks eksekusi secara transparan kepada mereka. Pendekatan kooperatif sering digunakan untuk mengimplementasikan 'utas hijau', yaitu coroutine konkuren logis yang tidak memetakan 1: 1 ke utas OS - ini adalah cara runtime Go dan penerapan goroutine-nya.

Memperbarui

Saya telah menyebutkan variabel lingkungan GOMAXPROCS tetapi tidak menjelaskan apa itu. Saatnya memperbaiki ini.

Jika variabel ini disetel ke angka positif N, waktu proses Go akan dapat membuat hingga Nutas asli, di mana semua utas hijau akan dijadwalkan. Utas asli sejenis utas yang dibuat oleh sistem operasi (utas Windows, pthreads, dll.). Ini berarti bahwa jika Nlebih besar dari 1, mungkin goroutine akan dijadwalkan untuk dijalankan di utas asli yang berbeda dan, akibatnya, berjalan secara paralel (setidaknya, sesuai dengan kemampuan komputer Anda: jika sistem Anda didasarkan pada prosesor multi inti, itu kemungkinan utas ini akan benar-benar paralel; jika prosesor Anda memiliki inti tunggal, maka multitasking preemptive yang diterapkan di utas OS akan membuat visibilitas eksekusi paralel).

Dimungkinkan untuk menyetel variabel GOMAXPROCS menggunakan runtime.GOMAXPROCS()fungsi alih-alih menyetel sebelumnya variabel lingkungan. Gunakan sesuatu seperti ini di program Anda, bukan yang saat ini main:

func main() {
    runtime.GOMAXPROCS(2)
    go say("world")
    say("hello")
}

Dalam hal ini Anda dapat mengamati hasil yang menarik. Ada kemungkinan bahwa Anda akan mendapatkan garis 'halo' dan 'dunia' tercetak berselang-seling tidak merata, mis

hello
hello
world
hello
world
world
...

Ini bisa terjadi jika goroutine dijadwalkan untuk memisahkan utas OS. Ini sebenarnya adalah cara kerja multitasking preemptive (atau pemrosesan paralel dalam kasus sistem multicore): utas paralel, dan output gabungannya tidak pasti. BTW, Anda dapat meninggalkan atau menghapus Goschedpanggilan, sepertinya tidak akan berpengaruh jika GOMAXPROCS lebih besar dari 1.

Berikut ini adalah apa yang saya dapatkan dari beberapa program dengan runtime.GOMAXPROCSpanggilan.

hyperplex /tmp % go run test.go
hello
hello
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world
hyperplex /tmp % go run test.go
hello
hello
hello
hello
hello
hyperplex /tmp % go run test.go
hello
world
hello
world
hello
world
hello
world
hello
world

Lihat, terkadang keluarannya bagus, terkadang tidak. Indeterminisme beraksi :)

Pembaruan lainnya

Sepertinya, dalam versi terbaru kompilator Go, waktu proses Go memaksa goroutine untuk menghasilkan tidak hanya penggunaan primitif konkurensi, tetapi juga pada panggilan sistem OS. Ini berarti bahwa konteks eksekusi dapat dialihkan antar goroutine juga pada pemanggilan fungsi IO. Akibatnya, di kompiler Go baru-baru ini, dimungkinkan untuk mengamati perilaku tak tentu bahkan ketika GOMAXPROCS tidak disetel atau disetel ke 1.

Vladimir Matveev
sumber
Kerja bagus ! Tetapi saya tidak menemukan masalah ini pada versi 1.0.3, aneh.
WoooHaaaa
1
Ini benar. Saya baru saja memeriksa ini dengan go 1.0.3, dan ya, perilaku ini tidak muncul: bahkan dengan GOMAXPROCS == 1 program bekerja seolah-olah GOMAXPROCS> = 2. Tampaknya di 1.0.3 penjadwal telah diubah.
Vladimir Matveev
Saya pikir hal-hal telah berubah wrt go 1.4 compiler. Contoh dalam pertanyaan OP tampaknya membuat utas OS sementara ini (-> gobyexample.com/atomic-counters ) tampaknya membuat penjadwalan kooperatif. Harap perbarui jawaban jika ini benar
tez
8
Mulai Go 1.5, GOMAXPROCS disetel ke jumlah inti perangkat keras: golang.org/doc/go1.5#runtime
thepanuto
1
@paulkon, Gosched()diperlukan atau tidaknya tergantung pada program Anda, tidak tergantung pada GOMAXPROCSnilai. Efisiensi multitasking preemptive di atas koperasi juga tergantung pada program Anda. Jika program Anda terikat I / O, maka multitasking kooperatif dengan I / O asinkron mungkin akan lebih efisien (yaitu memiliki lebih banyak throughput) daripada I / O berbasis thread sinkron; jika program Anda terikat dengan CPU (mis. komputasi panjang), maka multitasking kooperatif akan menjadi kurang berguna.
Vladimir Matveev
8

Penjadwalan kerjasama adalah pelakunya. Tanpa menghasilkan, goroutine lain (katakanlah "dunia") mungkin secara legal mendapatkan kesempatan nol untuk mengeksekusi sebelum / ketika main berakhir, yang menurut spesifikasi menghentikan semua gorutine - mis. seluruh proses.

zzzz
sumber
1
oke, jadi runtime.Gosched()hasilnya. Apa artinya? Ini menghasilkan kontrol kembali ke fungsi utama?
Jason Yeo
5
Dalam kasus khusus ini ya. Umumnya ini meminta penjadwal untuk memulai dan menjalankan salah satu goroutine "siap" dalam urutan pemilihan yang sengaja tidak ditentukan.
zzzz