← 中文资源

简体中文版本

协作式与抢占式调度

协作式调度只在任务让渡时切换——廉价可预期,但一个自私任务拖垮全场;抢占式调度强制切换——健壮但更贵,还需时间片调优。

阅读英文原文

概念定义

问题在于“谁决定任务何时停下”。任务自己决定:协作式——切换发生在让点,廉价且总在安全点。调度器决定:抢占式——按时间片与中断切换,无人能独占 CPU,代价是开销与无处不在的交错复杂度。

工作机制

协作式运行时在 await 边界、channel 操作、调度调用处插入让点,让点之间的计算不可触碰。抢占式内核经定时器中断切换,靠硬件支持完成完整上下文切换。混合式在粗粒度边界抢占——Go 在函数入口与循环回边抢占,平衡延迟保证与切换成本。

使用场景

海量任务、天然频繁挂起的管线选协作式;来源不明或对抗性代码与 CPU 并行选抢占式;现代服务器平台的主流正是混合运行时。

常见误区

协作式的活性取决于每个参与者:一个原生阻塞调用或紧凑循环就让世界停摆,看门狗线程才是检测手段。抢占式并不“让程序正确”,它让活性健壮,正确性仍靠同步,而抢占还成倍增加需要测试的交错。高频细粒度任务误用抢占线程会陷入上下文切换主导;协作停顿要测让点间隙而不是 CPU 总量。

代码示例

package main

import (
	"fmt"
	"sync"
	"time"
)

type Debouncer struct {
	mu    sync.Mutex
	delay time.Duration
	timer *time.Timer
}

func NewDebouncer(delay time.Duration) *Debouncer {
	return &Debouncer{delay: delay}
}

func (d *Debouncer) Do(fn func()) {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.timer != nil {
		d.timer.Stop()
	}
	d.timer = time.AfterFunc(d.delay, fn)
}

func (d *Debouncer) Stop() {
	d.mu.Lock()
	defer d.mu.Unlock()
	if d.timer != nil {
		d.timer.Stop()
	}
}

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	debounce := NewDebouncer(50 * time.Millisecond)
	start := time.Now()
	for index := 0; index < 5; index++ {
		debounce.Do(func() {
			fmt.Println("fired after", time.Since(start).Round(time.Millisecond))
			wg.Done()
		})
		time.Sleep(10 * time.Millisecond)
	}
	wg.Wait()
}
package main

import (
	"context"
	"fmt"
	"net/http"

	"golang.org/x/sync/errgroup"
)

type Result struct {
	URL  string
	Code int
}

func fetchAll(ctx context.Context, urls []string) ([]Result, error) {
	g, ctx := errgroup.WithContext(ctx)
	results := make([]Result, len(urls))

	for i, url := range urls {
		i, url := i, url // 捕获循环变量(Go 1.22 前必需)
		g.Go(func() error {
			req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
			if err != nil {
				return fmt.Errorf("build request %s: %w", url, err)
			}
			resp, err := http.DefaultClient.Do(req)
			if err != nil {
				return fmt.Errorf("fetch %s: %w", url, err)
			}
			defer resp.Body.Close()
			results[i] = Result{URL: url, Code: resp.StatusCode}
			return nil
		})
	}

	if err := g.Wait(); err != nil {
		return nil, err // ctx 已取消,其余 goroutine 全部提前退出
	}
	return results, nil
}

func main() {
	rs, err := fetchAll(
		context.Background(),
		[]string{"https://example.com/a", "https://example.com/b"},
	)
	fmt.Println(len(rs), err)
}