From bfca30ea0379c6111f2285a5a85826dbe82ba422 Mon Sep 17 00:00:00 2001 From: Changkun Ou Date: Thu, 21 Jan 2021 18:48:22 +0100 Subject: [PATCH] content: add example of zero-cost-thread-sched --- .../zero-cost-thread-sched/app/window.go | 67 ++++++++++++++ content/assets/zero-cost-thread-sched/go.mod | 8 ++ content/assets/zero-cost-thread-sched/go.sum | 4 + content/assets/zero-cost-thread-sched/main.go | 61 +++++++++++++ .../mainthread/mainthread.go | 80 +++++++++++++++++ .../mainthread/mainthread_test.go | 57 ++++++++++++ .../zero-cost-thread-sched/thread/thread.go | 89 +++++++++++++++++++ .../thread/thread_test.go | 75 ++++++++++++++++ 8 files changed, 441 insertions(+) create mode 100644 content/assets/zero-cost-thread-sched/app/window.go create mode 100644 content/assets/zero-cost-thread-sched/go.mod create mode 100644 content/assets/zero-cost-thread-sched/go.sum create mode 100644 content/assets/zero-cost-thread-sched/main.go create mode 100644 content/assets/zero-cost-thread-sched/mainthread/mainthread.go create mode 100644 content/assets/zero-cost-thread-sched/mainthread/mainthread_test.go create mode 100644 content/assets/zero-cost-thread-sched/thread/thread.go create mode 100644 content/assets/zero-cost-thread-sched/thread/thread_test.go diff --git a/content/assets/zero-cost-thread-sched/app/window.go b/content/assets/zero-cost-thread-sched/app/window.go new file mode 100644 index 0000000..c2da39d --- /dev/null +++ b/content/assets/zero-cost-thread-sched/app/window.go @@ -0,0 +1,67 @@ +// Copyright (c) 2021 The golang.design Initiative Authors. +// All rights reserved. +// +// The code below is produced by Changkun Ou . + +package app + +import ( + "x/mainthread" + + "github.com/go-gl/glfw/v3.3/glfw" +) + +// Win is a window. +type Win struct { + win *glfw.Window +} + +// NewWindow constructs a new graphical window. +func NewWindow() (*Win, error) { + var ( + w = &Win{} + err error + ) + mainthread.Call(func() { + err = glfw.Init() + if err != nil { + return + } + + w.win, err = glfw.CreateWindow(640, 480, "win", nil, nil) + if err != nil { + return + } + }) + return w, nil +} + +// Terminate terminates the entire application. +func Terminate() { + mainthread.Call(func() { + glfw.Terminate() + }) +} + +// Closed asserts if the given window is closed. +func (w *Win) Closed() (stop bool) { + return mainthread.CallV(func() interface{} { + return w.win.ShouldClose() + }).(bool) +} + +// Update updates the frame buffer of the given window. +func (w *Win) Update() { + mainthread.Call(func() { + w.win.SwapBuffers() + // glfw.WaitEventsTimeout(1.0 / 30) + glfw.PollEvents() + }) +} + +// Stop stops the given window. +func (w *Win) Stop() { + mainthread.Call(func() { + w.win.SetShouldClose(true) + }) +} diff --git a/content/assets/zero-cost-thread-sched/go.mod b/content/assets/zero-cost-thread-sched/go.mod new file mode 100644 index 0000000..95858f5 --- /dev/null +++ b/content/assets/zero-cost-thread-sched/go.mod @@ -0,0 +1,8 @@ +module x + +go 1.16 + +require ( + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20201108214237-06ea97f0c265 + golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 +) diff --git a/content/assets/zero-cost-thread-sched/go.sum b/content/assets/zero-cost-thread-sched/go.sum new file mode 100644 index 0000000..177a0fd --- /dev/null +++ b/content/assets/zero-cost-thread-sched/go.sum @@ -0,0 +1,4 @@ +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20201108214237-06ea97f0c265 h1:BcbKYUZo/TKPsiSh7LymK3p+TNAJJW3OfGO/21sBbiA= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20201108214237-06ea97f0c265/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/content/assets/zero-cost-thread-sched/main.go b/content/assets/zero-cost-thread-sched/main.go new file mode 100644 index 0000000..73a80cb --- /dev/null +++ b/content/assets/zero-cost-thread-sched/main.go @@ -0,0 +1,61 @@ +// Copyright (c) 2021 The golang.design Initiative Authors. +// All rights reserved. +// +// The code below is produced by Changkun Ou . + +package main + +import ( + "flag" + "fmt" + "os" + "runtime/trace" + "time" + "x/app" + "x/mainthread" +) + +func main() { + mainthread.Init(fn) +} + +func fn() { + run := flag.Bool("run", false, "start test") + traceF := flag.String("trace", "trace.out", "trace file, default: trace.out") + traceT := flag.String("d", "10s", "trace duration, default: 10s") + flag.Usage = func() { + fmt.Fprintf(os.Stderr, `usage: go run main.go -run [-trace FILENAME -d DURATION] +options: +`) + flag.PrintDefaults() + } + flag.Parse() + if !*run { + flag.Usage() + os.Exit(2) + } + + d, err := time.ParseDuration(*traceT) + if err != nil { + flag.Usage() + os.Exit(2) + } + + w, err := app.NewWindow() + if err != nil { + panic(err) + } + defer app.Terminate() + + go func() { + f, _ := os.Create(*traceF) + defer f.Close() + trace.Start(f) + defer trace.Stop() + time.Sleep(d) + w.Stop() + }() + for !w.Closed() { + w.Update() + } +} diff --git a/content/assets/zero-cost-thread-sched/mainthread/mainthread.go b/content/assets/zero-cost-thread-sched/mainthread/mainthread.go new file mode 100644 index 0000000..226595e --- /dev/null +++ b/content/assets/zero-cost-thread-sched/mainthread/mainthread.go @@ -0,0 +1,80 @@ +// Copyright (c) 2021 The golang.design Initiative Authors. +// All rights reserved. +// +// The code below is produced by Changkun Ou . + +package mainthread + +import ( + "runtime" + "sync" +) + +func init() { + runtime.LockOSThread() +} + +// Init initializes the functionality of running arbitrary subsequent +// functions be called on the main system thread. +// +// Init must be called in the main.main function. +func Init(main func()) { + done := donePool.Get().(chan struct{}) + defer donePool.Put(done) + + go func() { + defer func() { + done <- struct{}{} + }() + main() + }() + + for { + select { + case f := <-funcQ: + if f.fn != nil { + f.fn() + f.done <- struct{}{} + } else if f.fnv != nil { + f.ret <- f.fnv() + } + case <-done: + return + } + } +} + +// Call calls f on the main thread and blocks until f finishes. +func Call(f func()) { + done := donePool.Get().(chan struct{}) + defer donePool.Put(done) + + funcQ <- funcData{fn: f, done: done} + <-done +} + +// CallV calls f on the main thread. It returns what f returns. +func CallV(f func() interface{}) interface{} { + ret := retPool.Get().(chan interface{}) + defer retPool.Put(ret) + + funcQ <- funcData{fnv: f, ret: ret} + return <-ret +} + +var ( + funcQ = make(chan funcData, runtime.GOMAXPROCS(0)) + donePool = sync.Pool{New: func() interface{} { + return make(chan struct{}) + }} + retPool = sync.Pool{New: func() interface{} { + return make(chan interface{}) + }} +) + +type funcData struct { + fn func() + done chan struct{} + fnv func() interface{} + ret chan interface{} +} diff --git a/content/assets/zero-cost-thread-sched/mainthread/mainthread_test.go b/content/assets/zero-cost-thread-sched/mainthread/mainthread_test.go new file mode 100644 index 0000000..aa832b9 --- /dev/null +++ b/content/assets/zero-cost-thread-sched/mainthread/mainthread_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2021 The golang.design Initiative Authors. +// All rights reserved. +// +// The code below is produced by Changkun Ou . + +package mainthread_test + +import ( + "testing" + + "x/mainthread" +) + +/* +bench: run benchmarks under 90% cpufreq... +bench: go test -run=^$ -bench=. -count=10 +goos: linux +goarch: amd64 +pkg: x/mainthread +cpu: Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz + +name time/op +Call-16 391ns ±0% +CallV-16 398ns ±0% + +name alloc/op +Call-16 0.00B +CallV-16 0.00B + +name allocs/op +Call-16 0.00 +CallV-16 0.00 +*/ + +func BenchmarkCall(b *testing.B) { + f := func() {} + mainthread.Init(func() { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + mainthread.Call(f) + } + }) +} + +func BenchmarkCallV(b *testing.B) { + f := func() interface{} { + return true + } + mainthread.Init(func() { + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = mainthread.CallV(f).(bool) + } + }) +} diff --git a/content/assets/zero-cost-thread-sched/thread/thread.go b/content/assets/zero-cost-thread-sched/thread/thread.go new file mode 100644 index 0000000..b4fdd63 --- /dev/null +++ b/content/assets/zero-cost-thread-sched/thread/thread.go @@ -0,0 +1,89 @@ +// Copyright (c) 2021 The golang.design Initiative Authors. +// All rights reserved. +// +// The code below is produced by Changkun Ou . + +package thread + +import ( + "runtime" + "sync" +) + +var donePool = sync.Pool{ + New: func() interface{} { + return make(chan struct{}) + }, +} + +func init() { + runtime.LockOSThread() +} + +type funcData struct { + fn func() + done chan struct{} +} + +// Thread offers facilities to schedule function calls to run +// on a same thread. +type Thread struct { + f chan funcData + terminate chan struct{} +} + +// Call calls f on the given thread. +func (t *Thread) Call(f func()) bool { + if f == nil { + return false + } + select { + case <-t.terminate: + return false + default: + done := donePool.Get().(chan struct{}) + defer donePool.Put(done) + defer func() { + <-done + }() + + t.f <- funcData{fn: f, done: done} + } + return true +} + +// Terminate terminates the current thread. +func (t *Thread) Terminate() { + select { + case <-t.terminate: + return + default: + t.terminate <- struct{}{} + } +} + +// New creates +func New() *Thread { + t := Thread{ + f: make(chan funcData), + terminate: make(chan struct{}), + } + go func() { + runtime.LockOSThread() + for { + select { + case f := <-t.f: + func() { + defer func() { + f.done <- struct{}{} + }() + f.fn() + }() + case <-t.terminate: + close(t.terminate) + return + } + } + }() + return &t +} diff --git a/content/assets/zero-cost-thread-sched/thread/thread_test.go b/content/assets/zero-cost-thread-sched/thread/thread_test.go new file mode 100644 index 0000000..3e96660 --- /dev/null +++ b/content/assets/zero-cost-thread-sched/thread/thread_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2021 The golang.design Initiative Authors. +// All rights reserved. +// +// The code below is produced by Changkun Ou . + +package thread_test + +import ( + "testing" + "x/thread" + + "golang.org/x/sys/unix" +) + +func TestThread(t *testing.T) { + lastThId := 0 + + th := thread.New() + th.Call(func() { + lastThId = unix.Gettid() + t.Logf("thread id: %d", lastThId) + }) + var failed bool + th.Call(func() { + if unix.Gettid() != lastThId { + failed = true + } + lastThId = unix.Gettid() + t.Logf("thread id: %d", lastThId) + }) + if failed { + t.Fatalf("failed to schedule function on the same thread.") + } + th.Terminate() + th.Terminate() + + th.Call(func() { + panic("unexpected call") + }) + th.Call(func() { + panic("unexpected call") + }) + + th = thread.New() + th.Call(func() { + if unix.Gettid() == lastThId { + failed = true + } + lastThId = unix.Gettid() + t.Logf("thread id: %d", lastThId) + }) + if failed { + t.Fatalf("failed to schedule function on a different thread.") + } + th.Call(func() { + if unix.Gettid() != lastThId { + failed = true + } + lastThId = unix.Gettid() + t.Logf("thread id: %d", lastThId) + }) + if failed { + t.Fatalf("failed to schedule function on the same thread.") + } +} + +func BenchmarkThreadCall(b *testing.B) { + th := thread.New() + f := func() {} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + th.Call(f) + } +}