Не могу понять, о чём говорится в книге по Go
Фрагмент:
A goroutine is a task, whereas everything after the calling statement of a goroutine is a continuation. In the work-stealing strategy used by the Go scheduler, a (logical) processor that is underutilized looks for additional work from other processors. When it finds such jobs, it steals them from the other processor or processors, hence the name. Additionally, the work-stealing algorithm of Go queues and steals continuations. A stalling join, as is suggested by its name, is a point where a thread of execution stalls at a join and starts looking for other work to do. Although both task stealing and continuation stealing have stalling joins, continuations happen more often than tasks; therefore, the Go scheduling algorithm works with continuations rather than tasks. The main disadvantage of continuation stealing is that it requires extra work from the compiler of the programming language. Fortunately, Go provides that extra help and therefore uses continuation stealing in its work-stealing algorithm. One of the benefits of continuation stealing is that you get the same results when using function calls instead of goroutines or a single thread with multiple goroutines. This makes perfect sense, as only one thing is executed at any given point in both cases.
Так вот, не могу понять, что значит continuation? Чем он отличается от таска? Чем task stealing отличается от continuation stealing? Статью на вики читал, не особо помогло
Ответы (1 шт):
Разница между task stealing и continuation stealing в том, что является единицей управления планировщика.
В традиционной модели многопоточности вызов параллельной функции выполняется в отдельном потоке, а при одноядерном выполнении только после завершения порождающей функции, так как планировщики уровня task не умеют переключать контексты посреди выполняющейся функции / задачи. Такой планировщик зависит от планировщика ядра, который вбрасывает прерывание и переключает контексты потоков.
Планировщик continuation знает, как устроены функци и умеет переключать выполнение посреди функций на одном ядре без участия прерывания от ядра. Пример такого планировщика - планировщих async в Javascript.
Немного модифицируем пример из "A Tour of Go"
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go say("world")
fmt.Println("hello")
time.Sleep(600 * time.Millisecond)
fmt.Println("!")
}
Внутри main инструкция go say("world") порождает две сущности: task say("world") и continuation (продолжение выполнения) функции main - выполнение последовательности инструкций
fmt.Println("hello")
time.Sleep(600 * time.Millisecond)
fmt.Println("!")
Этим планировщик Go отличается от OpenMP и pthreads, которые будут без вопросов продолжать выполнение main, а say("world") будет выполняться на другом ядре / в другом потоке.
В результате при выполнении кода из примера сначала будет напечатано слово hello, так как fmt.Println("hello") принадлежит continuation, которое выполняется первым, а затем будет напечатано world, так как continuation заблокируется в таймере time.Sleep:
hello
world
world
world
world
world
!
На самом деле в многоядерном случае ситуация будет сложнее, так как одно из стоящих в ожидании ядер начнет выполнять горутину, и world может оказаться напечатанным раньше чем hello, но вероятность такого события мала. В случае же одноядерного выполнения ответ гарантирован: сначала будет продолжаться выполнение main, и только когда continuation застопорится в time.Sleep планировщик передаст на выполнение горутину.
Переключение в time.Sleep с выполнения main на выполнение say("world") - это то же самое, что делают в Javascript асинхронные функции. Когда текущая функция прерывается, стек переключается на другую асинхронную функцию без прерывания от ОС. Не случайно в цитате из вашего вопроса поминается компилятор. Он должен подсказать, что time.Sleep заблокировалась и сформировать условие ожидания разблокировки (в данном случае завести таймер).