├── go.mod ├── go.sum ├── .gitignore ├── README.md ├── .github └── workflows │ └── thread.yml ├── bench_test.go ├── LICENSE ├── thread_test.go └── thread.go /go.mod: -------------------------------------------------------------------------------- 1 | module golang.design/x/thread 2 | 3 | go 1.13 4 | 5 | require golang.org/x/sys v0.0.0-20210122093101-04d7465088b8 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.0.0-20210122093101-04d7465088b8 h1:de2yTH1xuxjmGB7i6Z5o2z3RCHVa0XlpSZzjd8Fe6bE= 2 | golang.org/x/sys v0.0.0-20210122093101-04d7465088b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # thread [![PkgGoDev](https://pkg.go.dev/badge/golang.design/x/thread)](https://pkg.go.dev/golang.design/x/thread) [![Go Report Card](https://goreportcard.com/badge/golang.design/x/thread)](https://goreportcard.com/report/golang.design/x/thread) ![thread](https://github.com/golang-design/thread/workflows/thread/badge.svg?branch=main) 2 | 3 | Package thread provides threading facilities, such as scheduling 4 | calls on a specific thread, local storage, etc. 5 | 6 | ```go 7 | import "golang.design/x/thread" 8 | ``` 9 | 10 | ## Quick Start 11 | 12 | ```go 13 | th := thread.New() 14 | 15 | th.Call(func() { 16 | // call on the created thread 17 | }) 18 | ``` 19 | 20 | ## License 21 | 22 | MIT © 2021 The golang.design Initiative -------------------------------------------------------------------------------- /.github/workflows/thread.yml: -------------------------------------------------------------------------------- 1 | name: thread 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ^1.13 19 | id: go 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | - name: Race Test 28 | run: | 29 | go test -v -race -count=1 ./... 30 | - name: Test 31 | run: | 32 | go test -v -coverprofile=coverage.txt -covermode=atomic ./... 33 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package thread_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.design/x/thread" 7 | ) 8 | 9 | func BenchmarkThread_Call(b *testing.B) { 10 | th := thread.New() 11 | b.ReportAllocs() 12 | b.ResetTimer() 13 | 14 | for i := 0; i < b.N; i++ { 15 | th.Call(func() {}) 16 | } 17 | } 18 | func BenchmarkThread_CallV(b *testing.B) { 19 | th := thread.New() 20 | b.ReportAllocs() 21 | b.ResetTimer() 22 | 23 | for i := 0; i < b.N; i++ { 24 | _ = th.CallV(func() interface{} { 25 | return true 26 | }).(bool) 27 | } 28 | } 29 | 30 | func BenchmarkThread_TLS(b *testing.B) { 31 | th := thread.New() 32 | th.Call(func() { 33 | th.SetTLS(1) 34 | }) 35 | 36 | b.ReportAllocs() 37 | b.ResetTimer() 38 | for i := 0; i < b.N; i++ { 39 | _ = th.CallV(func() interface{} { 40 | return th.GetTLS() 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 The golang.design Initiative 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /thread_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | 5 | // +build linux 6 | 7 | package thread_test 8 | 9 | import ( 10 | "strings" 11 | "sync" 12 | "testing" 13 | 14 | "golang.design/x/thread" 15 | "golang.org/x/sys/unix" 16 | ) 17 | 18 | func TestNew(t *testing.T) { 19 | th1 := thread.New() 20 | th2 := thread.New() 21 | 22 | if th1.ID() == th2.ID() { 23 | t.Fatalf("two different threads have same id") 24 | } 25 | } 26 | 27 | func TestThread_Call(t *testing.T) { 28 | th := thread.New() 29 | defer th.Terminate() 30 | osThreadID := 0 31 | 32 | th.Call(func() { 33 | osThreadID = unix.Gettid() 34 | t.Logf("thread id: %v", osThreadID) 35 | }) 36 | 37 | var shouldFail bool 38 | th.Call(func() { 39 | if unix.Gettid() != osThreadID { 40 | shouldFail = true 41 | } 42 | osThreadID = unix.Gettid() 43 | t.Logf("thread id: %v", osThreadID) 44 | }) 45 | if shouldFail { 46 | t.Fatalf("failed to schedule function call on the same thread.") 47 | } 48 | } 49 | 50 | func TestThread_Terminate(t *testing.T) { 51 | th := thread.New() 52 | 53 | th.Terminate() 54 | th.Terminate() 55 | th.Terminate() // thread should be able to terminate multiple times 56 | 57 | th.Call(func() { 58 | panic("call should not be scheduled on a terminated thread.") 59 | }) 60 | th.Call(func() { 61 | panic("call should not be scheduled on a terminated thread.") 62 | }) 63 | } 64 | 65 | func TestThread_CallNonBlock(t *testing.T) { 66 | th := thread.New() 67 | defer th.Terminate() 68 | 69 | th.CallNonBlock(func() { 70 | th.SetTLS(1) 71 | }) 72 | 73 | v := th.CallV(func() interface{} { 74 | return th.GetTLS() 75 | }).(int) 76 | if v != 1 { 77 | t.Fatalf("non blocking call is not scheduled before a blocking call.") 78 | } 79 | } 80 | 81 | func TestThread_CallV(t *testing.T) { 82 | th := thread.New() 83 | defer th.Terminate() 84 | 85 | osThreadID1 := th.CallV(func() interface{} { 86 | return unix.Gettid() 87 | }).(int) 88 | 89 | osThreadID2 := th.CallV(func() interface{} { 90 | return unix.Gettid() 91 | }).(int) 92 | if osThreadID1 != osThreadID2 { 93 | t.Fatalf("failed to schedule function call on the same thread.") 94 | } 95 | } 96 | 97 | func TestThread_TLS(t *testing.T) { 98 | th1 := thread.New() 99 | th1.SetTLS("hello") 100 | 101 | th2 := thread.New() 102 | th2.SetTLS("world") 103 | 104 | tls1 := th1.CallV(func() interface{} { 105 | return th1.GetTLS() 106 | }).(string) 107 | if strings.Compare(tls1, "hello") != 0 { 108 | t.Fatalf("incorrect TLS access") 109 | } 110 | t.Log(tls1) 111 | tls2 := th2.CallV(func() interface{} { 112 | return th2.GetTLS() 113 | }).(string) 114 | if strings.Compare(tls2, "world") != 0 { 115 | t.Fatalf("incorrect TLS access") 116 | } 117 | t.Log(tls2) 118 | } 119 | 120 | func TestThread_TLSConcurrent(t *testing.T) { 121 | wg := sync.WaitGroup{} 122 | wg.Add(2) 123 | 124 | th := thread.New() 125 | go func() { 126 | defer wg.Done() 127 | fn := func() { 128 | th.SetTLS(1) 129 | } 130 | th.Call(fn) 131 | }() 132 | go func() { 133 | defer wg.Done() 134 | fn := func() { 135 | th.SetTLS(2) 136 | } 137 | th.Call(fn) 138 | }() 139 | wg.Wait() 140 | } 141 | -------------------------------------------------------------------------------- /thread.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The golang.design Initiative Authors. 2 | // All rights reserved. Use of this source code is governed 3 | // by a MIT license that can be found in the LICENSE file. 4 | 5 | // Package thread provides threading facilities, such as scheduling 6 | // calls on a specific thread, local storage, etc. 7 | package thread // import "golang.design/x/thread" 8 | 9 | import ( 10 | "runtime" 11 | "sync" 12 | "sync/atomic" 13 | ) 14 | 15 | // Thread represents a thread instance. 16 | type Thread interface { 17 | // ID returns the ID of the thread. 18 | ID() uint64 19 | 20 | // Call calls fn from the given thread. It blocks until fn returns. 21 | Call(fn func()) 22 | 23 | // CallNonBlock call fn from the given thread without waiting 24 | // fn to complete. 25 | CallNonBlock(fn func()) 26 | 27 | // CallV call fn from the given thread and returns the returned 28 | // value from fn. 29 | // 30 | // The purpose of this function is to avoid value escaping. 31 | // In particular: 32 | // 33 | // th := thread.New() 34 | // var ret interface{} 35 | // th.Call(func() { 36 | // ret = 1 37 | // }) 38 | // 39 | // will cause variable ret be allocated on the heap, whereas 40 | // 41 | // th := thread.New() 42 | // ret := th.CallV(func() interface{} { 43 | // return 1 44 | // }).(int) 45 | // 46 | // will offer zero allocation benefits. 47 | CallV(fn func() interface{}) interface{} 48 | 49 | // SetTLS stores a given value to the local storage of the given 50 | // thread. This method must be accessed in Call, or CallV, or 51 | // CallNonBlock. For instance: 52 | // 53 | // th := thread.New() 54 | // th.Call(func() { 55 | // th.SetTLS("store in thread local storage") 56 | // }) 57 | SetTLS(x interface{}) 58 | 59 | // GetTLS returns the locally stored value from local storage of 60 | // the given thread. This method must be access in Call, or CallV, 61 | // or CallNonBlock. For instance: 62 | // 63 | // th := thread.New() 64 | // th.Call(func() { 65 | // tls := th.GetTLS() 66 | // // ... do what ever you want to do with tls value ... 67 | // }) 68 | // 69 | GetTLS() interface{} 70 | 71 | // Terminate terminates the given thread gracefully. 72 | // Scheduled but unexecuted calls will be discarded. 73 | Terminate() 74 | } 75 | 76 | // New creates a new thread instance. 77 | func New() Thread { 78 | th := thread{ 79 | id: atomic.AddUint64(&globalID, 1), 80 | fdCh: make(chan funcData, runtime.GOMAXPROCS(0)), 81 | doneCh: make(chan struct{}), 82 | } 83 | runtime.SetFinalizer(&th, func(th interface{}) { 84 | th.(*thread).Terminate() 85 | }) 86 | go func() { 87 | runtime.LockOSThread() 88 | for { 89 | select { 90 | case fd := <-th.fdCh: 91 | func() { 92 | if fd.fn != nil { 93 | defer func() { 94 | if fd.done != nil { 95 | fd.done <- struct{}{} 96 | } 97 | }() 98 | fd.fn() 99 | } else if fd.fnv != nil { 100 | var ret interface{} 101 | defer func() { 102 | if fd.ret != nil { 103 | fd.ret <- ret 104 | } 105 | }() 106 | ret = fd.fnv() 107 | } 108 | }() 109 | case <-th.doneCh: 110 | close(th.doneCh) 111 | return 112 | } 113 | } 114 | }() 115 | return &th 116 | } 117 | 118 | var ( 119 | donePool = sync.Pool{ 120 | New: func() interface{} { 121 | return make(chan struct{}) 122 | }, 123 | } 124 | varPool = sync.Pool{ 125 | New: func() interface{} { 126 | return make(chan interface{}) 127 | }, 128 | } 129 | globalID uint64 // atomic 130 | _ Thread = &thread{} 131 | ) 132 | 133 | type funcData struct { 134 | fn func() 135 | done chan struct{} 136 | 137 | fnv func() interface{} 138 | ret chan interface{} 139 | } 140 | 141 | type thread struct { 142 | id uint64 143 | tls interface{} 144 | 145 | fdCh chan funcData 146 | doneCh chan struct{} 147 | } 148 | 149 | func (th thread) ID() uint64 { 150 | return th.id 151 | } 152 | 153 | func (th *thread) Call(fn func()) { 154 | if fn == nil { 155 | return 156 | } 157 | 158 | select { 159 | case <-th.doneCh: 160 | return 161 | default: 162 | done := donePool.Get().(chan struct{}) 163 | defer donePool.Put(done) 164 | defer func() { <-done }() 165 | 166 | th.fdCh <- funcData{fn: fn, done: done} 167 | } 168 | return 169 | } 170 | 171 | func (th *thread) CallNonBlock(fn func()) { 172 | if fn == nil { 173 | return 174 | } 175 | select { 176 | case <-th.doneCh: 177 | return 178 | default: 179 | th.fdCh <- funcData{fn: fn} 180 | } 181 | } 182 | 183 | func (th *thread) CallV(fn func() interface{}) (ret interface{}) { 184 | if fn == nil { 185 | return nil 186 | } 187 | 188 | select { 189 | case <-th.doneCh: 190 | return nil 191 | default: 192 | done := varPool.Get().(chan interface{}) 193 | defer varPool.Put(done) 194 | defer func() { ret = <-done }() 195 | 196 | th.fdCh <- funcData{fnv: fn, ret: done} 197 | return 198 | } 199 | } 200 | 201 | func (th *thread) GetTLS() interface{} { 202 | return th.tls 203 | } 204 | 205 | func (th *thread) SetTLS(x interface{}) { 206 | th.tls = x 207 | } 208 | 209 | func (th *thread) Terminate() { 210 | select { 211 | case <-th.doneCh: 212 | return 213 | default: 214 | th.doneCh <- struct{}{} 215 | select { 216 | case <-th.doneCh: 217 | return 218 | } 219 | } 220 | } 221 | --------------------------------------------------------------------------------