├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug.yml
│ ├── config.yml
│ └── question.yml
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── static.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── README_zh.md
├── SECURITY.md
├── VERSION
├── api_cloneable.go
├── api_cloneable_test.go
├── api_error.go
├── api_error_test.go
├── api_future_task.go
├── api_future_task_test.go
├── api_goid.go
├── api_goid_test.go
├── api_routine.go
├── api_routine_test.go
├── api_thread_local.go
├── api_thread_local_test.go
├── error.go
├── error_test.go
├── future_task.go
├── future_task_test.go
├── g.go
├── g
├── asm_386.s
├── asm_amd64.s
├── asm_arm.s
├── asm_arm64.s
├── asm_loong64.s
├── asm_mips64x.s
├── asm_mipsx.s
├── asm_ppc64x.s
├── asm_riscv64.s
├── asm_s390x.s
├── asm_wasm.s
├── g.go
├── g_link.go
├── g_test.go
├── go_tls.h
├── reflect.go
└── reflect_test.go
├── g_test.go
├── g_x.go
├── g_x_link.go
├── go.mod
├── go.sum
├── pprof_label_go118.go
├── pprof_label_go124.go
├── pprof_label_test.go
├── reflect.go
├── reflect_test.go
├── routine.go
├── routine_test.go
├── runtime.go
├── runtime_test.go
├── stack.go
├── stack_test.go
├── thread.go
├── thread_link.go
├── thread_local.go
├── thread_local_inheritable.go
├── thread_local_inheritable_test.go
├── thread_local_map.go
├── thread_local_map_entry.go
├── thread_local_map_entry_test.go
├── thread_local_map_test.go
├── thread_local_test.go
└── thread_test.go
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Handle line endings automatically for files detected as text
2 | # and leave all files detected as binary untouched.
3 | * text=auto
4 |
5 | #
6 | # The above will handle all files NOT found below
7 | #
8 | # These files are text and should be normalized (Convert crlf => lf)
9 | *.c text
10 | *.cpp text
11 | *.go text
12 | *.h text
13 | *.md text
14 | *.mod text
15 | *.s text
16 | *.sum text
17 |
18 | # These files are binary and should be left untouched
19 | # (binary is a macro for -text -diff)
20 | *.dll binary
21 | *.exe binary
22 | *.so binary
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | name: Bugs
2 | description: Create a report to help us improve
3 | title: "[Bug]
"
4 | labels: [ "bug" ]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thanks for taking the time to fill out this bug report!
9 |
10 | - type: checkboxes
11 | attributes:
12 | label: Is there an existing issue for this?
13 | description: Please search to see if an issue already exists for the bug you encountered.
14 | options:
15 | - label: I have searched the existing issues
16 | required: true
17 |
18 | - type: checkboxes
19 | attributes:
20 | label: Does this issue reproduce with the latest release?
21 | description: Please upgrade to the latest version to see if the issue still exists.
22 | options:
23 | - label: I have upgrade to the latest version
24 | required: true
25 |
26 | - type: textarea
27 | attributes:
28 | label: Steps To Reproduce
29 | description: The smallest possible code example to show the problem that can be compiled.
30 | placeholder: |
31 | ```go
32 | package main
33 |
34 | import (
35 | "fmt"
36 | )
37 |
38 | func main() {
39 | fmt.Println("Hello World")
40 | }
41 | ```
42 | validations:
43 | required: true
44 |
45 | - type: textarea
46 | attributes:
47 | label: Expected Behavior
48 | description: A concise description of what you expected to happen.
49 | placeholder: |
50 | The console prints `Hello World`
51 | validations:
52 | required: true
53 |
54 | - type: textarea
55 | attributes:
56 | label: Current Behavior
57 | description: A concise description of what you're experiencing.
58 | placeholder: |
59 | The console prints nothing
60 | validations:
61 | required: true
62 |
63 | - type: textarea
64 | attributes:
65 | label: Environment
66 | description: What version of Go are you using (`go version`)?
67 | placeholder: |
68 | `go version go1.18.3 windows/amd64`
69 | validations:
70 | required: true
71 |
72 | - type: checkboxes
73 | id: terms
74 | attributes:
75 | label: Code of Conduct
76 | description: By submitting this issue, you agree to follow our [Code of Conduct](../blob/main/CODE_OF_CONDUCT.md)
77 | options:
78 | - label: I agree to follow this project's Code of Conduct
79 | required: true
80 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.yml:
--------------------------------------------------------------------------------
1 | name: Questions
2 | description: Create an issue for help
3 | title: "[Question] "
4 | labels: [ "question" ]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thanks for taking the time to fill out this issue!
9 |
10 | - type: checkboxes
11 | attributes:
12 | label: Is there an existing issue for this?
13 | description: Please search to see if an issue already exists for the issue you encountered.
14 | options:
15 | - label: I have searched the existing issues
16 | required: true
17 |
18 | - type: textarea
19 | attributes:
20 | label: Question
21 | description: the question you want to ask.
22 | validations:
23 | required: true
24 |
25 | - type: checkboxes
26 | id: terms
27 | attributes:
28 | label: Code of Conduct
29 | description: By submitting this issue, you agree to follow our [Code of Conduct](../blob/main/CODE_OF_CONDUCT.md)
30 | options:
31 | - label: I agree to follow this project's Code of Conduct
32 | required: true
33 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | - With pull requests:
2 | - Open your pull request against `main` branch.
3 | - It should pass all tests in the available continuous integration systems such as GitHub Actions.
4 | - You should add/modify tests to cover your proposed code changes.
5 | - If your pull request contains a new feature, please document it on the README.md and README_zh.md.
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Configuration file of GitHub Dependabot
2 |
3 | version: 2
4 | updates:
5 |
6 | # Maintain dependencies for gomod
7 | - package-ecosystem: "gomod"
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 | time: "08:00"
12 | timezone: "Asia/Shanghai"
13 |
14 | # Maintain dependencies for GitHub Actions
15 | - package-ecosystem: "github-actions"
16 | directory: "/"
17 | schedule:
18 | interval: "daily"
19 | time: "08:00"
20 | timezone: "Asia/Shanghai"
21 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # Workflow file of GitHub Actions
2 |
3 | name: build
4 |
5 | on:
6 | push:
7 | branches:
8 | - main
9 | - feature/**
10 | pull_request:
11 | branches:
12 | - main
13 |
14 | jobs:
15 | Lint:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout scm
19 | uses: actions/checkout@v4
20 |
21 | - name: Set up Go
22 | uses: actions/setup-go@v5
23 | with:
24 | go-version-file: go.mod
25 | cache: false
26 |
27 | - name: Lint
28 | uses: golangci/golangci-lint-action@v6
29 |
30 | CodeQL:
31 | needs: Lint
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Checkout scm
35 | uses: actions/checkout@v4
36 |
37 | - name: Set up Go
38 | uses: actions/setup-go@v5
39 | with:
40 | go-version-file: go.mod
41 | cache: false
42 |
43 | - name: Initialize CodeQL
44 | uses: github/codeql-action/init@v3
45 | with:
46 | languages: go
47 |
48 | - name: Perform CodeQL Analysis
49 | uses: github/codeql-action/analyze@v3
50 |
51 | Test:
52 | needs: Lint
53 | runs-on: ${{ matrix.runs-on }}
54 | strategy:
55 | fail-fast: false
56 | matrix:
57 | os: [ darwin, linux, windows, freebsd, js ]
58 | arch: [ 386, amd64, armv6, armv7, arm64, loong64, mips, mipsle, mips64, mips64le, ppc64, ppc64le, riscv64, s390x, wasm ]
59 | go: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24' ]
60 | exclude:
61 | # darwin excludes
62 | - os: darwin
63 | arch: 386
64 | - os: darwin
65 | arch: armv6
66 | - os: darwin
67 | arch: armv7
68 | - os: darwin
69 | arch: loong64
70 | - os: darwin
71 | arch: mips
72 | - os: darwin
73 | arch: mipsle
74 | - os: darwin
75 | arch: mips64
76 | - os: darwin
77 | arch: mips64le
78 | - os: darwin
79 | arch: ppc64
80 | - os: darwin
81 | arch: ppc64le
82 | - os: darwin
83 | arch: riscv64
84 | - os: darwin
85 | arch: s390x
86 | - os: darwin
87 | arch: wasm
88 | # linux excludes
89 | - os: linux
90 | arch: loong64
91 | go: 1.18
92 | - os: linux
93 | arch: mips64
94 | go: 1.22
95 | - os: linux
96 | arch: mips64le
97 | go: 1.22
98 | - os: linux
99 | arch: wasm
100 | # windows excludes
101 | - os: windows
102 | arch: armv6
103 | - os: windows
104 | arch: armv7
105 | - os: windows
106 | arch: arm64
107 | - os: windows
108 | arch: loong64
109 | - os: windows
110 | arch: mips
111 | - os: windows
112 | arch: mipsle
113 | - os: windows
114 | arch: mips64
115 | - os: windows
116 | arch: mips64le
117 | - os: windows
118 | arch: ppc64
119 | - os: windows
120 | arch: ppc64le
121 | - os: windows
122 | arch: riscv64
123 | - os: windows
124 | arch: s390x
125 | - os: windows
126 | arch: wasm
127 | # freebsd excludes
128 | - os: freebsd
129 | arch: armv6
130 | - os: freebsd
131 | arch: armv7
132 | - os: freebsd
133 | arch: arm64
134 | - os: freebsd
135 | arch: loong64
136 | - os: freebsd
137 | arch: mips
138 | - os: freebsd
139 | arch: mipsle
140 | - os: freebsd
141 | arch: mips64
142 | - os: freebsd
143 | arch: mips64le
144 | - os: freebsd
145 | arch: ppc64
146 | - os: freebsd
147 | arch: ppc64le
148 | - os: freebsd
149 | arch: riscv64
150 | - os: freebsd
151 | arch: s390x
152 | - os: freebsd
153 | arch: wasm
154 | # js excludes
155 | - os: js
156 | arch: 386
157 | - os: js
158 | arch: amd64
159 | - os: js
160 | arch: armv6
161 | - os: js
162 | arch: armv7
163 | - os: js
164 | arch: arm64
165 | - os: js
166 | arch: loong64
167 | - os: js
168 | arch: mips
169 | - os: js
170 | arch: mipsle
171 | - os: js
172 | arch: mips64
173 | - os: js
174 | arch: mips64le
175 | - os: js
176 | arch: ppc64
177 | - os: js
178 | arch: ppc64le
179 | - os: js
180 | arch: riscv64
181 | - os: js
182 | arch: s390x
183 | include:
184 | # combine runs on
185 | - os: darwin
186 | runs-on: macos-13
187 | - os: darwin
188 | arch: arm64
189 | runs-on: macos-latest
190 | - os: linux
191 | runs-on: ubuntu-latest
192 | - os: windows
193 | runs-on: windows-latest
194 | - os: windows
195 | go: 1.18
196 | runs-on: windows-2019
197 | - os: freebsd
198 | runs-on: ubuntu-latest
199 | - os: js
200 | runs-on: ubuntu-latest
201 |
202 | steps:
203 | - name: Checkout scm
204 | uses: actions/checkout@v4
205 |
206 | - name: Set up Go
207 | uses: actions/setup-go@v5
208 | with:
209 | go-version: ${{ matrix.go }}
210 | cache: false
211 |
212 | # darwin
213 | - name: 'Test on [darwin] arch [amd64]'
214 | if: ${{ matrix.os == 'darwin' && contains(fromJson('["amd64"]'), matrix.arch) }}
215 | env:
216 | GOOS: ${{ matrix.os }}
217 | GOARCH: ${{ matrix.arch }}
218 | run: go test -v -race -coverprofile='coverage.txt' -covermode=atomic ./...
219 |
220 | - name: 'Test on [darwin] arch [arm64]'
221 | if: ${{ matrix.os == 'darwin' && contains(fromJson('["arm64"]'), matrix.arch) }}
222 | env:
223 | GOOS: ${{ matrix.os }}
224 | GOARCH: ${{ matrix.arch }}
225 | run: go test -v -race -coverprofile='coverage.txt' -covermode=atomic ./...
226 |
227 | # linux
228 | - name: 'Test on [linux] arch [386]'
229 | if: ${{ matrix.os == 'linux' && contains(fromJson('["386"]'), matrix.arch) }}
230 | env:
231 | GOOS: ${{ matrix.os }}
232 | GOARCH: ${{ matrix.arch }}
233 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
234 |
235 | - name: 'Test on [linux] arch [amd64]'
236 | if: ${{ matrix.os == 'linux' && contains(fromJson('["amd64"]'), matrix.arch) }}
237 | env:
238 | GOOS: ${{ matrix.os }}
239 | GOARCH: ${{ matrix.arch }}
240 | run: go test -v -race -coverprofile='coverage.txt' -covermode=atomic ./...
241 |
242 | - name: 'Setup qemu-user-static on [linux] arch [armv6, armv7, arm64, mips, mipsle, mips64, mips64le, ppc64, ppc64le, riscv64, s390x]'
243 | if: ${{ matrix.os == 'linux' && contains(fromJson('["armv6", "armv7", "arm64", "mips", "mipsle", "mips64", "mips64le", "ppc64", "ppc64le", "riscv64", "s390x"]'), matrix.arch) }}
244 | run: |
245 | sudo apt-get update
246 | sudo apt-get -y install qemu-user-static
247 |
248 | - name: 'Test on [linux] arch [armv6]'
249 | if: ${{ matrix.os == 'linux' && contains(fromJson('["armv6"]'), matrix.arch) }}
250 | env:
251 | GOOS: ${{ matrix.os }}
252 | GOARCH: arm
253 | GOARM: 6
254 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
255 |
256 | - name: 'Test on [linux] arch [armv7]'
257 | if: ${{ matrix.os == 'linux' && contains(fromJson('["armv7"]'), matrix.arch) }}
258 | env:
259 | GOOS: ${{ matrix.os }}
260 | GOARCH: arm
261 | GOARM: 7
262 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
263 |
264 | - name: 'Test on [linux] arch [mips, mipsle]'
265 | if: ${{ matrix.os == 'linux' && contains(fromJson('["mips", "mipsle"]'), matrix.arch) }}
266 | env:
267 | GOOS: ${{ matrix.os }}
268 | GOARCH: ${{ matrix.arch }}
269 | GOMIPS: softfloat
270 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
271 |
272 | - name: 'Test on [linux] arch [arm64, mips64, mips64le, ppc64, ppc64le, riscv64, s390x]'
273 | if: ${{ matrix.os == 'linux' && contains(fromJson('["arm64", "mips64", "mips64le", "ppc64", "ppc64le", "riscv64", "s390x"]'), matrix.arch) }}
274 | env:
275 | GOOS: ${{ matrix.os }}
276 | GOARCH: ${{ matrix.arch }}
277 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
278 |
279 | - name: 'Setup qemu-loongarch64-static on [linux] arch [loong64]'
280 | if: ${{ matrix.os == 'linux' && contains(fromJson('["loong64"]'), matrix.arch) }}
281 | run: |
282 | sudo wget -O /usr/bin/qemu-loongarch64-static https://github.com/loongson/build-tools/releases/download/2025.02.21/qemu-loongarch64
283 | sudo chmod +x /usr/bin/qemu-loongarch64-static
284 | sudo mkdir -p /usr/libexec/qemu-binfmt
285 | sudo ln -s /usr/bin/qemu-loongarch64-static /usr/libexec/qemu-binfmt/loongarch64-binfmt-P
286 | sudo sh -c 'echo ":qemu-loongarch64:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02\x01:\xff\xff\xff\xff\xff\xff\xff\xfc\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/libexec/qemu-binfmt/loongarch64-binfmt-P:" > /proc/sys/fs/binfmt_misc/register'
287 |
288 | - name: 'Test on [linux] arch [loong64]'
289 | if: ${{ matrix.os == 'linux' && contains(fromJson('["loong64"]'), matrix.arch) }}
290 | env:
291 | GOOS: ${{ matrix.os }}
292 | GOARCH: ${{ matrix.arch }}
293 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
294 |
295 | # windows
296 | - name: 'Test on [windows] arch [386]'
297 | if: ${{ matrix.os == 'windows' && contains(fromJson('["386"]'), matrix.arch) }}
298 | env:
299 | GOOS: ${{ matrix.os }}
300 | GOARCH: ${{ matrix.arch }}
301 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
302 |
303 | - name: 'Test on [windows] arch [amd64]'
304 | if: ${{ matrix.os == 'windows' && contains(fromJson('["amd64"]'), matrix.arch) }}
305 | env:
306 | GOOS: ${{ matrix.os }}
307 | GOARCH: ${{ matrix.arch }}
308 | run: go test -v -race -coverprofile='coverage.txt' -covermode=atomic ./...
309 |
310 | # freebsd
311 | - name: 'Build for [freebsd] arch [386, amd64]'
312 | if: ${{ matrix.os == 'freebsd' && contains(fromJson('["386", "amd64"]'), matrix.arch) }}
313 | env:
314 | GOOS: ${{ matrix.os }}
315 | GOARCH: ${{ matrix.arch }}
316 | run: go test -v -c -covermode=atomic
317 |
318 | - name: 'Test on [freebsd] arch [386, amd64]'
319 | if: ${{ matrix.os == 'freebsd' && contains(fromJson('["386", "amd64"]'), matrix.arch) }}
320 | uses: vmactions/freebsd-vm@v1
321 | with:
322 | run: ./routine.test -test.v -test.coverprofile='coverage.txt'
323 |
324 | # js
325 | - name: 'Setup Node.js on [js] arch [wasm]'
326 | if: ${{ matrix.os == 'js' && contains(fromJson('["wasm"]'), matrix.arch) }}
327 | uses: actions/setup-node@v4
328 | with:
329 | node-version: 18
330 |
331 | - name: 'Test on [js] arch [wasm]'
332 | if: ${{ matrix.os == 'js' && contains(fromJson('["wasm"]'), matrix.arch) }}
333 | env:
334 | GOOS: ${{ matrix.os }}
335 | GOARCH: ${{ matrix.arch }}
336 | run: |
337 | go_version=$(go env GOVERSION | cut -c3-)
338 | max_version=$(printf '%s\n' "$go_version" '1.24' | sort -V | tail -n1)
339 | if [ "$go_version" = "$max_version" ]; then
340 | PATH="$PATH:$(go env GOROOT)/lib/wasm"
341 | else
342 | PATH="$PATH:$(go env GOROOT)/misc/wasm"
343 | fi
344 | go test -v -coverprofile='coverage.txt' -covermode=atomic ./...
345 |
346 | - name: Codecov
347 | uses: codecov/codecov-action@v5
348 | with:
349 | name: Codecov on ${{ matrix.os }}/${{ matrix.arch }} go${{ matrix.go }}
350 | token: ${{ secrets.CODECOV_TOKEN }}
351 | fail_ci_if_error: false
352 |
--------------------------------------------------------------------------------
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Workflow file of GitHub Actions
2 |
3 | name: static
4 |
5 | on:
6 | push:
7 | branches:
8 | - main
9 | - feature/**
10 | pull_request:
11 | branches:
12 | - main
13 |
14 | jobs:
15 | Lint:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Checkout scm
19 | uses: actions/checkout@v4
20 |
21 | - name: Set up Go
22 | uses: actions/setup-go@v5
23 | with:
24 | go-version-file: go.mod
25 | cache: false
26 |
27 | - name: Lint
28 | uses: golangci/golangci-lint-action@v6
29 |
30 | CodeQL:
31 | needs: Lint
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Checkout scm
35 | uses: actions/checkout@v4
36 |
37 | - name: Set up Go
38 | uses: actions/setup-go@v5
39 | with:
40 | go-version-file: go.mod
41 | cache: false
42 |
43 | - name: Initialize CodeQL
44 | uses: github/codeql-action/init@v3
45 | with:
46 | languages: go
47 |
48 | - name: Perform CodeQL Analysis
49 | uses: github/codeql-action/analyze@v3
50 |
51 | Test:
52 | needs: Lint
53 | runs-on: ${{ matrix.runs-on }}
54 | strategy:
55 | fail-fast: false
56 | matrix:
57 | os: [ darwin, linux, windows, freebsd, js ]
58 | arch: [ 386, amd64, armv6, armv7, arm64, loong64, mips, mipsle, mips64, mips64le, ppc64, ppc64le, riscv64, s390x, wasm ]
59 | go: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24' ]
60 | exclude:
61 | # darwin excludes
62 | - os: darwin
63 | arch: 386
64 | - os: darwin
65 | arch: armv6
66 | - os: darwin
67 | arch: armv7
68 | - os: darwin
69 | arch: loong64
70 | - os: darwin
71 | arch: mips
72 | - os: darwin
73 | arch: mipsle
74 | - os: darwin
75 | arch: mips64
76 | - os: darwin
77 | arch: mips64le
78 | - os: darwin
79 | arch: ppc64
80 | - os: darwin
81 | arch: ppc64le
82 | - os: darwin
83 | arch: riscv64
84 | - os: darwin
85 | arch: s390x
86 | - os: darwin
87 | arch: wasm
88 | # linux excludes
89 | - os: linux
90 | arch: loong64
91 | go: 1.18
92 | - os: linux
93 | arch: mips64
94 | go: 1.22
95 | - os: linux
96 | arch: mips64le
97 | go: 1.22
98 | - os: linux
99 | arch: wasm
100 | # windows excludes
101 | - os: windows
102 | arch: armv6
103 | - os: windows
104 | arch: armv7
105 | - os: windows
106 | arch: arm64
107 | - os: windows
108 | arch: loong64
109 | - os: windows
110 | arch: mips
111 | - os: windows
112 | arch: mipsle
113 | - os: windows
114 | arch: mips64
115 | - os: windows
116 | arch: mips64le
117 | - os: windows
118 | arch: ppc64
119 | - os: windows
120 | arch: ppc64le
121 | - os: windows
122 | arch: riscv64
123 | - os: windows
124 | arch: s390x
125 | - os: windows
126 | arch: wasm
127 | # freebsd excludes
128 | - os: freebsd
129 | arch: armv6
130 | - os: freebsd
131 | arch: armv7
132 | - os: freebsd
133 | arch: arm64
134 | - os: freebsd
135 | arch: loong64
136 | - os: freebsd
137 | arch: mips
138 | - os: freebsd
139 | arch: mipsle
140 | - os: freebsd
141 | arch: mips64
142 | - os: freebsd
143 | arch: mips64le
144 | - os: freebsd
145 | arch: ppc64
146 | - os: freebsd
147 | arch: ppc64le
148 | - os: freebsd
149 | arch: riscv64
150 | - os: freebsd
151 | arch: s390x
152 | - os: freebsd
153 | arch: wasm
154 | # js excludes
155 | - os: js
156 | arch: 386
157 | - os: js
158 | arch: amd64
159 | - os: js
160 | arch: armv6
161 | - os: js
162 | arch: armv7
163 | - os: js
164 | arch: arm64
165 | - os: js
166 | arch: loong64
167 | - os: js
168 | arch: mips
169 | - os: js
170 | arch: mipsle
171 | - os: js
172 | arch: mips64
173 | - os: js
174 | arch: mips64le
175 | - os: js
176 | arch: ppc64
177 | - os: js
178 | arch: ppc64le
179 | - os: js
180 | arch: riscv64
181 | - os: js
182 | arch: s390x
183 | include:
184 | # combine runs on
185 | - os: darwin
186 | runs-on: macos-13
187 | - os: darwin
188 | arch: arm64
189 | runs-on: macos-latest
190 | - os: linux
191 | runs-on: ubuntu-latest
192 | - os: windows
193 | runs-on: windows-latest
194 | - os: windows
195 | go: 1.18
196 | runs-on: windows-2019
197 | - os: freebsd
198 | runs-on: ubuntu-latest
199 | - os: js
200 | runs-on: ubuntu-latest
201 |
202 | steps:
203 | - name: Checkout scm
204 | uses: actions/checkout@v4
205 |
206 | - name: Set up Go
207 | uses: actions/setup-go@v5
208 | with:
209 | go-version: ${{ matrix.go }}
210 | cache: false
211 |
212 | # prepare
213 | - name: 'Install routinex'
214 | run: go install github.com/timandy/routinex@latest
215 |
216 | # darwin
217 | - name: 'Test on [darwin] arch [amd64]'
218 | if: ${{ matrix.os == 'darwin' && contains(fromJson('["amd64"]'), matrix.arch) }}
219 | env:
220 | GOOS: ${{ matrix.os }}
221 | GOARCH: ${{ matrix.arch }}
222 | run: go test -v -race -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
223 |
224 | - name: 'Test on [darwin] arch [arm64]'
225 | if: ${{ matrix.os == 'darwin' && contains(fromJson('["arm64"]'), matrix.arch) }}
226 | env:
227 | GOOS: ${{ matrix.os }}
228 | GOARCH: ${{ matrix.arch }}
229 | run: go test -v -race -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
230 |
231 | # linux
232 | - name: 'Test on [linux] arch [386]'
233 | if: ${{ matrix.os == 'linux' && contains(fromJson('["386"]'), matrix.arch) }}
234 | env:
235 | GOOS: ${{ matrix.os }}
236 | GOARCH: ${{ matrix.arch }}
237 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
238 |
239 | - name: 'Test on [linux] arch [amd64]'
240 | if: ${{ matrix.os == 'linux' && contains(fromJson('["amd64"]'), matrix.arch) }}
241 | env:
242 | GOOS: ${{ matrix.os }}
243 | GOARCH: ${{ matrix.arch }}
244 | run: go test -v -race -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
245 |
246 | - name: 'Setup qemu-user-static on [linux] arch [armv6, armv7, arm64, mips, mipsle, mips64, mips64le, ppc64, ppc64le, riscv64, s390x]'
247 | if: ${{ matrix.os == 'linux' && contains(fromJson('["armv6", "armv7", "arm64", "mips", "mipsle", "mips64", "mips64le", "ppc64", "ppc64le", "riscv64", "s390x"]'), matrix.arch) }}
248 | run: |
249 | sudo apt-get update
250 | sudo apt-get -y install qemu-user-static
251 |
252 | - name: 'Test on [linux] arch [armv6]'
253 | if: ${{ matrix.os == 'linux' && contains(fromJson('["armv6"]'), matrix.arch) }}
254 | env:
255 | GOOS: ${{ matrix.os }}
256 | GOARCH: arm
257 | GOARM: 6
258 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
259 |
260 | - name: 'Test on [linux] arch [armv7]'
261 | if: ${{ matrix.os == 'linux' && contains(fromJson('["armv7"]'), matrix.arch) }}
262 | env:
263 | GOOS: ${{ matrix.os }}
264 | GOARCH: arm
265 | GOARM: 7
266 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
267 |
268 | - name: 'Test on [linux] arch [mips, mipsle]'
269 | if: ${{ matrix.os == 'linux' && contains(fromJson('["mips", "mipsle"]'), matrix.arch) }}
270 | env:
271 | GOOS: ${{ matrix.os }}
272 | GOARCH: ${{ matrix.arch }}
273 | GOMIPS: softfloat
274 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
275 |
276 | - name: 'Test on [linux] arch [arm64, mips64, mips64le, ppc64, ppc64le, riscv64, s390x]'
277 | if: ${{ matrix.os == 'linux' && contains(fromJson('["arm64", "mips64", "mips64le", "ppc64", "ppc64le", "riscv64", "s390x"]'), matrix.arch) }}
278 | env:
279 | GOOS: ${{ matrix.os }}
280 | GOARCH: ${{ matrix.arch }}
281 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
282 |
283 | - name: 'Setup qemu-loongarch64-static on [linux] arch [loong64]'
284 | if: ${{ matrix.os == 'linux' && contains(fromJson('["loong64"]'), matrix.arch) }}
285 | run: |
286 | sudo wget -O /usr/bin/qemu-loongarch64-static https://github.com/loongson/build-tools/releases/download/2025.02.21/qemu-loongarch64
287 | sudo chmod +x /usr/bin/qemu-loongarch64-static
288 | sudo mkdir -p /usr/libexec/qemu-binfmt
289 | sudo ln -s /usr/bin/qemu-loongarch64-static /usr/libexec/qemu-binfmt/loongarch64-binfmt-P
290 | sudo sh -c 'echo ":qemu-loongarch64:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02\x01:\xff\xff\xff\xff\xff\xff\xff\xfc\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/libexec/qemu-binfmt/loongarch64-binfmt-P:" > /proc/sys/fs/binfmt_misc/register'
291 |
292 | - name: 'Test on [linux] arch [loong64]'
293 | if: ${{ matrix.os == 'linux' && contains(fromJson('["loong64"]'), matrix.arch) }}
294 | env:
295 | GOOS: ${{ matrix.os }}
296 | GOARCH: ${{ matrix.arch }}
297 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
298 |
299 | # windows
300 | - name: 'Test on [windows] arch [386]'
301 | if: ${{ matrix.os == 'windows' && contains(fromJson('["386"]'), matrix.arch) }}
302 | env:
303 | GOOS: ${{ matrix.os }}
304 | GOARCH: ${{ matrix.arch }}
305 | run: go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
306 |
307 | - name: 'Test on [windows] arch [amd64]'
308 | if: ${{ matrix.os == 'windows' && contains(fromJson('["amd64"]'), matrix.arch) }}
309 | env:
310 | GOOS: ${{ matrix.os }}
311 | GOARCH: ${{ matrix.arch }}
312 | run: go test -v -race -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
313 |
314 | # freebsd
315 | - name: 'Build for [freebsd] arch [386, amd64]'
316 | if: ${{ matrix.os == 'freebsd' && contains(fromJson('["386", "amd64"]'), matrix.arch) }}
317 | env:
318 | GOOS: ${{ matrix.os }}
319 | GOARCH: ${{ matrix.arch }}
320 | run: go test -v -c -covermode=atomic -a -toolexec='routinex -v'
321 |
322 | - name: 'Test on [freebsd] arch [386, amd64]'
323 | if: ${{ matrix.os == 'freebsd' && contains(fromJson('["386", "amd64"]'), matrix.arch) }}
324 | uses: vmactions/freebsd-vm@v1
325 | with:
326 | run: ./routine.test -test.v -test.coverprofile='coverage.txt'
327 |
328 | # js
329 | - name: 'Setup Node.js on [js] arch [wasm]'
330 | if: ${{ matrix.os == 'js' && contains(fromJson('["wasm"]'), matrix.arch) }}
331 | uses: actions/setup-node@v4
332 | with:
333 | node-version: 18
334 |
335 | - name: 'Test on [js] arch [wasm]'
336 | if: ${{ matrix.os == 'js' && contains(fromJson('["wasm"]'), matrix.arch) }}
337 | env:
338 | GOOS: ${{ matrix.os }}
339 | GOARCH: ${{ matrix.arch }}
340 | run: |
341 | go_version=$(go env GOVERSION | cut -c3-)
342 | max_version=$(printf '%s\n' "$go_version" '1.24' | sort -V | tail -n1)
343 | if [ "$go_version" = "$max_version" ]; then
344 | PATH="$PATH:$(go env GOROOT)/lib/wasm"
345 | else
346 | PATH="$PATH:$(go env GOROOT)/misc/wasm"
347 | fi
348 | go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./...
349 |
350 | - name: Codecov
351 | uses: codecov/codecov-action@v5
352 | with:
353 | name: Codecov on ${{ matrix.os }}/${{ matrix.arch }} go${{ matrix.go }}
354 | token: ${{ secrets.CODECOV_TOKEN }}
355 | fail_ci_if_error: false
356 |
--------------------------------------------------------------------------------
/.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 |
17 | # IntelliJ
18 | .idea
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # v1.1.5 Release notes
4 |
5 | ### Bugs
6 |
7 | - Fix missing nil check for the underlying data when copying the context of `InheritableThreadLocal`.
8 |
9 | ### Features
10 |
11 | - Support go version range `go1.18` ~ `go1.24`(New support `go1.24`).
12 | - Add a new `static mode`, which improves performance by over `20%` and provides higher memory safety by adding the compilation parameter `-a -toolexec='routinex -v'`.
13 |
14 | ### Changes
15 |
16 | - Modify the `goid` type to `uint64`.
17 | - Update copyright.
18 |
19 | # Links
20 |
21 | - Source code [https://github.com/timandy/routine/tree/v1.1.5](https://github.com/timandy/routine/tree/v1.1.5)
22 |
23 | ---
24 |
25 | # v1.1.4 Release notes
26 |
27 | ### Features
28 |
29 | - Support go version range `go1.18` ~ `go1.23`(New support `go1.23`).
30 |
31 | ### Changes
32 |
33 | - Fix interface conversion error: getting nil value from ThreadLocal[T], where T is interface type.
34 | - Update copyright.
35 |
36 | # Links
37 |
38 | - Source code [https://github.com/timandy/routine/tree/v1.1.4](https://github.com/timandy/routine/tree/v1.1.4)
39 |
40 | ---
41 |
42 | # v1.1.3 Release notes
43 |
44 | ### Features
45 |
46 | - Support go version range `go1.18` ~ `go1.21`.
47 | - Support `generic` programming.
48 |
49 | # Links
50 |
51 | - Source code [https://github.com/timandy/routine/tree/v1.1.3](https://github.com/timandy/routine/tree/v1.1.3)
52 |
53 | ---
54 |
55 | # v1.1.2 Release notes
56 |
57 | ### Features
58 |
59 | - Support go version range `go1.13` ~ `go1.21`(New support `go1.21`).
60 | - Support capture values of `InheritableThreadLocal` by `WrapTask()`, `WrapWaitTask()` and `WrapWaitResultTask()` methods.
61 | - Support run `FutureTask` by `FutureTask.Run()` method.
62 | - Define function type `Runnable` and `FutureCallable`.
63 |
64 | ### Changes
65 |
66 | - Rename type `Future` to `FutureTask`.
67 | - Skip first runtime panic stack automatically for `RuntimeError`.
68 |
69 | # Links
70 |
71 | - Source code [https://github.com/timandy/routine/tree/v1.1.2](https://github.com/timandy/routine/tree/v1.1.2)
72 |
73 | ---
74 |
75 | # v1.1.1 Release notes
76 |
77 | ### Features
78 |
79 | - Support go version range `go1.13` ~ `go1.20`(New support `go1.20`).
80 |
81 | ### Changes
82 |
83 | - Fix a memory leak risk caused by Timer.
84 | - Reduce memory by using less chan.
85 | - Update copyright.
86 |
87 | # Links
88 |
89 | - Source code [https://github.com/timandy/routine/tree/v1.1.1](https://github.com/timandy/routine/tree/v1.1.1)
90 |
91 | ---
92 |
93 | # v1.1.0 Release notes
94 |
95 | ### Features
96 |
97 | - Support more arch `loong64`, `mips`, `mipsle`, `mips64`, `mips64le`, `ppc64le`, `riscv64`, `wasm`.
98 |
99 | ### Changes
100 |
101 | - Upgrade dependencies to the latest version.
102 | - Modify continuous integration script to support go1.19.
103 |
104 | # Links
105 |
106 | - Source code [https://github.com/timandy/routine/tree/v1.1.0](https://github.com/timandy/routine/tree/v1.1.0)
107 |
108 | ---
109 |
110 | # v1.0.9 Release notes
111 |
112 | ### Features
113 |
114 | - Support arch `386` & `amd64` on `freebsd` and arch `ppc64` & `s390x` on `linux`.
115 | - Support `Cancel()` and `GetWithTimeout()` methods for type `Future`.
116 | - Support checking whether the tasks created by `GoWait(CancelRunnable)` and `GoWaitResult(CancelCallable)` methods are canceled.
117 |
118 | ### Changes
119 |
120 | - Fix spell error of type `Future`.
121 | - Rename type `Any` to `any`.
122 |
123 | # Links
124 |
125 | - Source code [https://github.com/timandy/routine/tree/v1.0.9](https://github.com/timandy/routine/tree/v1.0.9)
126 |
127 | ---
128 |
129 | # v1.0.8 Release notes
130 |
131 | ### Changes
132 |
133 | - Rename `StackError` to `RuntimeError`.
134 | - Support error nesting for `RuntimeError`.
135 | - Beautify the error message of `RuntimeError`.
136 | - Remove `bytesconv.Bytes()` and `bytesconv.String()` methods.
137 | - Restore to the previous value if an overflow occurs when getting the index of `ThreadLocal`.
138 |
139 | # Links
140 |
141 | - Source code [https://github.com/timandy/routine/tree/v1.0.8](https://github.com/timandy/routine/tree/v1.0.8)
142 |
143 | ---
144 |
145 | # v1.0.7 Release notes
146 |
147 | ### Bugs
148 |
149 | - Fix released `thread` struct may be resurrected from invalid memory and cause fault error.
150 |
151 | ### Changes
152 |
153 | - Modify the error message format of `StackError`.
154 | - Define function type `Supplier` for `threadLocal` and `inheritableThreadLocal` types.
155 | - Define function type `Runnable` and `Callable` for `Go(Runnable)`, `GoWait(Runnable)` and `GoWaitResult(Callable)` methods.
156 |
157 | # Links
158 |
159 | - Source code [https://github.com/timandy/routine/tree/v1.0.7](https://github.com/timandy/routine/tree/v1.0.7)
160 |
161 | ---
162 |
163 | # v1.0.6 Release notes
164 |
165 | ### Bugs
166 |
167 | - Fix fault error when pprof is running.
168 |
169 | ### Features
170 |
171 | - Support more architectures `386`, `amd64`, `armv6`, `armv7`, `arm64`.
172 |
173 | ### Changes
174 |
175 | - Read and write `coroutine` information through the `gohack` library, theoretically support unreleased `go` versions in the future.
176 | - When `runtime.g` cannot be obtained natively, `panic` directly instead of falling back to invoke `runtime.Stack()` method.
177 | - Remove api `ThreadLocal.Id()`.
178 |
179 | # Links
180 |
181 | - Source code [https://github.com/timandy/routine/tree/v1.0.6](https://github.com/timandy/routine/tree/v1.0.6)
182 |
183 | ---
184 |
185 | # v1.0.5 Release notes
186 |
187 | ### Features
188 |
189 | - Support go version range `go1.13` ~ `go1.18`(New support `go1.18`).
190 |
191 | ### Changes
192 |
193 | - Change license to `Apache-2.0`.
194 | - Upgrade dependencies to the latest version.
195 |
196 | # Links
197 |
198 | - Source code [https://github.com/timandy/routine/tree/v1.0.5](https://github.com/timandy/routine/tree/v1.0.5)
199 |
200 | ---
201 |
202 | # v1.0.4 Release notes
203 |
204 | ### Features
205 |
206 | - Add zero-copy conversion method between `bytes` and `string`, see `bytesconv.Bytes()` and `bytesconv.String()` methods.
207 |
208 | ### Changes
209 |
210 | - Modify the garbage collection mechanism, remove `gcTimer`, no longer perform garbage collection through timers.
211 | - Store the context in the `g.labels` field of the coroutine structure which will be set to `nil` after coroutine ends. The context data will be collected at the next `GC`.
212 | - Use `go:linkname` to invoke assembly code `getg()` directly to improve performance.
213 | - Implement the `getGoidByStack()` method by invoke `http.http2curGoroutineID()`.
214 | - Remove api `AllGoids()` and `ForeachGoid()`.
215 |
216 | # Links
217 |
218 | - Source code [https://github.com/timandy/routine/tree/v1.0.4](https://github.com/timandy/routine/tree/v1.0.4)
219 |
220 | ---
221 |
222 | # v1.0.3 Release notes
223 |
224 | ### Features
225 |
226 | - Support copy `Cloneable` objects to sub goroutine when create sub goroutines by `Go()`, `GoWait()` and `GoWaitResult()` methods.
227 | - Add api `ForeachGoid(func(goid int64))` to run a func for each goid.
228 |
229 | ### Changes
230 |
231 | - Support go version range `go1.13` ~ `go1.17`(Not support `go1.12` anymore).
232 | - Use segment locks to reduce competition and improve `ThreadLocal`'s `read`, `write` and `gc` performance.
233 | - Get all goids through `runtime.allgs` instead of `runtime.atomicAllG`, so `go1.13` ~ `go1.15` can also get all goids natively.
234 |
235 | # Links
236 |
237 | - Source code [https://github.com/timandy/routine/tree/v1.0.3](https://github.com/timandy/routine/tree/v1.0.3)
238 |
239 | ---
240 |
241 | # v1.0.2 Release notes
242 |
243 | ### Bugs
244 |
245 | - Fix bug in `getAllGoidByStack()` method, Buffer may too small when dump all stack info.
246 |
247 | ### Features
248 |
249 | - Support initialize value when first get from `ThreadLocal`.
250 | - Add `StackError` to catch stack info.
251 | - Add `Feature` to wait goroutine finished or get result from goroutine.
252 | - Add api `NewThreadLocalWithInitial()`, `NewInheritableThreadLocal()` and `NewInheritableThreadLocalWithInitial()`.
253 | - Support Inherit values of `ThreadLocal` by `Go`, `GoWait()` and `GoWaitResult()`.
254 |
255 | ### Changes
256 |
257 | - Rename `LocalStorage` to `ThreadLocal`.
258 | - Remove api `Clear()`, `InheritContext()` and `RestoreContext()`.
259 | - Improve `gc` performance by reducing the number of for loops.
260 |
261 | # Links
262 |
263 | - Source code [https://github.com/timandy/routine/tree/v1.0.2](https://github.com/timandy/routine/tree/v1.0.2)
264 |
265 | ---
266 |
267 | # v1.0.1 Release notes
268 |
269 | ### Features
270 |
271 | - Improve performance by use slice to store goroutine local values.
272 | - Optimize `clearDeadStore()` method.
273 |
274 | # Links
275 |
276 | - Source code [https://github.com/timandy/routine/tree/v1.0.1](https://github.com/timandy/routine/tree/v1.0.1)
277 |
278 | ---
279 |
280 | # v1.0.0 Release notes
281 |
282 | `This is the first stable version available for production. It is highly recommended to upgrade to this version if you have used a previous version.`
283 |
284 | ### Bugs
285 |
286 | - Fix `NewLocalStorage()` always return the same value, so we can define multi `LocalStorage` instances.
287 | - Fix `NewLocalStorage()` clear other `LocalStorage`'s value.
288 | - Fix `RestoreContext()` not clear values when restore from empty `*ImmutableContext`.
289 |
290 | ### Features
291 |
292 | - Not force create `store` when invoke `Get()`, `Remove()`, `Clear()`, `BackupContext()` methods to reduce memory usage.
293 |
294 | ### Changes
295 |
296 | - Rename `InheritContext()` to `RestoreContext()`.
297 | - Rename `Del()` to `Remove()`.
298 | - Move Clear() method to `routine` package.
299 |
300 | # Links
301 |
302 | - Source code [https://github.com/timandy/routine/tree/v1.0.0](https://github.com/timandy/routine/tree/v1.0.0)
303 |
304 | ---
305 |
306 | # v0.0.2 Release notes
307 |
308 | ### Features
309 |
310 | - Support go version range `go1.12` ~ `go1.17`(New support `go1.17`).
311 | - Enable GitHub actions for continuous integration.
312 |
313 | ### Known Issues
314 |
315 | - `NewLocalStorage()` always return the same value.
316 |
317 | # Links
318 |
319 | - Source code [https://github.com/timandy/routine/tree/v0.0.2](https://github.com/timandy/routine/tree/v0.0.2)
320 |
321 | ---
322 |
323 | # v0.0.1 Release notes
324 |
325 | ### Features
326 |
327 | - Support go version range `go1.12` ~ `go1.16`.
328 | - Support `Goid()` to get current goroutine id.
329 | - Support `AllGoids` to get all goroutine ids.
330 | - Support `ThreadLocal` to save values ingo to goroutine.
331 |
332 | ### Known Issues
333 |
334 | - `NewLocalStorage()` always return the same value.
335 |
336 | # Links
337 |
338 | - Source code [https://github.com/timandy/routine/tree/v0.0.1](https://github.com/timandy/routine/tree/v0.0.1)
339 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8 |
9 | ## Our Standards
10 |
11 | Examples of behavior that contributes to a positive environment for our community include:
12 |
13 | * Demonstrating empathy and kindness toward other people
14 | * Being respectful of differing opinions, viewpoints, and experiences
15 | * Giving and gracefully accepting constructive feedback
16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17 | * Focusing on what is best not just for us as individuals, but for the overall community
18 |
19 | Examples of unacceptable behavior include:
20 |
21 | * The use of sexualized language or imagery, and sexual attention or advances of any kind
22 | * Trolling, insulting or derogatory comments, and personal or political attacks
23 | * Public or private harassment
24 | * Publishing others' private information, such as a physical or email address, without their explicit permission
25 | * Other conduct which could reasonably be considered inappropriate in a professional setting
26 |
27 | ## Enforcement Responsibilities
28 |
29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
32 |
33 | ## Scope
34 |
35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces.
36 | Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
37 |
38 | ## Enforcement
39 |
40 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Tim Andy](mailto:xuchonglei@126.com).
41 | All complaints will be reviewed and investigated promptly and fairly.
42 |
43 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
44 |
45 | ## Enforcement Guidelines
46 |
47 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
48 |
49 | ### 1. Correction
50 |
51 | **Community Impact**:
52 | Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53 |
54 | **Consequence**:
55 | A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate.
56 | A public apology may be requested.
57 |
58 | ### 2. Warning
59 |
60 | **Community Impact**:
61 | A violation through a single incident or series of actions.
62 |
63 | **Consequence**:
64 | A warning with consequences for continued behavior.
65 | No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time.
66 | This includes avoiding interactions in community spaces as well as external channels like social media.
67 | Violating these terms may lead to a temporary or permanent ban.
68 |
69 | ### 3. Temporary Ban
70 |
71 | **Community Impact**:
72 | A serious violation of community standards, including sustained inappropriate behavior.
73 |
74 | **Consequence**:
75 | A temporary ban from any sort of interaction or public communication with the community for a specified period of time.
76 | No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period.
77 | Violating these terms may lead to a permanent ban.
78 |
79 | ### 4. Permanent Ban
80 |
81 | **Community Impact**:
82 | Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
83 |
84 | **Consequence**:
85 | A permanent ban from any sort of public interaction within the community.
86 |
87 | ## Attribution
88 |
89 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
90 |
91 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
92 |
93 | [homepage]: https://www.contributor-covenant.org
94 |
95 | For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq.
96 | Translations are available at https://www.contributor-covenant.org/translations.
97 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | - With issues:
4 | - Use the search tool before opening a new issue.
5 | - Please provide source code and commit sha if you found a bug.
6 | - Review existing issues and provide feedback or react to them.
7 |
8 | - With pull requests:
9 | - Open your pull request against `main` branch.
10 | - It should pass all tests in the available continuous integration systems such as GitHub Actions.
11 | - You should add/modify tests to cover your proposed code changes.
12 | - If your pull request contains a new feature, please document it on the README.md and README_zh.md.
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | https://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2021-2025 TimAndy
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | https://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # routine
2 |
3 | [](https://github.com/timandy/routine/actions)
4 | [](https://app.codecov.io/gh/timandy/routine)
5 | [](https://goreportcard.com/report/github.com/timandy/routine)
6 | [](https://pkg.go.dev/github.com/timandy/routine)
7 | [](https://github.com/timandy/routine/releases)
8 | [](https://github.com/timandy/routine/blob/main/LICENSE)
9 |
10 | > [中文版](README_zh.md)
11 |
12 | `routine` encapsulates and provides some easy-to-use, non-competitive, high-performance `goroutine` context access interfaces, which can help you access coroutine context information more gracefully.
13 |
14 | # :house:Introduce
15 |
16 | From the very beginning of its design, the `Golang` language has spared no effort to shield the concept of coroutine context from developers, including the acquisition of coroutine `goid`, the state of coroutine within the process, and the storage of coroutine context.
17 |
18 | If you have used other languages such as `C++`, `Java` and so on, then you must be familiar with `ThreadLocal`, but after starting to use `Golang`, you will be deeply confused and distressed by the lack of convenient functions like `ThreadLocal`.
19 |
20 | Of course, you can choose to use `Context`, which carries all the context information, appears in the first input parameter of all functions, and then shuttles around your system.
21 |
22 | And the core goal of `routine` is to open up another way: Introduce `goroutine local storage` to the `Golang` world.
23 |
24 | # :loudspeaker:Update Notice
25 |
26 | :fire:**Version `1.1.5` introduces a new static mode.**
27 |
28 | - :rocket:Performance improved by over `20%`.
29 |
30 | - :rocket:Memory access is now safer.
31 |
32 | - :exclamation:The compile command requires additional parameters `-a -toolexec='routinex -v'`.
33 |
34 | For more details, visit: [RoutineX Compiler](https://github.com/timandy/routinex)
35 |
36 | # :hammer_and_wrench:Usage & Demo
37 |
38 | This chapter briefly introduces how to install and use the `routine` library.
39 |
40 | ## Install
41 |
42 | ```bash
43 | go get github.com/timandy/routine
44 | ```
45 |
46 | ## Use `goid`
47 |
48 | The following code simply demonstrates the use of `routine.Goid()`:
49 |
50 | ```go
51 | package main
52 |
53 | import (
54 | "fmt"
55 | "time"
56 |
57 | "github.com/timandy/routine"
58 | )
59 |
60 | func main() {
61 | goid := routine.Goid()
62 | fmt.Printf("cur goid: %v\n", goid)
63 | go func() {
64 | goid := routine.Goid()
65 | fmt.Printf("sub goid: %v\n", goid)
66 | }()
67 |
68 | // Wait for the sub-coroutine to finish executing.
69 | time.Sleep(time.Second)
70 | }
71 | ```
72 |
73 | In this example, the `main` function starts a new coroutine, so `Goid()` returns the main coroutine `1` and the child coroutine `6`:
74 |
75 | ```text
76 | cur goid: 1
77 | sub goid: 6
78 | ```
79 |
80 | ## Use `ThreadLocal`
81 |
82 | The following code briefly demonstrates `ThreadLocal`'s creation, setting, getting, spreading across coroutines, etc.:
83 |
84 | ```go
85 | package main
86 |
87 | import (
88 | "fmt"
89 | "time"
90 |
91 | "github.com/timandy/routine"
92 | )
93 |
94 | var threadLocal = routine.NewThreadLocal[string]()
95 | var inheritableThreadLocal = routine.NewInheritableThreadLocal[string]()
96 |
97 | func main() {
98 | threadLocal.Set("hello world")
99 | inheritableThreadLocal.Set("Hello world2")
100 | fmt.Println("threadLocal:", threadLocal.Get())
101 | fmt.Println("inheritableThreadLocal:", inheritableThreadLocal.Get())
102 |
103 | // The child coroutine cannot read the previously assigned "hello world".
104 | go func() {
105 | fmt.Println("threadLocal in goroutine:", threadLocal.Get())
106 | fmt.Println("inheritableThreadLocal in goroutine:", inheritableThreadLocal.Get())
107 | }()
108 |
109 | // However, a new sub-coroutine can be started via the Go/GoWait/GoWaitResult function, and all inheritable variables of the current coroutine can be passed automatically.
110 | routine.Go(func() {
111 | fmt.Println("threadLocal in goroutine by Go:", threadLocal.Get())
112 | fmt.Println("inheritableThreadLocal in goroutine by Go:", inheritableThreadLocal.Get())
113 | })
114 |
115 | // You can also create a task via the WrapTask/WrapWaitTask/WrapWaitResultTask function, and all inheritable variables of the current coroutine can be automatically captured.
116 | task := routine.WrapTask(func() {
117 | fmt.Println("threadLocal in task by WrapTask:", threadLocal.Get())
118 | fmt.Println("inheritableThreadLocal in task by WrapTask:", inheritableThreadLocal.Get())
119 | })
120 | go task.Run()
121 |
122 | // Wait for the sub-coroutine to finish executing.
123 | time.Sleep(time.Second)
124 | }
125 | ```
126 |
127 | The execution result is:
128 |
129 | ```text
130 | threadLocal: hello world
131 | inheritableThreadLocal: Hello world2
132 | threadLocal in goroutine:
133 | inheritableThreadLocal in goroutine:
134 | threadLocal in goroutine by Go:
135 | inheritableThreadLocal in goroutine by Go: Hello world2
136 | threadLocal in task by WrapTask:
137 | inheritableThreadLocal in task by WrapTask: Hello world2
138 | ```
139 |
140 | # :books:API
141 |
142 | This chapter introduces in detail all the interfaces encapsulated by the `routine` library, as well as their core functions and implementation methods.
143 |
144 | ## `Goid() uint64`
145 |
146 | Get the `goid` of the current `goroutine`.
147 |
148 | It can be obtained directly through assembly code under `386`, `amd64`, `armv6`, `armv7`, `arm64`, `loong64`, `mips`, `mipsle`, `mips64`, `mips64le`, `ppc64`, `ppc64le`, `riscv64`, `s390x`, `wasm` architectures. This operation has extremely high performance and the time-consuming is usually only one-fifth of `rand.Int()`.
149 |
150 | ## `NewThreadLocal[T any]() ThreadLocal[T]`
151 |
152 | Create a new `ThreadLocal[T]` instance with the initial value stored with the default value of type `T`.
153 |
154 | ## `NewThreadLocalWithInitial[T any](supplier Supplier[T]) ThreadLocal[T]`
155 |
156 | Create a new `ThreadLocal[T]` instance with the initial value stored as the return value of the method `supplier()`.
157 |
158 | ## `NewInheritableThreadLocal[T any]() ThreadLocal[T]`
159 |
160 | Create a new `ThreadLocal[T]` instance with the initial value stored with the default value of type `T`.
161 | When a new coroutine is started via `Go()`, `GoWait()` or `GoWaitResult()`, the value of the current coroutine is copied to the new coroutine.
162 | When a new task is created via `WrapTask()`, `WrapWaitTask()` or `WrapWaitResultTask()`, the value of the current coroutine is captured to the new task.
163 |
164 | ## `NewInheritableThreadLocalWithInitial[T any](supplier Supplier[T]) ThreadLocal[T]`
165 |
166 | Create a new `ThreadLocal[T]` instance with the initial value stored as the return value of the method `supplier()`.
167 | When a new coroutine is started via `Go()`, `GoWait()` or `GoWaitResult()`, the value of the current coroutine is copied to the new coroutine.
168 | When a new task is created via `WrapTask()`, `WrapWaitTask()` or `WrapWaitResultTask()`, the value of the current coroutine is captured to the new task.
169 |
170 | ## `WrapTask(fun Runnable) FutureTask[any]`
171 |
172 | Create a new task and capture the `inheritableThreadLocals` from the current goroutine.
173 | This function returns a `FutureTask` instance, but the return task will not run automatically.
174 | You can run it in a sub-goroutine or goroutine-pool by `FutureTask.Run()` method, wait by `FutureTask.Get()` or `FutureTask.GetWithTimeout()` method.
175 | When the returned task run `panic` will be caught and error stack will be printed, the `panic` will be trigger again when calling `FutureTask.Get()` or `FutureTask.GetWithTimeout()` method.
176 |
177 | ## `WrapWaitTask(fun CancelRunnable) FutureTask[any]`
178 |
179 | Create a new task and capture the `inheritableThreadLocals` from the current goroutine.
180 | This function returns a `FutureTask` instance, but the return task will not run automatically.
181 | You can run it in a sub-goroutine or goroutine-pool by `FutureTask.Run()` method, wait by `FutureTask.Get()` or `FutureTask.GetWithTimeout()` method.
182 | When the returned task run `panic` will be caught, the `panic` will be trigger again when calling `FutureTask.Get()` or `FutureTask.GetWithTimeout()` method.
183 |
184 | ## `WrapWaitResultTask[TResult any](fun CancelCallable[TResult]) FutureTask[TResult]`
185 |
186 | Create a new task and capture the `inheritableThreadLocals` from the current goroutine.
187 | This function returns a `FutureTask` instance, but the return task will not run automatically.
188 | You can run it in a sub-goroutine or goroutine-pool by `FutureTask.Run()` method, wait and get result by `FutureTask.Get()` or `FutureTask.GetWithTimeout()` method.
189 | When the returned task run `panic` will be caught, the `panic` will be trigger again when calling `FutureTask.Get()` or `FutureTask.GetWithTimeout()` method.
190 |
191 | ## `Go(fun Runnable)`
192 |
193 | Start a new coroutine and automatically copy all contextual `inheritableThreadLocals` data of the current coroutine to the new coroutine.
194 | Any `panic` while the child coroutine is executing will be caught and the stack automatically printed.
195 |
196 | ## `GoWait(fun CancelRunnable) FutureTask[any]`
197 |
198 | Start a new coroutine and automatically copy all contextual `inheritableThreadLocals` data of the current coroutine to the new coroutine.
199 | You can wait for the sub-coroutine to finish executing through the `FutureTask.Get()` or `FutureTask.GetWithTimeout()` method that returns a value.
200 | Any `panic` while the child coroutine is executing will be caught and thrown again when `FutureTask.Get()` or `FutureTask.GetWithTimeout()` is called.
201 |
202 | ## `GoWaitResult[TResult any](fun CancelCallable[TResult]) FutureTask[TResult]`
203 |
204 | Start a new coroutine and automatically copy all contextual `inheritableThreadLocals` data of the current coroutine to the new coroutine.
205 | You can wait for the sub-coroutine to finish executing and get the return value through the `FutureTask.Get()` or `FutureTask.GetWithTimeout()` method of the return value.
206 | Any `panic` while the child coroutine is executing will be caught and thrown again when `FutureTask.Get()` or `FutureTask.GetWithTimeout()` is called.
207 |
208 | [More API Documentation](https://pkg.go.dev/github.com/timandy/routine#section-documentation)
209 |
210 | # :wastebasket:Garbage Collection
211 |
212 | `routine` allocates a `thread` structure for each coroutine, which stores context variable information related to the coroutine.
213 |
214 | A pointer to this structure is stored on the `g.labels` field of the coroutine structure.
215 |
216 | When the coroutine finishes executing and exits, `g.labels` will be set to `nil`, no longer referencing the `thread` structure.
217 |
218 | The `thread` structure will be collected at the next `GC`.
219 |
220 | If the data stored in `thread` is not additionally referenced, these data will be collected together.
221 |
222 | # :globe_with_meridians:Support Grid
223 |
224 | | | **`darwin`** | **`linux`** | **`windows`** | **`freebsd`** | **`js`** | |
225 | |---------------:|:------------:|:-----------:|:-------------:|:-------------:|:--------:|:---------------|
226 | | **`386`** | | ✅ | ✅ | ✅ | | **`386`** |
227 | | **`amd64`** | ✅ | ✅ | ✅ | ✅ | | **`amd64`** |
228 | | **`armv6`** | | ✅ | | | | **`armv6`** |
229 | | **`armv7`** | | ✅ | | | | **`armv7`** |
230 | | **`arm64`** | ✅ | ✅ | | | | **`arm64`** |
231 | | **`loong64`** | | ✅ | | | | **`loong64`** |
232 | | **`mips`** | | ✅ | | | | **`mips`** |
233 | | **`mipsle`** | | ✅ | | | | **`mipsle`** |
234 | | **`mips64`** | | ✅ | | | | **`mips64`** |
235 | | **`mips64le`** | | ✅ | | | | **`mips64le`** |
236 | | **`ppc64`** | | ✅ | | | | **`ppc64`** |
237 | | **`ppc64le`** | | ✅ | | | | **`ppc64le`** |
238 | | **`riscv64`** | | ✅ | | | | **`riscv64`** |
239 | | **`s390x`** | | ✅ | | | | **`s390x`** |
240 | | **`wasm`** | | | | | ✅ | **`wasm`** |
241 | | | **`darwin`** | **`linux`** | **`windows`** | **`freebsd`** | **`js`** | |
242 |
243 | ✅: Supported
244 |
245 | # :pray:Thanks
246 |
247 | Thanks to all [contributors](https://github.com/timandy/routine/graphs/contributors) for their contributions!
248 |
249 | # :scroll:*License*
250 |
251 | `routine` is released under the [Apache License 2.0](LICENSE).
252 |
253 | ```
254 | Copyright 2021-2025 TimAndy
255 |
256 | Licensed under the Apache License, Version 2.0 (the "License");
257 | you may not use this file except in compliance with the License.
258 | You may obtain a copy of the License at
259 |
260 | https://www.apache.org/licenses/LICENSE-2.0
261 |
262 | Unless required by applicable law or agreed to in writing, software
263 | distributed under the License is distributed on an "AS IS" BASIS,
264 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
265 | See the License for the specific language governing permissions and
266 | limitations under the License.
267 | ```
268 |
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | # routine
2 |
3 | [](https://github.com/timandy/routine/actions)
4 | [](https://app.codecov.io/gh/timandy/routine)
5 | [](https://goreportcard.com/report/github.com/timandy/routine)
6 | [](https://pkg.go.dev/github.com/timandy/routine)
7 | [](https://github.com/timandy/routine/releases)
8 | [](https://github.com/timandy/routine/blob/main/LICENSE)
9 |
10 | > [English Version](README.md)
11 |
12 | `routine`封装并提供了一些易用、无竞争、高性能的`goroutine`上下文访问接口,它可以帮助你更优雅地访问协程上下文信息。
13 |
14 | # :house:介绍
15 |
16 | `Golang`语言从设计之初,就一直在不遗余力地向开发者屏蔽协程上下文的概念,包括协程`goid`的获取、进程内部协程状态、协程上下文存储等。
17 |
18 | 如果你使用过其他语言如`C++`、`Java`等,那么你一定很熟悉`ThreadLocal`,而在开始使用`Golang`之后,你一定会为缺少类似`ThreadLocal`的便捷功能而深感困惑与苦恼。
19 |
20 | 当然你可以选择使用`Context`,让它携带着全部上下文信息,在所有函数的第一个输入参数中出现,然后在你的系统中到处穿梭。
21 |
22 | 而`routine`的核心目标就是开辟另一条路:将`goroutine local storage`引入`Golang`世界。
23 |
24 | # :loudspeaker:更新提示
25 |
26 | :fire:**`1.1.5`版本引入了全新的静态模式。**
27 |
28 | - :rocket:性能提升超过`20%`。
29 |
30 | - :rocket:内存访问变得更加安全。
31 |
32 | - :exclamation:编译命令需要额外的参数`-a -toolexec='routinex -v'`。
33 |
34 | 详细信息,请访问:[RoutineX 编译器](https://github.com/timandy/routinex)
35 |
36 | # :hammer_and_wrench:使用演示
37 |
38 | 此章节简要介绍如何安装与使用`routine`库。
39 |
40 | ## 安装
41 |
42 | ```bash
43 | go get github.com/timandy/routine
44 | ```
45 |
46 | ## 使用`goid`
47 |
48 | 以下代码简单演示了`routine.Goid()`的使用:
49 |
50 | ```go
51 | package main
52 |
53 | import (
54 | "fmt"
55 | "time"
56 |
57 | "github.com/timandy/routine"
58 | )
59 |
60 | func main() {
61 | goid := routine.Goid()
62 | fmt.Printf("cur goid: %v\n", goid)
63 | go func() {
64 | goid := routine.Goid()
65 | fmt.Printf("sub goid: %v\n", goid)
66 | }()
67 |
68 | // 等待子协程执行完。
69 | time.Sleep(time.Second)
70 | }
71 | ```
72 |
73 | 此例中`main`函数启动了一个新的协程,因此`Goid()`返回了主协程`1`和子协程`6`:
74 |
75 | ```text
76 | cur goid: 1
77 | sub goid: 6
78 | ```
79 |
80 | ## 使用`ThreadLocal`
81 |
82 | 以下代码简单演示了`ThreadLocal`的创建、设置、获取、跨协程传播等:
83 |
84 | ```go
85 | package main
86 |
87 | import (
88 | "fmt"
89 | "time"
90 |
91 | "github.com/timandy/routine"
92 | )
93 |
94 | var threadLocal = routine.NewThreadLocal[string]()
95 | var inheritableThreadLocal = routine.NewInheritableThreadLocal[string]()
96 |
97 | func main() {
98 | threadLocal.Set("hello world")
99 | inheritableThreadLocal.Set("Hello world2")
100 | fmt.Println("threadLocal:", threadLocal.Get())
101 | fmt.Println("inheritableThreadLocal:", inheritableThreadLocal.Get())
102 |
103 | // 子协程无法读取之前赋值的“hello world”。
104 | go func() {
105 | fmt.Println("threadLocal in goroutine:", threadLocal.Get())
106 | fmt.Println("inheritableThreadLocal in goroutine:", inheritableThreadLocal.Get())
107 | }()
108 |
109 | // 但是,可以通过 Go/GoWait/GoWaitResult 函数启动一个新的子协程,当前协程的所有可继承变量都可以自动传递。
110 | routine.Go(func() {
111 | fmt.Println("threadLocal in goroutine by Go:", threadLocal.Get())
112 | fmt.Println("inheritableThreadLocal in goroutine by Go:", inheritableThreadLocal.Get())
113 | })
114 |
115 | // 也可以通过 WrapTask/WrapWaitTask/WrapWaitResultTask 函数创建一个任务,当前协程的所有可继承变量都可以被自动捕获。
116 | task := routine.WrapTask(func() {
117 | fmt.Println("threadLocal in task by WrapTask:", threadLocal.Get())
118 | fmt.Println("inheritableThreadLocal in task by WrapTask:", inheritableThreadLocal.Get())
119 | })
120 | go task.Run()
121 |
122 | // 等待子协程执行完。
123 | time.Sleep(time.Second)
124 | }
125 | ```
126 |
127 | 执行结果为:
128 |
129 | ```text
130 | threadLocal: hello world
131 | inheritableThreadLocal: Hello world2
132 | threadLocal in goroutine:
133 | inheritableThreadLocal in goroutine:
134 | threadLocal in goroutine by Go:
135 | inheritableThreadLocal in goroutine by Go: Hello world2
136 | threadLocal in task by WrapTask:
137 | inheritableThreadLocal in task by WrapTask: Hello world2
138 | ```
139 |
140 | # :books:API文档
141 |
142 | 此章节详细介绍了`routine`库封装的全部接口,以及它们的核心功能、实现方式等。
143 |
144 | ## `Goid() uint64`
145 |
146 | 获取当前`goroutine`的`goid`。
147 |
148 | 在`386`、`amd64`、`armv6`、`armv7`、`arm64`、`loong64`、`mips`、`mipsle`、`mips64`、`mips64le`、`ppc64`、`ppc64le`、`riscv64`、`s390x`、`wasm`架构下通过汇编代码直接获取,此操作性能极高,耗时通常只相当于`rand.Int()`的五分之一。
149 |
150 | ## `NewThreadLocal[T any]() ThreadLocal[T]`
151 |
152 | 创建一个新的`ThreadLocal[T]`实例,其存储的初始值为类型`T`的默认值。
153 |
154 | ## `NewThreadLocalWithInitial[T any](supplier Supplier[T]) ThreadLocal[T]`
155 |
156 | 创建一个新的`ThreadLocal[T]`实例,其存储的初始值为方法`supplier()`的返回值。
157 |
158 | ## `NewInheritableThreadLocal[T any]() ThreadLocal[T]`
159 |
160 | 创建一个新的`ThreadLocal[T]`实例,其存储的初始值为类型`T`的默认值。
161 | 当通过`Go()`、`GoWait()`或`GoWaitResult()`启动新协程时,当前协程的值会被复制到新协程。
162 | 当通过`WrapTask()`、`WrapWaitTask()`或`WrapWaitResultTask()`创建任务时,当前协程的值会被捕获。
163 |
164 | ## `NewInheritableThreadLocalWithInitial[T any](supplier Supplier[T]) ThreadLocal[T]`
165 |
166 | 创建一个新的`ThreadLocal[T]`实例,其存储的初始值为方法`supplier()`的返回值。
167 | 当通过`Go()`、`GoWait()`或`GoWaitResult()`启动新协程时,当前协程的值会被复制到新协程。
168 | 当通过`WrapTask()`、`WrapWaitTask()`或`WrapWaitResultTask()`创建任务时,当前协程的值会被捕获。
169 |
170 | ## `WrapTask(fun Runnable) FutureTask[any]`
171 |
172 | 创建一个新任务,并捕获当前协程的`inheritableThreadLocals`。
173 | 此函数返回一个`FutureTask`实例,但返回的任务不会自动运行。
174 | 你可以通过`FutureTask.Run()`方法在子协程或协程池中运行它,通过`FutureTask.Get()`或`FutureTask.GetWithTimeout()`方法等待任务执行完毕。
175 | 任务执行时的任何`panic`都会被捕获并打印错误堆栈,在调用`FutureTask.Get()`或`FutureTask.GetWithTimeout()`方法时`panic`会被再次抛出。
176 |
177 | ## `WrapWaitTask(fun CancelRunnable) FutureTask[any]`
178 |
179 | 创建一个新任务,并捕获当前协程的`inheritableThreadLocals`。
180 | 此函数返回一个`FutureTask`实例,但返回的任务不会自动运行。
181 | 你可以通过`FutureTask.Run()`方法在子协程或协程池中运行它,通过`FutureTask.Get()`或`FutureTask.GetWithTimeout()`方法等待任务执行完毕。
182 | 任务执行时的任何`panic`都会被捕获,在调用`FutureTask.Get()`或`FutureTask.GetWithTimeout()`方法时`panic`会被再次抛出。
183 |
184 | ## `WrapWaitResultTask[TResult any](fun CancelCallable[TResult]) FutureTask[TResult]`
185 |
186 | 创建一个新任务,并捕获当前协程的`inheritableThreadLocals`。
187 | 此函数返回一个`FutureTask`实例,但返回的任务不会自动运行。
188 | 你可以通过`FutureTask.Run()`方法在子协程或协程池中运行它,通过`FutureTask.Get()`或`FutureTask.GetWithTimeout()`方法等待任务执行完毕并获取结果。
189 | 任务执行时的任何`panic`都会被捕获,在调用`FutureTask.Get()`或`FutureTask.GetWithTimeout()`方法时`panic`会被再次抛出。
190 |
191 | ## `Go(fun Runnable)`
192 |
193 | 启动一个新的协程,同时自动将当前协程的全部上下文`inheritableThreadLocals`数据复制至新协程。
194 | 子协程执行时的任何`panic`都会被捕获并自动打印堆栈。
195 |
196 | ## `GoWait(fun CancelRunnable) FutureTask[any]`
197 |
198 | 启动一个新的协程,同时自动将当前协程的全部上下文`inheritableThreadLocals`数据复制至新协程。
199 | 可以通过返回值的`FutureTask.Get()`或`FutureTask.GetWithTimeout()`方法等待子协程执行完毕。
200 | 子协程执行时的任何`panic`都会被捕获并在调用`FutureTask.Get()`或`FutureTask.GetWithTimeout()`时再次抛出。
201 |
202 | ## `GoWaitResult[TResult any](fun CancelCallable[TResult]) FutureTask[TResult]`
203 |
204 | 启动一个新的协程,同时自动将当前协程的全部上下文`inheritableThreadLocals`数据复制至新协程。
205 | 可以通过返回值的`FutureTask.Get()`或`FutureTask.GetWithTimeout()`方法等待子协程执行完毕并获取返回值。
206 | 子协程执行时的任何`panic`都会被捕获并在调用`FutureTask.Get()`或`FutureTask.GetWithTimeout()`时再次抛出。
207 |
208 | [更多API文档](https://pkg.go.dev/github.com/timandy/routine#section-documentation)
209 |
210 | # :wastebasket:垃圾回收
211 |
212 | `routine`为每个协程分配了一个`thread`结构,它存储了协程相关的上下文变量信息。
213 |
214 | 指向该结构的指针存储在协程结构的`g.labels`字段上。
215 |
216 | 当协程执行完毕退出时,`g.labels`将被设置为`nil`,不再引用`thread`结构。
217 |
218 | `thread`结构将在下次`GC`时被回收。
219 |
220 | 如果`thread`中存储的数据也没有额外被引用,这些数据将被一并回收。
221 |
222 | # :globe_with_meridians:支持网格
223 |
224 | | | **`darwin`** | **`linux`** | **`windows`** | **`freebsd`** | **`js`** | |
225 | |---------------:|:------------:|:-----------:|:-------------:|:-------------:|:--------:|:---------------|
226 | | **`386`** | | ✅ | ✅ | ✅ | | **`386`** |
227 | | **`amd64`** | ✅ | ✅ | ✅ | ✅ | | **`amd64`** |
228 | | **`armv6`** | | ✅ | | | | **`armv6`** |
229 | | **`armv7`** | | ✅ | | | | **`armv7`** |
230 | | **`arm64`** | ✅ | ✅ | | | | **`arm64`** |
231 | | **`loong64`** | | ✅ | | | | **`loong64`** |
232 | | **`mips`** | | ✅ | | | | **`mips`** |
233 | | **`mipsle`** | | ✅ | | | | **`mipsle`** |
234 | | **`mips64`** | | ✅ | | | | **`mips64`** |
235 | | **`mips64le`** | | ✅ | | | | **`mips64le`** |
236 | | **`ppc64`** | | ✅ | | | | **`ppc64`** |
237 | | **`ppc64le`** | | ✅ | | | | **`ppc64le`** |
238 | | **`riscv64`** | | ✅ | | | | **`riscv64`** |
239 | | **`s390x`** | | ✅ | | | | **`s390x`** |
240 | | **`wasm`** | | | | | ✅ | **`wasm`** |
241 | | | **`darwin`** | **`linux`** | **`windows`** | **`freebsd`** | **`js`** | |
242 |
243 | ✅:支持
244 |
245 | # :pray:鸣谢
246 |
247 | 感谢所有[贡献者](https://github.com/timandy/routine/graphs/contributors)的贡献!
248 |
249 | # :scroll:*许可证*
250 |
251 | `routine`是在 [Apache License 2.0](LICENSE) 下发布的。
252 |
253 | ```
254 | Copyright 2021-2025 TimAndy
255 |
256 | Licensed under the Apache License, Version 2.0 (the "License");
257 | you may not use this file except in compliance with the License.
258 | You may obtain a copy of the License at
259 |
260 | https://www.apache.org/licenses/LICENSE-2.0
261 |
262 | Unless required by applicable law or agreed to in writing, software
263 | distributed under the License is distributed on an "AS IS" BASIS,
264 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
265 | See the License for the specific language governing permissions and
266 | limitations under the License.
267 | ```
268 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | At the moment, only the latest commit on the `main` branch will be supported for security vulnerabilities.
6 |
7 | | **Branch** | **Supported** |
8 | |:----------:|:-------------:|
9 | | `main` | ✅ |
10 |
11 | ## Reporting a Vulnerability
12 |
13 | **Please do not report security vulnerabilities through public GitHub issues.**
14 |
15 | If you found a security vulnerability in the current repository, please send a mail to [Tim Andy](mailto:xuchonglei@126.com).
16 | You should get a reply within *72 hours* that we have received your report and a tentative [CVSS](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator) score.
17 | We will do a preliminary analysis to confirm that the vulnerability is a plausible claim and decline the report otherwise.
18 |
19 | If possible, please include:
20 |
21 | 1. reproducible steps on how to trigger the vulnerability.
22 | 2. a description on why you are convinced that it exists.
23 | 3. any information you may have on active exploitation of the vulnerability.
24 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | v1.1.5
--------------------------------------------------------------------------------
/api_cloneable.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | // Cloneable interface to support copy itself.
4 | type Cloneable interface {
5 | // Clone create and returns a copy of this object.
6 | Clone() any
7 | }
8 |
--------------------------------------------------------------------------------
/api_cloneable_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestCloneable(t *testing.T) {
10 | //struct can not be cast to interface
11 | var value any = personCloneable{Id: 1, Name: "Hello"}
12 | _, ok := value.(Cloneable)
13 | assert.False(t, ok)
14 | //pointer can be cast to interface
15 | var pointer any = &personCloneable{Id: 1, Name: "Hello"}
16 | _, ok2 := pointer.(Cloneable)
17 | assert.True(t, ok2)
18 | //nil pointer can be cast to interface
19 | pointer = (*personCloneable)(nil)
20 | cloneable, ok3 := pointer.(Cloneable)
21 | assert.True(t, ok3)
22 | assert.True(t, cloneable != nil)
23 | }
24 |
25 | func TestCloneable_Clone(t *testing.T) {
26 | //clone struct
27 | pc := &personCloneable{Id: 1, Name: "Hello"}
28 | assert.NotSame(t, pc, pc.Clone())
29 | assert.Equal(t, *pc, *(pc.Clone().(*personCloneable)))
30 | //copy pointer
31 | pcs := make([]*personCloneable, 1)
32 | pcs[0] = pc
33 | pcs2 := make([]*personCloneable, 1)
34 | copy(pcs2, pcs)
35 | assert.Same(t, pc, pcs2[0])
36 | //clone nil panic
37 | assert.Panics(t, func() {
38 | pc2 := (*personCloneable)(nil)
39 | _ = pc2.Clone()
40 | })
41 | }
42 |
43 | type personCloneable struct {
44 | Id int
45 | Name string
46 | }
47 |
48 | func (p *personCloneable) Clone() any {
49 | return &personCloneable{Id: p.Id, Name: p.Name}
50 | }
51 |
--------------------------------------------------------------------------------
/api_error.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | // RuntimeError runtime error with stack info.
4 | type RuntimeError interface {
5 | // Goid returns the goid of the coroutine that created the current error.
6 | Goid() uint64
7 |
8 | // Gopc returns the pc of go statement that created the current error coroutine.
9 | Gopc() uintptr
10 |
11 | // Message returns the detail message string of this error.
12 | Message() string
13 |
14 | // StackTrace returns an array of stack trace elements, each representing one stack frame.
15 | StackTrace() []uintptr
16 |
17 | // Cause returns the cause of this error or nil if the cause is nonexistent or unknown.
18 | Cause() RuntimeError
19 |
20 | // Error returns a short description of this error.
21 | Error() string
22 | }
23 |
24 | // NewRuntimeError create a new RuntimeError instance.
25 | func NewRuntimeError(cause any) RuntimeError {
26 | goid, gopc, msg, stackTrace, innerErr := runtimeErrorNew(cause)
27 | return &runtimeError{goid: goid, gopc: gopc, message: msg, stackTrace: stackTrace, cause: innerErr}
28 | }
29 |
30 | // NewRuntimeErrorWithMessage create a new RuntimeError instance.
31 | func NewRuntimeErrorWithMessage(message string) RuntimeError {
32 | goid, gopc, msg, stackTrace, innerErr := runtimeErrorNewWithMessage(message)
33 | return &runtimeError{goid: goid, gopc: gopc, message: msg, stackTrace: stackTrace, cause: innerErr}
34 | }
35 |
36 | // NewRuntimeErrorWithMessageCause create a new RuntimeError instance.
37 | func NewRuntimeErrorWithMessageCause(message string, cause any) RuntimeError {
38 | goid, gopc, msg, stackTrace, innerErr := runtimeErrorNewWithMessageCause(message, cause)
39 | return &runtimeError{goid: goid, gopc: gopc, message: msg, stackTrace: stackTrace, cause: innerErr}
40 | }
41 |
--------------------------------------------------------------------------------
/api_future_task.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import "time"
4 |
5 | // FutureCallable provides a future function that returns a value of type TResult.
6 | type FutureCallable[TResult any] func(task FutureTask[TResult]) TResult
7 |
8 | // CancelToken propagates notification that operations should be canceled.
9 | type CancelToken interface {
10 | // IsCanceled returns true if task was canceled.
11 | IsCanceled() bool
12 |
13 | // Cancel notifies the waiting coroutine that the task has canceled and returns stack information.
14 | Cancel()
15 | }
16 |
17 | // FutureTask provide a way to wait for the sub-coroutine to finish executing, get the return value of the sub-coroutine, and catch the sub-coroutine panic.
18 | type FutureTask[TResult any] interface {
19 | // IsDone returns true if completed in any fashion: normally, exceptionally or via cancellation.
20 | IsDone() bool
21 |
22 | // IsCanceled returns true if task was canceled.
23 | IsCanceled() bool
24 |
25 | // IsFailed returns true if completed exceptionally.
26 | IsFailed() bool
27 |
28 | // Complete notifies the waiting coroutine that the task has completed normally and returns the execution result.
29 | Complete(result TResult)
30 |
31 | // Cancel notifies the waiting coroutine that the task has canceled and returns stack information.
32 | Cancel()
33 |
34 | // Fail notifies the waiting coroutine that the task has terminated due to panic and returns stack information.
35 | Fail(error any)
36 |
37 | // Get return the execution result of the sub-coroutine, if there is no result, return nil.
38 | // If task is canceled, a panic with cancellation will be raised.
39 | // If panic is raised during the execution of the sub-coroutine, it will be raised again at this time.
40 | Get() TResult
41 |
42 | // GetWithTimeout return the execution result of the sub-coroutine, if there is no result, return nil.
43 | // If task is canceled, a panic with cancellation will be raised.
44 | // If panic is raised during the execution of the sub-coroutine, it will be raised again at this time.
45 | // If the deadline is reached, a panic with timeout error will be raised.
46 | GetWithTimeout(timeout time.Duration) TResult
47 |
48 | // Run execute the task, the method can be called repeatedly, but the task will only execute once.
49 | Run()
50 | }
51 |
52 | // NewFutureTask Create a new instance.
53 | func NewFutureTask[TResult any](callable FutureCallable[TResult]) FutureTask[TResult] {
54 | if callable == nil {
55 | panic("callable can not be nil.")
56 | }
57 | task := &futureTask[TResult]{callable: callable}
58 | task.await.Add(1)
59 | return task
60 | }
61 |
--------------------------------------------------------------------------------
/api_future_task_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestFutureCallable(t *testing.T) {
10 | var futureCallable FutureCallable[string] = func(task FutureTask[string]) string {
11 | return "Hello"
12 | }
13 | assert.Equal(t, "Hello", futureCallable(nil))
14 | //
15 | var fun func(FutureTask[string]) string = futureCallable
16 | assert.Equal(t, "Hello", fun(nil))
17 | }
18 |
19 | func TestCancelToken(t *testing.T) {
20 | task := NewFutureTask[any](func(task FutureTask[any]) any { return nil })
21 | token, ok := task.(CancelToken)
22 | assert.Same(t, task, token)
23 | assert.True(t, ok)
24 | }
25 |
26 | func TestNewFutureTask(t *testing.T) {
27 | assert.Panics(t, func() {
28 | NewFutureTask[any](nil)
29 | })
30 | //
31 | task := NewFutureTask[any](func(task FutureTask[any]) any { return nil })
32 | assert.NotNil(t, task)
33 | //
34 | p, ok := task.(*futureTask[any])
35 | assert.Same(t, p, task)
36 | assert.True(t, ok)
37 | }
38 |
--------------------------------------------------------------------------------
/api_goid.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | // Goid return the current goroutine's unique id.
4 | func Goid() uint64 {
5 | return getg().goid()
6 | }
7 |
--------------------------------------------------------------------------------
/api_goid_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestGoid(t *testing.T) {
10 | assert.NotEqual(t, 0, Goid())
11 | assert.Equal(t, Goid(), Goid())
12 | }
13 |
14 | //===
15 |
16 | // BenchmarkGoid-8 331324310 3.589 ns/op 0 B/op 0 allocs/op
17 | func BenchmarkGoid(b *testing.B) {
18 | b.ReportAllocs()
19 | b.ResetTimer()
20 | for i := 0; i < b.N; i++ {
21 | _ = Goid()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/api_routine.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | // Runnable provides a function without return values.
4 | type Runnable func()
5 |
6 | // Callable provides a function that returns a value of type TResult.
7 | type Callable[TResult any] func() TResult
8 |
9 | // CancelRunnable provides a cancellable function without return values.
10 | type CancelRunnable func(token CancelToken)
11 |
12 | // CancelCallable provides a cancellable function that returns a value of type TResult.
13 | type CancelCallable[TResult any] func(token CancelToken) TResult
14 |
15 | // WrapTask create a new task and capture the inheritableThreadLocals from the current goroutine.
16 | // This function returns a FutureTask instance, but the return task will not run automatically.
17 | // You can run it in a sub-goroutine or goroutine-pool by FutureTask.Run method, wait by FutureTask.Get or FutureTask.GetWithTimeout method.
18 | // When the returned task run panic will be caught and error stack will be printed, the panic will be trigger again when calling FutureTask.Get or FutureTask.GetWithTimeout method.
19 | func WrapTask(fun Runnable) FutureTask[any] {
20 | ctx := createInheritedMap()
21 | callable := inheritedTask{context: ctx, function: fun}.run
22 | return NewFutureTask[any](callable)
23 | }
24 |
25 | // WrapWaitTask create a new task and capture the inheritableThreadLocals from the current goroutine.
26 | // This function returns a FutureTask instance, but the return task will not run automatically.
27 | // You can run it in a sub-goroutine or goroutine-pool by FutureTask.Run method, wait by FutureTask.Get or FutureTask.GetWithTimeout method.
28 | // When the returned task run panic will be caught, the panic will be trigger again when calling FutureTask.Get or FutureTask.GetWithTimeout method.
29 | func WrapWaitTask(fun CancelRunnable) FutureTask[any] {
30 | ctx := createInheritedMap()
31 | callable := inheritedWaitTask{context: ctx, function: fun}.run
32 | return NewFutureTask[any](callable)
33 | }
34 |
35 | // WrapWaitResultTask create a new task and capture the inheritableThreadLocals from the current goroutine.
36 | // This function returns a FutureTask instance, but the return task will not run automatically.
37 | // You can run it in a sub-goroutine or goroutine-pool by FutureTask.Run method, wait and get result by FutureTask.Get or FutureTask.GetWithTimeout method.
38 | // When the returned task run panic will be caught, the panic will be trigger again when calling FutureTask.Get or FutureTask.GetWithTimeout method.
39 | func WrapWaitResultTask[TResult any](fun CancelCallable[TResult]) FutureTask[TResult] {
40 | ctx := createInheritedMap()
41 | callable := inheritedWaitResultTask[TResult]{context: ctx, function: fun}.run
42 | return NewFutureTask[TResult](callable)
43 | }
44 |
45 | // Go starts a new goroutine, and copy inheritableThreadLocals from current goroutine.
46 | // This function will auto invoke the func and print error stack when panic occur in goroutine.
47 | func Go(fun Runnable) {
48 | task := WrapTask(fun)
49 | go task.Run()
50 | }
51 |
52 | // GoWait starts a new goroutine, and copy inheritableThreadLocals from current goroutine.
53 | // This function will auto invoke the func and return a FutureTask instance, so we can wait by FutureTask.Get or FutureTask.GetWithTimeout method.
54 | // If panic occur in goroutine, The panic will be trigger again when calling FutureTask.Get or FutureTask.GetWithTimeout method.
55 | func GoWait(fun CancelRunnable) FutureTask[any] {
56 | task := WrapWaitTask(fun)
57 | go task.Run()
58 | return task
59 | }
60 |
61 | // GoWaitResult starts a new goroutine, and copy inheritableThreadLocals from current goroutine.
62 | // This function will auto invoke the func and return a FutureTask instance, so we can wait and get result by FutureTask.Get or FutureTask.GetWithTimeout method.
63 | // If panic occur in goroutine, The panic will be trigger again when calling FutureTask.Get or FutureTask.GetWithTimeout method.
64 | func GoWaitResult[TResult any](fun CancelCallable[TResult]) FutureTask[TResult] {
65 | task := WrapWaitResultTask(fun)
66 | go task.Run()
67 | return task
68 | }
69 |
--------------------------------------------------------------------------------
/api_thread_local.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | // ThreadLocal provides goroutine-local variables.
4 | type ThreadLocal[T any] interface {
5 | // Get returns the value in the current goroutine's local threadLocals or inheritableThreadLocals, if it was set before.
6 | Get() T
7 |
8 | // Set copy the value into the current goroutine's local threadLocals or inheritableThreadLocals.
9 | Set(value T)
10 |
11 | // Remove delete the value from the current goroutine's local threadLocals or inheritableThreadLocals.
12 | Remove()
13 | }
14 |
15 | // Supplier provides a function that returns a value of type T.
16 | type Supplier[T any] func() T
17 |
18 | // NewThreadLocal create and return a new ThreadLocal instance.
19 | // The initial value stored with the default value of type T.
20 | func NewThreadLocal[T any]() ThreadLocal[T] {
21 | return &threadLocal[T]{index: nextThreadLocalIndex()}
22 | }
23 |
24 | // NewThreadLocalWithInitial create and return a new ThreadLocal instance.
25 | // The initial value stored as the return value of the method supplier.
26 | func NewThreadLocalWithInitial[T any](supplier Supplier[T]) ThreadLocal[T] {
27 | return &threadLocal[T]{index: nextThreadLocalIndex(), supplier: supplier}
28 | }
29 |
30 | // NewInheritableThreadLocal create and return a new ThreadLocal instance.
31 | // The initial value stored with the default value of type T.
32 | // The value can be inherited to sub goroutines witch started by Go, GoWait, GoWaitResult methods.
33 | // The value can be captured to FutureTask which created by WrapTask, WrapWaitTask, WrapWaitResultTask methods.
34 | func NewInheritableThreadLocal[T any]() ThreadLocal[T] {
35 | return &inheritableThreadLocal[T]{index: nextInheritableThreadLocalIndex()}
36 | }
37 |
38 | // NewInheritableThreadLocalWithInitial create and return a new ThreadLocal instance.
39 | // The initial value stored as the return value of the method supplier.
40 | // The value can be inherited to sub goroutines witch started by Go, GoWait, GoWaitResult methods.
41 | // The value can be captured to FutureTask which created by WrapTask, WrapWaitTask, WrapWaitResultTask methods.
42 | func NewInheritableThreadLocalWithInitial[T any](supplier Supplier[T]) ThreadLocal[T] {
43 | return &inheritableThreadLocal[T]{index: nextInheritableThreadLocalIndex(), supplier: supplier}
44 | }
45 |
--------------------------------------------------------------------------------
/api_thread_local_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "math/rand"
5 | "sync"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | const (
12 | concurrency = 500
13 | loopTimes = 200
14 | )
15 |
16 | func TestSupplier(t *testing.T) {
17 | var supplier Supplier[string] = func() string {
18 | return "Hello"
19 | }
20 | assert.Equal(t, "Hello", supplier())
21 | //
22 | var fun func() string = supplier
23 | assert.Equal(t, "Hello", fun())
24 | }
25 |
26 | //===
27 |
28 | func TestNewThreadLocal_Single(t *testing.T) {
29 | tls := NewThreadLocal[string]()
30 | tls.Set("Hello")
31 | assert.Equal(t, "Hello", tls.Get())
32 | //
33 | tls2 := NewThreadLocal[int]()
34 | assert.Equal(t, "Hello", tls.Get())
35 | tls2.Set(22)
36 | assert.Equal(t, 22, tls2.Get())
37 | //
38 | tls2.Set(33)
39 | assert.Equal(t, 33, tls2.Get())
40 | //
41 | task := GoWait(func(token CancelToken) {
42 | assert.Equal(t, "", tls.Get())
43 | assert.Equal(t, 0, tls2.Get())
44 | })
45 | task.Get()
46 | }
47 |
48 | func TestNewThreadLocal_Multi(t *testing.T) {
49 | tls := NewThreadLocal[string]()
50 | tls2 := NewThreadLocal[int]()
51 | tls.Set("Hello")
52 | tls2.Set(22)
53 | assert.Equal(t, 22, tls2.Get())
54 | assert.Equal(t, "Hello", tls.Get())
55 | //
56 | tls2.Set(33)
57 | assert.Equal(t, 33, tls2.Get())
58 | //
59 | task := GoWait(func(token CancelToken) {
60 | assert.Equal(t, "", tls.Get())
61 | assert.Equal(t, 0, tls2.Get())
62 | })
63 | task.Get()
64 | }
65 |
66 | func TestNewThreadLocal_Concurrency(t *testing.T) {
67 | tls := NewThreadLocal[uint64]()
68 | tls2 := NewThreadLocal[uint64]()
69 | //
70 | tls2.Set(33)
71 | assert.Equal(t, uint64(33), tls2.Get())
72 | //
73 | wg := &sync.WaitGroup{}
74 | wg.Add(concurrency)
75 | for i := 0; i < concurrency; i++ {
76 | assert.Equal(t, uint64(0), tls.Get())
77 | assert.Equal(t, uint64(33), tls2.Get())
78 | Go(func() {
79 | assert.Equal(t, uint64(0), tls.Get())
80 | assert.Equal(t, uint64(0), tls2.Get())
81 | v := rand.Uint64()
82 | v2 := rand.Uint64()
83 | for j := 0; j < loopTimes; j++ {
84 | tls.Set(v)
85 | tmp := tls.Get()
86 | assert.Equal(t, v, tmp)
87 | //
88 | tls2.Set(v2)
89 | tmp2 := tls2.Get()
90 | assert.Equal(t, v2, tmp2)
91 | }
92 | wg.Done()
93 | })
94 | }
95 | wg.Wait()
96 | //
97 | task := GoWait(func(token CancelToken) {
98 | assert.Equal(t, uint64(0), tls.Get())
99 | assert.Equal(t, uint64(0), tls2.Get())
100 | })
101 | task.Get()
102 | }
103 |
104 | func TestNewThreadLocal_Interface(t *testing.T) {
105 | tls := NewThreadLocal[Cloneable]()
106 | tls2 := NewThreadLocal[Cloneable]()
107 | //
108 | assert.Nil(t, tls.Get())
109 | assert.Nil(t, tls2.Get())
110 | //
111 | tls.Set(nil)
112 | tls2.Set(nil)
113 | assert.Nil(t, tls.Get())
114 | assert.Nil(t, tls2.Get())
115 | //
116 | tls.Set(&personCloneable{Id: 1, Name: "Hello"})
117 | tls2.Set(&personCloneable{Id: 1, Name: "Hello"})
118 | assert.NotNil(t, tls.Get())
119 | assert.NotNil(t, tls2.Get())
120 | //
121 | tls.Remove()
122 | tls2.Remove()
123 | assert.Nil(t, tls.Get())
124 | assert.Nil(t, tls2.Get())
125 | //
126 | task := GoWait(func(token CancelToken) {
127 | assert.Nil(t, tls.Get())
128 | assert.Nil(t, tls2.Get())
129 | })
130 | task.Get()
131 | }
132 |
133 | func TestNewThreadLocal_Pointer(t *testing.T) {
134 | tls := NewThreadLocal[*personCloneable]()
135 | tls2 := NewThreadLocal[*personCloneable]()
136 | //
137 | assert.Nil(t, tls.Get())
138 | assert.Nil(t, tls2.Get())
139 | //
140 | tls.Set(nil)
141 | tls2.Set(nil)
142 | assert.Nil(t, tls.Get())
143 | assert.Nil(t, tls2.Get())
144 | //
145 | tls.Set(&personCloneable{Id: 1, Name: "Hello"})
146 | tls2.Set(&personCloneable{Id: 1, Name: "Hello"})
147 | assert.NotNil(t, tls.Get())
148 | assert.NotNil(t, tls2.Get())
149 | //
150 | tls.Remove()
151 | tls2.Remove()
152 | assert.Nil(t, tls.Get())
153 | assert.Nil(t, tls2.Get())
154 | //
155 | task := GoWait(func(token CancelToken) {
156 | assert.Nil(t, tls.Get())
157 | assert.Nil(t, tls2.Get())
158 | })
159 | task.Get()
160 | }
161 |
162 | //===
163 |
164 | func TestNewThreadLocalWithInitial_Single(t *testing.T) {
165 | tls := NewThreadLocalWithInitial[string](func() string {
166 | return "Hello"
167 | })
168 | assert.Equal(t, "Hello", tls.Get())
169 | //
170 | tls2 := NewThreadLocalWithInitial[int](func() int {
171 | return 22
172 | })
173 | assert.Equal(t, "Hello", tls.Get())
174 | assert.Equal(t, 22, tls2.Get())
175 | //
176 | tls2.Set(33)
177 | assert.Equal(t, 33, tls2.Get())
178 | //
179 | task := GoWait(func(token CancelToken) {
180 | assert.Equal(t, "Hello", tls.Get())
181 | assert.Equal(t, 22, tls2.Get())
182 | })
183 | task.Get()
184 | }
185 |
186 | func TestNewThreadLocalWithInitial_Multi(t *testing.T) {
187 | tls := NewThreadLocalWithInitial[string](func() string {
188 | return "Hello"
189 | })
190 | tls2 := NewThreadLocalWithInitial[int](func() int {
191 | return 22
192 | })
193 | tls.Set("Hello")
194 | tls2.Set(22)
195 | assert.Equal(t, 22, tls2.Get())
196 | assert.Equal(t, "Hello", tls.Get())
197 | //
198 | tls2.Set(33)
199 | assert.Equal(t, 33, tls2.Get())
200 | //
201 | task := GoWait(func(token CancelToken) {
202 | assert.Equal(t, "Hello", tls.Get())
203 | assert.Equal(t, 22, tls2.Get())
204 | })
205 | task.Get()
206 | }
207 |
208 | func TestNewThreadLocalWithInitial_Concurrency(t *testing.T) {
209 | tls := NewThreadLocalWithInitial[any](func() any {
210 | return "Hello"
211 | })
212 | tls2 := NewThreadLocalWithInitial[uint64](func() uint64 {
213 | return uint64(22)
214 | })
215 | //
216 | tls2.Set(33)
217 | assert.Equal(t, uint64(33), tls2.Get())
218 | //
219 | wg := &sync.WaitGroup{}
220 | wg.Add(concurrency)
221 | for i := 0; i < concurrency; i++ {
222 | assert.Equal(t, "Hello", tls.Get())
223 | assert.Equal(t, uint64(33), tls2.Get())
224 | Go(func() {
225 | assert.Equal(t, "Hello", tls.Get())
226 | assert.Equal(t, uint64(22), tls2.Get())
227 | v := rand.Uint64()
228 | v2 := rand.Uint64()
229 | for j := 0; j < loopTimes; j++ {
230 | tls.Set(v)
231 | tmp := tls.Get()
232 | assert.Equal(t, v, tmp.(uint64))
233 | //
234 | tls2.Set(v2)
235 | tmp2 := tls2.Get()
236 | assert.Equal(t, v2, tmp2)
237 | }
238 | wg.Done()
239 | })
240 | }
241 | wg.Wait()
242 | //
243 | task := GoWait(func(token CancelToken) {
244 | assert.Equal(t, "Hello", tls.Get())
245 | assert.Equal(t, uint64(22), tls2.Get())
246 | })
247 | task.Get()
248 | }
249 |
250 | func TestNewThreadLocalWithInitial_Interface(t *testing.T) {
251 | tls := NewThreadLocalWithInitial[Cloneable](func() Cloneable {
252 | return nil
253 | })
254 | tls2 := NewThreadLocalWithInitial[Cloneable](func() Cloneable {
255 | return nil
256 | })
257 | //
258 | assert.Nil(t, tls.Get())
259 | assert.Nil(t, tls2.Get())
260 | //
261 | tls.Set(nil)
262 | tls2.Set(nil)
263 | assert.Nil(t, tls.Get())
264 | assert.Nil(t, tls2.Get())
265 | //
266 | tls.Set(&personCloneable{Id: 1, Name: "Hello"})
267 | tls2.Set(&personCloneable{Id: 1, Name: "Hello"})
268 | assert.NotNil(t, tls.Get())
269 | assert.NotNil(t, tls2.Get())
270 | //
271 | tls.Remove()
272 | tls2.Remove()
273 | assert.Nil(t, tls.Get())
274 | assert.Nil(t, tls2.Get())
275 | //
276 | task := GoWait(func(token CancelToken) {
277 | assert.Nil(t, tls.Get())
278 | assert.Nil(t, tls2.Get())
279 | })
280 | task.Get()
281 | }
282 |
283 | func TestNewThreadLocalWithInitial_Pointer(t *testing.T) {
284 | tls := NewThreadLocalWithInitial[*personCloneable](func() *personCloneable {
285 | return nil
286 | })
287 | tls2 := NewThreadLocalWithInitial[*personCloneable](func() *personCloneable {
288 | return nil
289 | })
290 | //
291 | assert.Nil(t, tls.Get())
292 | assert.Nil(t, tls2.Get())
293 | //
294 | tls.Set(nil)
295 | tls2.Set(nil)
296 | assert.Nil(t, tls.Get())
297 | assert.Nil(t, tls2.Get())
298 | //
299 | tls.Set(&personCloneable{Id: 1, Name: "Hello"})
300 | tls2.Set(&personCloneable{Id: 1, Name: "Hello"})
301 | assert.NotNil(t, tls.Get())
302 | assert.NotNil(t, tls2.Get())
303 | //
304 | tls.Remove()
305 | tls2.Remove()
306 | assert.Nil(t, tls.Get())
307 | assert.Nil(t, tls2.Get())
308 | //
309 | task := GoWait(func(token CancelToken) {
310 | assert.Nil(t, tls.Get())
311 | assert.Nil(t, tls2.Get())
312 | })
313 | task.Get()
314 | }
315 |
316 | //===
317 |
318 | func TestNewInheritableThreadLocal_Single(t *testing.T) {
319 | tls := NewInheritableThreadLocal[string]()
320 | tls.Set("Hello")
321 | assert.Equal(t, "Hello", tls.Get())
322 | //
323 | tls2 := NewInheritableThreadLocal[int]()
324 | assert.Equal(t, "Hello", tls.Get())
325 | tls2.Set(22)
326 | assert.Equal(t, 22, tls2.Get())
327 | //
328 | tls2.Set(33)
329 | assert.Equal(t, 33, tls2.Get())
330 | //
331 | task := GoWait(func(token CancelToken) {
332 | assert.Equal(t, "Hello", tls.Get())
333 | assert.Equal(t, 33, tls2.Get())
334 | })
335 | task.Get()
336 | }
337 |
338 | func TestNewInheritableThreadLocal_Multi(t *testing.T) {
339 | tls := NewInheritableThreadLocal[string]()
340 | tls2 := NewInheritableThreadLocal[int]()
341 | tls.Set("Hello")
342 | tls2.Set(22)
343 | assert.Equal(t, 22, tls2.Get())
344 | assert.Equal(t, "Hello", tls.Get())
345 | //
346 | tls2.Set(33)
347 | assert.Equal(t, 33, tls2.Get())
348 | //
349 | task := GoWait(func(token CancelToken) {
350 | assert.Equal(t, "Hello", tls.Get())
351 | assert.Equal(t, 33, tls2.Get())
352 | })
353 | task.Get()
354 | }
355 |
356 | func TestNewInheritableThreadLocal_Concurrency(t *testing.T) {
357 | tls := NewInheritableThreadLocal[uint64]()
358 | tls2 := NewInheritableThreadLocal[uint64]()
359 | //
360 | tls2.Set(33)
361 | assert.Equal(t, uint64(33), tls2.Get())
362 | //
363 | wg := &sync.WaitGroup{}
364 | wg.Add(concurrency)
365 | for i := 0; i < concurrency; i++ {
366 | assert.Equal(t, uint64(0), tls.Get())
367 | assert.Equal(t, uint64(33), tls2.Get())
368 | Go(func() {
369 | assert.Equal(t, uint64(0), tls.Get())
370 | assert.Equal(t, uint64(33), tls2.Get())
371 | v := rand.Uint64()
372 | v2 := rand.Uint64()
373 | for j := 0; j < loopTimes; j++ {
374 | tls.Set(v)
375 | tmp := tls.Get()
376 | assert.Equal(t, v, tmp)
377 | //
378 | tls2.Set(v2)
379 | tmp2 := tls2.Get()
380 | assert.Equal(t, v2, tmp2)
381 | }
382 | wg.Done()
383 | })
384 | }
385 | wg.Wait()
386 | //
387 | task := GoWait(func(token CancelToken) {
388 | assert.Equal(t, uint64(0), tls.Get())
389 | assert.Equal(t, uint64(33), tls2.Get())
390 | })
391 | task.Get()
392 | }
393 |
394 | func TestNewInheritableThreadLocal_Interface(t *testing.T) {
395 | tls := NewInheritableThreadLocal[Cloneable]()
396 | tls2 := NewInheritableThreadLocal[Cloneable]()
397 | //
398 | assert.Nil(t, tls.Get())
399 | assert.Nil(t, tls2.Get())
400 | //
401 | tls.Set(nil)
402 | tls2.Set(nil)
403 | assert.Nil(t, tls.Get())
404 | assert.Nil(t, tls2.Get())
405 | //
406 | tls.Set(&personCloneable{Id: 1, Name: "Hello"})
407 | tls2.Set(&personCloneable{Id: 1, Name: "Hello"})
408 | assert.NotNil(t, tls.Get())
409 | assert.NotNil(t, tls2.Get())
410 | //
411 | tls.Remove()
412 | tls2.Remove()
413 | assert.Nil(t, tls.Get())
414 | assert.Nil(t, tls2.Get())
415 | //
416 | task := GoWait(func(token CancelToken) {
417 | assert.Nil(t, tls.Get())
418 | assert.Nil(t, tls2.Get())
419 | })
420 | task.Get()
421 | }
422 |
423 | func TestNewInheritableThreadLocal_Pointer(t *testing.T) {
424 | tls := NewInheritableThreadLocal[*personCloneable]()
425 | tls2 := NewInheritableThreadLocal[*personCloneable]()
426 | //
427 | assert.Nil(t, tls.Get())
428 | assert.Nil(t, tls2.Get())
429 | //
430 | tls.Set(nil)
431 | tls2.Set(nil)
432 | assert.Nil(t, tls.Get())
433 | assert.Nil(t, tls2.Get())
434 | //
435 | tls.Set(&personCloneable{Id: 1, Name: "Hello"})
436 | tls2.Set(&personCloneable{Id: 1, Name: "Hello"})
437 | assert.NotNil(t, tls.Get())
438 | assert.NotNil(t, tls2.Get())
439 | //
440 | tls.Remove()
441 | tls2.Remove()
442 | assert.Nil(t, tls.Get())
443 | assert.Nil(t, tls2.Get())
444 | //
445 | task := GoWait(func(token CancelToken) {
446 | assert.Nil(t, tls.Get())
447 | assert.Nil(t, tls2.Get())
448 | })
449 | task.Get()
450 | }
451 |
452 | //===
453 |
454 | func TestNewInheritableThreadLocalWithInitial_Single(t *testing.T) {
455 | tls := NewInheritableThreadLocalWithInitial[string](func() string {
456 | return "Hello"
457 | })
458 | assert.Equal(t, "Hello", tls.Get())
459 | //
460 | tls2 := NewInheritableThreadLocalWithInitial[int](func() int {
461 | return 22
462 | })
463 | assert.Equal(t, "Hello", tls.Get())
464 | assert.Equal(t, 22, tls2.Get())
465 | //
466 | tls2.Set(33)
467 | assert.Equal(t, 33, tls2.Get())
468 | //
469 | task := GoWait(func(token CancelToken) {
470 | assert.Equal(t, "Hello", tls.Get())
471 | assert.Equal(t, 33, tls2.Get())
472 | })
473 | task.Get()
474 | }
475 |
476 | func TestNewInheritableThreadLocalWithInitial_Multi(t *testing.T) {
477 | tls := NewInheritableThreadLocalWithInitial[string](func() string {
478 | return "Hello"
479 | })
480 | tls2 := NewInheritableThreadLocalWithInitial[int](func() int {
481 | return 22
482 | })
483 | tls.Set("Hello")
484 | tls2.Set(22)
485 | assert.Equal(t, 22, tls2.Get())
486 | assert.Equal(t, "Hello", tls.Get())
487 | //
488 | tls2.Set(33)
489 | assert.Equal(t, 33, tls2.Get())
490 | //
491 | task := GoWait(func(token CancelToken) {
492 | assert.Equal(t, "Hello", tls.Get())
493 | assert.Equal(t, 33, tls2.Get())
494 | })
495 | task.Get()
496 | }
497 |
498 | func TestNewInheritableThreadLocalWithInitial_Concurrency(t *testing.T) {
499 | tls := NewInheritableThreadLocalWithInitial[any](func() any {
500 | return "Hello"
501 | })
502 | tls2 := NewInheritableThreadLocalWithInitial[uint64](func() uint64 {
503 | return uint64(22)
504 | })
505 | //
506 | tls2.Set(33)
507 | assert.Equal(t, uint64(33), tls2.Get())
508 | //
509 | wg := &sync.WaitGroup{}
510 | wg.Add(concurrency)
511 | for i := 0; i < concurrency; i++ {
512 | assert.Equal(t, "Hello", tls.Get())
513 | assert.Equal(t, uint64(33), tls2.Get())
514 | Go(func() {
515 | assert.Equal(t, "Hello", tls.Get())
516 | assert.Equal(t, uint64(33), tls2.Get())
517 | v := rand.Uint64()
518 | v2 := rand.Uint64()
519 | for j := 0; j < loopTimes; j++ {
520 | tls.Set(v)
521 | tmp := tls.Get()
522 | assert.Equal(t, v, tmp.(uint64))
523 | //
524 | tls2.Set(v2)
525 | tmp2 := tls2.Get()
526 | assert.Equal(t, v2, tmp2)
527 | }
528 | wg.Done()
529 | })
530 | }
531 | wg.Wait()
532 | //
533 | task := GoWait(func(token CancelToken) {
534 | assert.Equal(t, "Hello", tls.Get())
535 | assert.Equal(t, uint64(33), tls2.Get())
536 | })
537 | task.Get()
538 | }
539 |
540 | func TestNewInheritableThreadLocalWithInitial_Interface(t *testing.T) {
541 | tls := NewInheritableThreadLocalWithInitial[Cloneable](func() Cloneable {
542 | return nil
543 | })
544 | tls2 := NewInheritableThreadLocalWithInitial[Cloneable](func() Cloneable {
545 | return nil
546 | })
547 | //
548 | assert.Nil(t, tls.Get())
549 | assert.Nil(t, tls2.Get())
550 | //
551 | tls.Set(nil)
552 | tls2.Set(nil)
553 | assert.Nil(t, tls.Get())
554 | assert.Nil(t, tls2.Get())
555 | //
556 | tls.Set(&personCloneable{Id: 1, Name: "Hello"})
557 | tls2.Set(&personCloneable{Id: 1, Name: "Hello"})
558 | assert.NotNil(t, tls.Get())
559 | assert.NotNil(t, tls2.Get())
560 | //
561 | tls.Remove()
562 | tls2.Remove()
563 | assert.Nil(t, tls.Get())
564 | assert.Nil(t, tls2.Get())
565 | //
566 | task := GoWait(func(token CancelToken) {
567 | assert.Nil(t, tls.Get())
568 | assert.Nil(t, tls2.Get())
569 | })
570 | task.Get()
571 | }
572 |
573 | func TestNewInheritableThreadLocalWithInitial_Pointer(t *testing.T) {
574 | tls := NewInheritableThreadLocalWithInitial[*personCloneable](func() *personCloneable {
575 | return nil
576 | })
577 | tls2 := NewInheritableThreadLocalWithInitial[*personCloneable](func() *personCloneable {
578 | return nil
579 | })
580 | //
581 | assert.Nil(t, tls.Get())
582 | assert.Nil(t, tls2.Get())
583 | //
584 | tls.Set(nil)
585 | tls2.Set(nil)
586 | assert.Nil(t, tls.Get())
587 | assert.Nil(t, tls2.Get())
588 | //
589 | tls.Set(&personCloneable{Id: 1, Name: "Hello"})
590 | tls2.Set(&personCloneable{Id: 1, Name: "Hello"})
591 | assert.NotNil(t, tls.Get())
592 | assert.NotNil(t, tls2.Get())
593 | //
594 | tls.Remove()
595 | tls2.Remove()
596 | assert.Nil(t, tls.Get())
597 | assert.Nil(t, tls2.Get())
598 | //
599 | task := GoWait(func(token CancelToken) {
600 | assert.Nil(t, tls.Get())
601 | assert.Nil(t, tls2.Get())
602 | })
603 | task.Get()
604 | }
605 |
606 | //===
607 |
608 | // BenchmarkThreadLocal-8 13636471 94.17 ns/op 7 B/op 0 allocs/op
609 | func BenchmarkThreadLocal(b *testing.B) {
610 | tlsCount := 100
611 | tlsSlice := make([]ThreadLocal[int], tlsCount)
612 | for i := 0; i < tlsCount; i++ {
613 | tlsSlice[i] = NewThreadLocal[int]()
614 | }
615 | b.ReportAllocs()
616 | b.ResetTimer()
617 | for i := 0; i < b.N; i++ {
618 | index := i % tlsCount
619 | tls := tlsSlice[index]
620 | initValue := tls.Get()
621 | if initValue != 0 {
622 | b.Fail()
623 | }
624 | tls.Set(i)
625 | if tls.Get() != i {
626 | b.Fail()
627 | }
628 | tls.Remove()
629 | }
630 | }
631 |
632 | // BenchmarkThreadLocalWithInitial-8 13674153 86.76 ns/op 7 B/op 0 allocs/op
633 | func BenchmarkThreadLocalWithInitial(b *testing.B) {
634 | tlsCount := 100
635 | tlsSlice := make([]ThreadLocal[int], tlsCount)
636 | for i := 0; i < tlsCount; i++ {
637 | index := i
638 | tlsSlice[i] = NewThreadLocalWithInitial[int](func() int {
639 | return index
640 | })
641 | }
642 | b.ReportAllocs()
643 | b.ResetTimer()
644 | for i := 0; i < b.N; i++ {
645 | index := i % tlsCount
646 | tls := tlsSlice[index]
647 | initValue := tls.Get()
648 | if initValue != index {
649 | b.Fail()
650 | }
651 | tls.Set(i)
652 | if tls.Get() != i {
653 | b.Fail()
654 | }
655 | tls.Remove()
656 | }
657 | }
658 |
659 | // BenchmarkInheritableThreadLocal-8 13917819 84.27 ns/op 7 B/op 0 allocs/op
660 | func BenchmarkInheritableThreadLocal(b *testing.B) {
661 | tlsCount := 100
662 | tlsSlice := make([]ThreadLocal[int], tlsCount)
663 | for i := 0; i < tlsCount; i++ {
664 | tlsSlice[i] = NewInheritableThreadLocal[int]()
665 | }
666 | b.ReportAllocs()
667 | b.ResetTimer()
668 | for i := 0; i < b.N; i++ {
669 | index := i % tlsCount
670 | tls := tlsSlice[index]
671 | initValue := tls.Get()
672 | if initValue != 0 {
673 | b.Fail()
674 | }
675 | tls.Set(i)
676 | if tls.Get() != i {
677 | b.Fail()
678 | }
679 | tls.Remove()
680 | }
681 | }
682 |
683 | // BenchmarkInheritableThreadLocalWithInitial-8 13483130 90.03 ns/op 7 B/op 0 allocs/op
684 | func BenchmarkInheritableThreadLocalWithInitial(b *testing.B) {
685 | tlsCount := 100
686 | tlsSlice := make([]ThreadLocal[int], tlsCount)
687 | for i := 0; i < tlsCount; i++ {
688 | index := i
689 | tlsSlice[i] = NewInheritableThreadLocalWithInitial[int](func() int {
690 | return index
691 | })
692 | }
693 | b.ReportAllocs()
694 | b.ResetTimer()
695 | for i := 0; i < b.N; i++ {
696 | index := i % tlsCount
697 | tls := tlsSlice[index]
698 | initValue := tls.Get()
699 | if initValue != index {
700 | b.Fail()
701 | }
702 | tls.Set(i)
703 | if tls.Get() != i {
704 | b.Fail()
705 | }
706 | tls.Remove()
707 | }
708 | }
709 |
--------------------------------------------------------------------------------
/error.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "reflect"
7 | "runtime"
8 | "strconv"
9 | "unicode"
10 | )
11 |
12 | const (
13 | newLine = "\n"
14 | innerErrorPrefix = " ---> "
15 | endOfInnerErrorStack = "--- End of inner error stack trace ---"
16 | endOfErrorStack = "--- End of error stack trace ---"
17 | wordAt = "at"
18 | wordIn = "in"
19 | wordCreatedBy = "created by"
20 | )
21 |
22 | type runtimeError struct {
23 | goid uint64
24 | gopc uintptr
25 | message string
26 | stackTrace []uintptr
27 | cause RuntimeError
28 | }
29 |
30 | func (re *runtimeError) Goid() uint64 {
31 | return re.goid
32 | }
33 |
34 | func (re *runtimeError) Gopc() uintptr {
35 | return re.gopc
36 | }
37 |
38 | func (re *runtimeError) Message() string {
39 | return re.message
40 | }
41 |
42 | func (re *runtimeError) StackTrace() []uintptr {
43 | return re.stackTrace
44 | }
45 |
46 | func (re *runtimeError) Cause() RuntimeError {
47 | return re.cause
48 | }
49 |
50 | func (re *runtimeError) Error() string {
51 | return runtimeErrorError(re)
52 | }
53 |
54 | func runtimeErrorNew(cause any) (goid uint64, gopc uintptr, msg string, stackTrace []uintptr, innerErr RuntimeError) {
55 | runtimeErr, isRuntimeErr := cause.(RuntimeError)
56 | if !isRuntimeErr {
57 | if err, isErr := cause.(error); isErr {
58 | msg = err.Error()
59 | } else if cause != nil {
60 | msg = fmt.Sprint(cause)
61 | }
62 | }
63 | gp := getg()
64 | return gp.goid(), gp.gopc(), msg, captureStackTrace(2, 100), runtimeErr
65 | }
66 |
67 | func runtimeErrorNewWithMessage(message string) (goid uint64, gopc uintptr, msg string, stackTrace []uintptr, innerErr RuntimeError) {
68 | gp := getg()
69 | return gp.goid(), gp.gopc(), message, captureStackTrace(2, 100), nil
70 | }
71 |
72 | func runtimeErrorNewWithMessageCause(message string, cause any) (goid uint64, gopc uintptr, msg string, stackTrace []uintptr, innerErr RuntimeError) {
73 | runtimeErr, isRuntimeErr := cause.(RuntimeError)
74 | if !isRuntimeErr {
75 | causeMsg := ""
76 | if err, isErr := cause.(error); isErr {
77 | causeMsg = err.Error()
78 | } else if cause != nil {
79 | causeMsg = fmt.Sprint(cause)
80 | }
81 | if len(message) == 0 {
82 | message = causeMsg
83 | } else if len(causeMsg) != 0 {
84 | message += " - " + causeMsg
85 | }
86 | }
87 | gp := getg()
88 | return gp.goid(), gp.gopc(), message, captureStackTrace(2, 100), runtimeErr
89 | }
90 |
91 | func runtimeErrorError(re RuntimeError) string {
92 | builder := &bytes.Buffer{}
93 | runtimeErrorPrintStackTrace(re, builder)
94 | runtimeErrorPrintCreatedBy(re, builder)
95 | return builder.String()
96 | }
97 |
98 | func runtimeErrorPrintStackTrace(re RuntimeError, builder *bytes.Buffer) {
99 | builder.WriteString(runtimeErrorTypeName(re))
100 | message := re.Message()
101 | if len(message) > 0 {
102 | builder.WriteString(": ")
103 | builder.WriteString(message)
104 | }
105 | cause := re.Cause()
106 | if cause != nil {
107 | builder.WriteString(newLine)
108 | builder.WriteString(innerErrorPrefix)
109 | runtimeErrorPrintStackTrace(cause, builder)
110 | builder.WriteString(newLine)
111 | builder.WriteString(" ")
112 | builder.WriteString(endOfInnerErrorStack)
113 | }
114 | stackTrace := re.StackTrace()
115 | if stackTrace != nil {
116 | savePoint := builder.Len()
117 | skippedPanic := false
118 | frames := runtime.CallersFrames(stackTrace)
119 | for {
120 | frame, more := frames.Next()
121 | if showFrame(frame.Function) {
122 | builder.WriteString(newLine)
123 | builder.WriteString(" ")
124 | builder.WriteString(wordAt)
125 | builder.WriteString(" ")
126 | builder.WriteString(frame.Function)
127 | builder.WriteString("() ")
128 | builder.WriteString(wordIn)
129 | builder.WriteString(" ")
130 | builder.WriteString(frame.File)
131 | builder.WriteString(":")
132 | builder.WriteString(strconv.Itoa(frame.Line))
133 | } else if skipFrame(frame.Function, skippedPanic) {
134 | builder.Truncate(savePoint)
135 | skippedPanic = true
136 | }
137 | if !more {
138 | break
139 | }
140 | }
141 | }
142 | }
143 |
144 | func runtimeErrorPrintCreatedBy(re RuntimeError, builder *bytes.Buffer) {
145 | goid := re.Goid()
146 | if goid == 1 {
147 | return
148 | }
149 | pc := re.Gopc()
150 | frame, _ := runtime.CallersFrames([]uintptr{pc}).Next()
151 | if frame.Func == nil {
152 | return
153 | }
154 | builder.WriteString(newLine)
155 | builder.WriteString(" ")
156 | builder.WriteString(endOfErrorStack)
157 | builder.WriteString(newLine)
158 | builder.WriteString(" ")
159 | builder.WriteString(wordCreatedBy)
160 | builder.WriteString(" ")
161 | builder.WriteString(frame.Function)
162 | builder.WriteString("() ")
163 | builder.WriteString(wordIn)
164 | builder.WriteString(" ")
165 | builder.WriteString(frame.File)
166 | builder.WriteString(":")
167 | builder.WriteString(strconv.Itoa(frame.Line))
168 | }
169 |
170 | func runtimeErrorTypeName(re RuntimeError) string {
171 | typeName := []rune(reflect.TypeOf(re).Elem().Name())
172 | typeName[0] = unicode.ToUpper(typeName[0])
173 | return string(typeName)
174 | }
175 |
--------------------------------------------------------------------------------
/error_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "errors"
5 | "strings"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestRuntimeError_Goid(t *testing.T) {
12 | goid := Goid()
13 | err := NewRuntimeError(nil)
14 | assert.Equal(t, goid, err.Goid())
15 | task := GoWait(func(token CancelToken) {
16 | assert.Equal(t, goid, err.Goid())
17 | assert.NotEqual(t, Goid(), err.Goid())
18 | })
19 | task.Get()
20 | }
21 |
22 | func TestRuntimeError_Gopc(t *testing.T) {
23 | gopc := getg().gopc()
24 | err := NewRuntimeError(nil)
25 | assert.Equal(t, gopc, err.Gopc())
26 | task := GoWait(func(token CancelToken) {
27 | assert.Equal(t, gopc, err.Gopc())
28 | assert.NotEqual(t, getg().gopc(), err.Gopc())
29 | })
30 | task.Get()
31 | }
32 |
33 | func TestRuntimeError_Message(t *testing.T) {
34 | err := NewRuntimeError(nil)
35 | assert.Equal(t, "", err.Message())
36 |
37 | err2 := NewRuntimeError("Hello")
38 | assert.Equal(t, "Hello", err2.Message())
39 |
40 | err3 := NewRuntimeError(&person{Id: 1, Name: "Tim"})
41 | assert.Equal(t, "&{1 Tim}", err3.Message())
42 | }
43 |
44 | func TestRuntimeError_StackTrace(t *testing.T) {
45 | err := NewRuntimeError(nil)
46 | stackTrace := err.StackTrace()
47 | capturedStackTrace := captureStackTrace(0, 200)
48 | for i := 1; i < len(stackTrace); i++ {
49 | assert.Equal(t, capturedStackTrace[i], stackTrace[i])
50 | }
51 | }
52 |
53 | func TestRuntimeError_Panic_Panic(t *testing.T) {
54 | defer func() {
55 | cause := recover()
56 | assert.NotNil(t, cause)
57 | err := NewRuntimeError(cause)
58 | lines := strings.Split(err.Error(), newLine)
59 | assert.Equal(t, 6, len(lines))
60 | //
61 | line := lines[0]
62 | assert.Equal(t, "RuntimeError: 1", line)
63 | //
64 | line = lines[1]
65 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Panic_Panic."))
66 | assert.True(t, strings.HasSuffix(line, "error_test.go:74"))
67 | //
68 | line = lines[2]
69 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Panic_Panic()"))
70 | assert.True(t, strings.HasSuffix(line, "error_test.go:77"))
71 | }()
72 | defer func() {
73 | if cause := recover(); cause != nil {
74 | panic(cause)
75 | }
76 | }()
77 | panic(1)
78 | }
79 |
80 | func TestRuntimeError_Cause(t *testing.T) {
81 | err := NewRuntimeError(nil)
82 | assert.Nil(t, err.Cause())
83 |
84 | err2 := NewRuntimeError(errors.New("error"))
85 | assert.Nil(t, err2.Cause())
86 |
87 | err3 := NewRuntimeError(&person{Id: 1, Name: "Tim"})
88 | assert.Nil(t, err3.Cause())
89 |
90 | err4 := NewRuntimeError(err)
91 | assert.Same(t, err, err4.Cause())
92 | }
93 |
94 | func TestRuntimeError_Error_EmptyMessage_NilError(t *testing.T) {
95 | err := NewRuntimeErrorWithMessageCause("", nil)
96 | lines := strings.Split(err.Error(), newLine)
97 | assert.Equal(t, 5, len(lines))
98 | //
99 | line := lines[0]
100 | assert.Equal(t, "RuntimeError", line)
101 | //
102 | line = lines[1]
103 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_EmptyMessage_NilError() in "))
104 | assert.True(t, strings.HasSuffix(line, "error_test.go:95"))
105 | //
106 | line = lines[2]
107 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
108 | //
109 | line = lines[3]
110 | assert.Equal(t, " --- End of error stack trace ---", line)
111 | //
112 | line = lines[4]
113 | assert.True(t, strings.HasPrefix(line, " created by testing.(*T).Run() in "))
114 | }
115 |
116 | func TestRuntimeError_Error_EmptyMessage_NormalError(t *testing.T) {
117 | cause := NewRuntimeError("this is inner error")
118 | err := NewRuntimeErrorWithMessageCause("", cause)
119 | lines := strings.Split(err.Error(), newLine)
120 | assert.Equal(t, 9, len(lines))
121 | //
122 | line := lines[0]
123 | assert.Equal(t, "RuntimeError", line)
124 | //
125 | line = lines[1]
126 | assert.Equal(t, " ---> RuntimeError: this is inner error", line)
127 | //
128 | line = lines[2]
129 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_EmptyMessage_NormalError() in "))
130 | assert.True(t, strings.HasSuffix(line, "error_test.go:117"))
131 | //
132 | line = lines[3]
133 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
134 | //
135 | line = lines[4]
136 | assert.Equal(t, " --- End of inner error stack trace ---", line)
137 | //
138 | line = lines[5]
139 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_EmptyMessage_NormalError() in "))
140 | assert.True(t, strings.HasSuffix(line, "error_test.go:118"))
141 | //
142 | line = lines[6]
143 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
144 | //
145 | line = lines[7]
146 | assert.Equal(t, " --- End of error stack trace ---", line)
147 | //
148 | line = lines[8]
149 | assert.True(t, strings.HasPrefix(line, " created by testing.(*T).Run() in "))
150 | }
151 |
152 | func TestRuntimeError_Error_NormalMessage_NilError(t *testing.T) {
153 | err := NewRuntimeErrorWithMessageCause("this is error message", nil)
154 | lines := strings.Split(err.Error(), newLine)
155 | assert.Equal(t, 5, len(lines))
156 | //
157 | line := lines[0]
158 | assert.Equal(t, "RuntimeError: this is error message", line)
159 | //
160 | line = lines[1]
161 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_NormalMessage_NilError() in "))
162 | assert.True(t, strings.HasSuffix(line, "error_test.go:153"))
163 | //
164 | line = lines[2]
165 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
166 | //
167 | line = lines[3]
168 | assert.Equal(t, " --- End of error stack trace ---", line)
169 | //
170 | line = lines[4]
171 | assert.True(t, strings.HasPrefix(line, " created by testing.(*T).Run() in "))
172 | }
173 |
174 | func TestRuntimeError_Error_NormalMessage_NormalError(t *testing.T) {
175 | cause := NewRuntimeError("this is inner error")
176 | err := NewRuntimeErrorWithMessageCause("this is error message", cause)
177 | lines := strings.Split(err.Error(), newLine)
178 | assert.Equal(t, 9, len(lines))
179 | //
180 | line := lines[0]
181 | assert.Equal(t, "RuntimeError: this is error message", line)
182 | //
183 | line = lines[1]
184 | assert.Equal(t, " ---> RuntimeError: this is inner error", line)
185 | //
186 | line = lines[2]
187 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_NormalMessage_NormalError() in "))
188 | assert.True(t, strings.HasSuffix(line, "error_test.go:175"))
189 | //
190 | line = lines[3]
191 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
192 | //
193 | line = lines[4]
194 | assert.Equal(t, " --- End of inner error stack trace ---", line)
195 | //
196 | line = lines[5]
197 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_NormalMessage_NormalError() in "))
198 | assert.True(t, strings.HasSuffix(line, "error_test.go:176"))
199 | //
200 | line = lines[6]
201 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
202 | //
203 | line = lines[7]
204 | assert.Equal(t, " --- End of error stack trace ---", line)
205 | //
206 | line = lines[8]
207 | assert.True(t, strings.HasPrefix(line, " created by testing.(*T).Run() in "))
208 | }
209 |
210 | func TestRuntimeError_Error_NilStackTrace(t *testing.T) {
211 | cause := NewRuntimeError("this is inner error")
212 | cause.(*runtimeError).stackTrace = nil
213 | err := NewRuntimeErrorWithMessageCause("this is error message", cause)
214 | err.(*runtimeError).stackTrace = nil
215 | lines := strings.Split(err.Error(), newLine)
216 | assert.Equal(t, 5, len(lines))
217 | //
218 | line := lines[0]
219 | assert.Equal(t, "RuntimeError: this is error message", line)
220 | //
221 | line = lines[1]
222 | assert.Equal(t, " ---> RuntimeError: this is inner error", line)
223 | //
224 | line = lines[2]
225 | assert.Equal(t, " --- End of inner error stack trace ---", line)
226 | //
227 | line = lines[3]
228 | assert.Equal(t, " --- End of error stack trace ---", line)
229 | //
230 | line = lines[4]
231 | assert.True(t, strings.HasPrefix(line, " created by testing.(*T).Run() in "))
232 | }
233 |
234 | func TestRuntimeError_Error_MainGoid(t *testing.T) {
235 | err := NewRuntimeErrorWithMessageCause("this is error message", nil)
236 | err.(*runtimeError).goid = 1
237 | lines := strings.Split(err.Error(), newLine)
238 | assert.Equal(t, 3, len(lines))
239 | //
240 | line := lines[0]
241 | assert.Equal(t, "RuntimeError: this is error message", line)
242 | //
243 | line = lines[1]
244 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_MainGoid() in "))
245 | assert.True(t, strings.HasSuffix(line, "error_test.go:235"))
246 | //
247 | line = lines[2]
248 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
249 | }
250 |
251 | func TestRuntimeError_Error_ZeroGopc(t *testing.T) {
252 | err := NewRuntimeErrorWithMessageCause("this is error message", nil)
253 | err.(*runtimeError).gopc = 0
254 | lines := strings.Split(err.Error(), newLine)
255 | assert.Equal(t, 3, len(lines))
256 | //
257 | line := lines[0]
258 | assert.Equal(t, "RuntimeError: this is error message", line)
259 | //
260 | line = lines[1]
261 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestRuntimeError_Error_ZeroGopc() in "))
262 | assert.True(t, strings.HasSuffix(line, "error_test.go:252"))
263 | //
264 | line = lines[2]
265 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
266 | }
267 |
268 | func TestArgumentNilError_Goid(t *testing.T) {
269 | goid := Goid()
270 | err := NewArgumentNilError("number", nil)
271 | assert.Equal(t, goid, err.Goid())
272 | task := GoWait(func(token CancelToken) {
273 | assert.Equal(t, goid, err.Goid())
274 | assert.NotEqual(t, Goid(), err.Goid())
275 | })
276 | task.Get()
277 | }
278 |
279 | func TestArgumentNilError_Gopc(t *testing.T) {
280 | gopc := getg().gopc()
281 | err := NewArgumentNilError("number", nil)
282 | assert.Equal(t, gopc, err.Gopc())
283 | task := GoWait(func(token CancelToken) {
284 | assert.Equal(t, gopc, err.Gopc())
285 | assert.NotEqual(t, getg().gopc(), err.Gopc())
286 | })
287 | task.Get()
288 | }
289 |
290 | func TestArgumentNilError_Message(t *testing.T) {
291 | err := NewArgumentNilError("", nil)
292 | assert.Equal(t, "Value cannot be null.", err.Message())
293 |
294 | err2 := NewArgumentNilError("", "Hello")
295 | assert.Equal(t, "Hello", err2.Message())
296 |
297 | err3 := NewArgumentNilError("number", nil)
298 | assert.Equal(t, "Value cannot be null."+newLine+"Parameter name: number.", err3.Message())
299 |
300 | err4 := NewArgumentNilError("number", "Hello")
301 | assert.Equal(t, "Hello"+newLine+"Parameter name: number.", err4.Message())
302 | }
303 |
304 | func TestArgumentNilError_StackTrace(t *testing.T) {
305 | err := NewArgumentNilError("", nil)
306 | stackTrace := err.StackTrace()
307 | capturedStackTrace := captureStackTrace(0, 200)
308 | for i := 1; i < len(stackTrace); i++ {
309 | assert.Equal(t, capturedStackTrace[i], stackTrace[i])
310 | }
311 | }
312 |
313 | func TestArgumentNilError_Panic_Panic(t *testing.T) {
314 | defer func() {
315 | cause := recover()
316 | assert.NotNil(t, cause)
317 | err := NewArgumentNilError("a", nil)
318 | lines := strings.Split(err.Error(), newLine)
319 | assert.Equal(t, 7, len(lines))
320 | //
321 | line := lines[0]
322 | assert.Equal(t, "ArgumentNilError: Value cannot be null.", line)
323 | //
324 | line = lines[1]
325 | assert.Equal(t, "Parameter name: a.", line)
326 | //
327 | line = lines[2]
328 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestArgumentNilError_Panic_Panic."))
329 | assert.True(t, strings.HasSuffix(line, "error_test.go:337"))
330 | //
331 | line = lines[3]
332 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestArgumentNilError_Panic_Panic()"))
333 | assert.True(t, strings.HasSuffix(line, "error_test.go:341"))
334 | }()
335 | defer func() {
336 | if cause := recover(); cause != nil {
337 | panic(cause)
338 | }
339 | }()
340 | var a any
341 | _ = a.(string)
342 | }
343 |
344 | func TestArgumentNilError_Cause(t *testing.T) {
345 | err := NewArgumentNilError("", nil)
346 | assert.Nil(t, err.Cause())
347 |
348 | err2 := NewArgumentNilError("", errors.New("error"))
349 | assert.Nil(t, err2.Cause())
350 |
351 | err3 := NewArgumentNilError("", &person{Id: 1, Name: "Tim"})
352 | assert.Nil(t, err3.Cause())
353 |
354 | err4 := NewArgumentNilError("", err)
355 | assert.Same(t, err, err4.Cause())
356 | }
357 |
358 | func TestArgumentNilError_Error(t *testing.T) {
359 | cause := NewRuntimeError("this is inner error")
360 | err := NewArgumentNilError("number", cause)
361 | lines := strings.Split(err.Error(), newLine)
362 | assert.Equal(t, 10, len(lines))
363 | //
364 | line := lines[0]
365 | assert.Equal(t, "ArgumentNilError: Value cannot be null.", line)
366 | //
367 | line = lines[1]
368 | assert.Equal(t, "Parameter name: number.", line)
369 | //
370 | line = lines[2]
371 | assert.Equal(t, " ---> RuntimeError: this is inner error", line)
372 | //
373 | line = lines[3]
374 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestArgumentNilError_Error() in "))
375 | assert.True(t, strings.HasSuffix(line, "error_test.go:359"))
376 | //
377 | line = lines[4]
378 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
379 | //
380 | line = lines[5]
381 | assert.Equal(t, " --- End of inner error stack trace ---", line)
382 | //
383 | line = lines[6]
384 | assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestArgumentNilError_Error() in "))
385 | assert.True(t, strings.HasSuffix(line, "error_test.go:360"))
386 | //
387 | line = lines[7]
388 | assert.True(t, strings.HasPrefix(line, " at testing.tRunner() in "))
389 | //
390 | line = lines[8]
391 | assert.Equal(t, " --- End of error stack trace ---", line)
392 | //
393 | line = lines[9]
394 | assert.True(t, strings.HasPrefix(line, " created by testing.(*T).Run() in "))
395 | }
396 |
397 | func TestArgumentNilError_ParamName(t *testing.T) {
398 | err := NewArgumentNilError("", nil)
399 | assert.Equal(t, "", err.ParamName())
400 |
401 | err2 := NewArgumentNilError("number", nil)
402 | assert.Equal(t, "number", err2.ParamName())
403 | }
404 |
405 | type ArgumentNilError struct {
406 | goid uint64
407 | gopc uintptr
408 | message string
409 | stackTrace []uintptr
410 | cause RuntimeError
411 | paramName string
412 | }
413 |
414 | func (ae *ArgumentNilError) Goid() uint64 {
415 | return ae.goid
416 | }
417 |
418 | func (ae *ArgumentNilError) Gopc() uintptr {
419 | return ae.gopc
420 | }
421 |
422 | func (ae *ArgumentNilError) Message() string {
423 | builder := &strings.Builder{}
424 | if len(ae.message) == 0 {
425 | builder.WriteString("Value cannot be null.")
426 | } else {
427 | builder.WriteString(ae.message)
428 | }
429 | if len(ae.paramName) != 0 {
430 | builder.WriteString(newLine)
431 | builder.WriteString("Parameter name: ")
432 | builder.WriteString(ae.paramName)
433 | builder.WriteString(".")
434 | }
435 | return builder.String()
436 | }
437 |
438 | func (ae *ArgumentNilError) StackTrace() []uintptr {
439 | return ae.stackTrace
440 | }
441 |
442 | func (ae *ArgumentNilError) Cause() RuntimeError {
443 | return ae.cause
444 | }
445 |
446 | func (ae *ArgumentNilError) Error() string {
447 | return runtimeErrorError(ae)
448 | }
449 |
450 | func (ae *ArgumentNilError) ParamName() string {
451 | return ae.paramName
452 | }
453 |
454 | func NewArgumentNilError(paramName string, cause any) *ArgumentNilError {
455 | goid, gopc, msg, stackTrace, innerErr := runtimeErrorNew(cause)
456 | return &ArgumentNilError{goid: goid, gopc: gopc, message: msg, paramName: paramName, stackTrace: stackTrace, cause: innerErr}
457 | }
458 |
--------------------------------------------------------------------------------
/future_task.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | "sync/atomic"
7 | "time"
8 | )
9 |
10 | type taskState = int32
11 |
12 | const (
13 | taskStateNew taskState = iota
14 | taskStateRunning
15 | taskStateCompleted
16 | taskStateCanceled
17 | taskStateFailed
18 | )
19 |
20 | type futureTask[TResult any] struct {
21 | await sync.WaitGroup
22 | state taskState
23 | callable FutureCallable[TResult]
24 | result TResult
25 | error RuntimeError
26 | }
27 |
28 | func (task *futureTask[TResult]) IsDone() bool {
29 | state := atomic.LoadInt32(&task.state)
30 | return state == taskStateCompleted || state == taskStateCanceled || state == taskStateFailed
31 | }
32 |
33 | func (task *futureTask[TResult]) IsCanceled() bool {
34 | return atomic.LoadInt32(&task.state) == taskStateCanceled
35 | }
36 |
37 | func (task *futureTask[TResult]) IsFailed() bool {
38 | return atomic.LoadInt32(&task.state) == taskStateFailed
39 | }
40 |
41 | func (task *futureTask[TResult]) Complete(result TResult) {
42 | if atomic.CompareAndSwapInt32(&task.state, taskStateNew, taskStateCompleted) ||
43 | atomic.CompareAndSwapInt32(&task.state, taskStateRunning, taskStateCompleted) {
44 | task.result = result
45 | task.await.Done()
46 | }
47 | }
48 |
49 | func (task *futureTask[TResult]) Cancel() {
50 | if atomic.CompareAndSwapInt32(&task.state, taskStateNew, taskStateCanceled) ||
51 | atomic.CompareAndSwapInt32(&task.state, taskStateRunning, taskStateCanceled) {
52 | task.error = NewRuntimeError("Task was canceled.")
53 | task.await.Done()
54 | }
55 | }
56 |
57 | func (task *futureTask[TResult]) Fail(error any) {
58 | if atomic.CompareAndSwapInt32(&task.state, taskStateNew, taskStateFailed) ||
59 | atomic.CompareAndSwapInt32(&task.state, taskStateRunning, taskStateFailed) {
60 | runtimeErr, isRuntimeErr := error.(RuntimeError)
61 | if !isRuntimeErr {
62 | runtimeErr = NewRuntimeError(error)
63 | }
64 | task.error = runtimeErr
65 | task.await.Done()
66 | }
67 | }
68 |
69 | func (task *futureTask[TResult]) Get() TResult {
70 | task.await.Wait()
71 | if atomic.LoadInt32(&task.state) == taskStateCompleted {
72 | return task.result
73 | }
74 | panic(task.error)
75 | }
76 |
77 | func (task *futureTask[TResult]) GetWithTimeout(timeout time.Duration) TResult {
78 | waitChan := make(chan struct{})
79 | go func() {
80 | task.await.Wait()
81 | close(waitChan)
82 | }()
83 | timer := time.NewTimer(timeout)
84 | defer timer.Stop()
85 | select {
86 | case <-waitChan:
87 | if atomic.LoadInt32(&task.state) == taskStateCompleted {
88 | return task.result
89 | }
90 | panic(task.error)
91 | case <-timer.C:
92 | task.timeout(timeout)
93 | task.await.Wait()
94 | if atomic.LoadInt32(&task.state) == taskStateCompleted {
95 | return task.result
96 | }
97 | panic(task.error)
98 | }
99 | }
100 |
101 | func (task *futureTask[TResult]) Run() {
102 | if atomic.CompareAndSwapInt32(&task.state, taskStateNew, taskStateRunning) {
103 | defer func() {
104 | if cause := recover(); cause != nil {
105 | task.Fail(cause)
106 | }
107 | }()
108 | result := task.callable(task)
109 | task.Complete(result)
110 | }
111 | }
112 |
113 | func (task *futureTask[TResult]) timeout(timeout time.Duration) {
114 | if atomic.CompareAndSwapInt32(&task.state, taskStateNew, taskStateCanceled) ||
115 | atomic.CompareAndSwapInt32(&task.state, taskStateRunning, taskStateCanceled) {
116 | task.error = NewRuntimeError(fmt.Sprintf("Task execution timeout after %v.", timeout))
117 | task.await.Done()
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/g.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "unsafe"
7 | )
8 |
9 | type g struct {
10 | }
11 |
12 | //go:norace
13 | func (g *g) goid() uint64 {
14 | return *(*uint64)(add(unsafe.Pointer(g), offsetGoid))
15 | }
16 |
17 | //go:norace
18 | func (g *g) gopc() uintptr {
19 | return *(*uintptr)(add(unsafe.Pointer(g), offsetGopc))
20 | }
21 |
22 | //go:norace
23 | func (g *g) getPanicOnFault() bool {
24 | return *(*bool)(add(unsafe.Pointer(g), offsetPaniconfault))
25 | }
26 |
27 | //go:norace
28 | func (g *g) setPanicOnFault(new bool) (old bool) {
29 | panicOnFault := (*bool)(add(unsafe.Pointer(g), offsetPaniconfault))
30 | old = *panicOnFault
31 | *panicOnFault = new
32 | return old
33 | }
34 |
35 | //go:norace
36 | func (g *g) getLabels() unsafe.Pointer {
37 | return *(*unsafe.Pointer)(add(unsafe.Pointer(g), offsetLabels))
38 | }
39 |
40 | //go:norace
41 | func (g *g) setLabels(labels unsafe.Pointer) {
42 | *(*unsafe.Pointer)(add(unsafe.Pointer(g), offsetLabels)) = labels
43 | }
44 |
45 | // getg returns current coroutine struct.
46 | func getg() *g {
47 | gp := getgp()
48 | if gp == nil {
49 | panic("Failed to get gp from runtime natively.")
50 | }
51 | return gp
52 | }
53 |
54 | // offset returns the offset of the specified field.
55 | func offset(t reflect.Type, f string) uintptr {
56 | field, found := t.FieldByName(f)
57 | if found {
58 | return field.Offset
59 | }
60 | panic(fmt.Sprintf("No such field '%v' of struct '%v.%v'.", f, t.PkgPath(), t.Name()))
61 | }
62 |
63 | // add pointer addition operation.
64 | func add(p unsafe.Pointer, x uintptr) unsafe.Pointer {
65 | return unsafe.Pointer(uintptr(p) + x)
66 | }
67 |
--------------------------------------------------------------------------------
/g/asm_386.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | #include "funcdata.h"
5 | #include "go_asm.h"
6 | #include "go_tls.h"
7 | #include "textflag.h"
8 |
9 | TEXT ·getg(SB), NOSPLIT, $0-4
10 | get_tls(CX)
11 | MOVL g(CX), AX
12 | MOVL AX, ret+0(FP)
13 | RET
14 |
--------------------------------------------------------------------------------
/g/asm_amd64.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | #include "funcdata.h"
5 | #include "go_asm.h"
6 | #include "go_tls.h"
7 | #include "textflag.h"
8 |
9 | TEXT ·getg(SB), NOSPLIT, $0-8
10 | get_tls(CX)
11 | MOVQ g(CX), AX
12 | MOVQ AX, ret+0(FP)
13 | RET
14 |
--------------------------------------------------------------------------------
/g/asm_arm.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | #include "funcdata.h"
5 | #include "go_asm.h"
6 | #include "textflag.h"
7 |
8 | TEXT ·getg(SB), NOSPLIT, $0-4
9 | MOVW g, R8
10 | MOVW R8, ret+0(FP)
11 | RET
12 |
--------------------------------------------------------------------------------
/g/asm_arm64.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | #include "funcdata.h"
5 | #include "go_asm.h"
6 | #include "textflag.h"
7 |
8 | TEXT ·getg(SB), NOSPLIT, $0-8
9 | MOVD g, R8
10 | MOVD R8, ret+0(FP)
11 | RET
12 |
--------------------------------------------------------------------------------
/g/asm_loong64.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | //go:build loong64
5 | // +build loong64
6 |
7 | #include "funcdata.h"
8 | #include "go_asm.h"
9 | #include "textflag.h"
10 |
11 | TEXT ·getg(SB), NOSPLIT, $0-8
12 | MOVV g, R8
13 | MOVV R8, ret+0(FP)
14 | RET
15 |
--------------------------------------------------------------------------------
/g/asm_mips64x.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | //go:build mips64 || mips64le
5 | // +build mips64 mips64le
6 |
7 | #include "funcdata.h"
8 | #include "go_asm.h"
9 | #include "textflag.h"
10 |
11 | TEXT ·getg(SB), NOSPLIT, $0-8
12 | MOVV g, R8
13 | MOVV R8, ret+0(FP)
14 | RET
15 |
--------------------------------------------------------------------------------
/g/asm_mipsx.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | //go:build mips || mipsle
5 | // +build mips mipsle
6 |
7 | #include "funcdata.h"
8 | #include "go_asm.h"
9 | #include "textflag.h"
10 |
11 | TEXT ·getg(SB), NOSPLIT, $0-4
12 | MOVW g, R8
13 | MOVW R8, ret+0(FP)
14 | RET
15 |
--------------------------------------------------------------------------------
/g/asm_ppc64x.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | //go:build ppc64 || ppc64le
5 | // +build ppc64 ppc64le
6 |
7 | #include "funcdata.h"
8 | #include "go_asm.h"
9 | #include "textflag.h"
10 |
11 | TEXT ·getg(SB), NOSPLIT, $0-8
12 | MOVD g, R8
13 | MOVD R8, ret+0(FP)
14 | RET
15 |
--------------------------------------------------------------------------------
/g/asm_riscv64.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | #include "funcdata.h"
5 | #include "go_asm.h"
6 | #include "textflag.h"
7 |
8 | TEXT ·getg(SB), NOSPLIT, $0-8
9 | MOV g, X10
10 | MOV X10, ret+0(FP)
11 | RET
12 |
--------------------------------------------------------------------------------
/g/asm_s390x.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | #include "funcdata.h"
5 | #include "go_asm.h"
6 | #include "textflag.h"
7 |
8 | TEXT ·getg(SB), NOSPLIT, $0-8
9 | MOVD g, R8
10 | MOVD R8, ret+0(FP)
11 | RET
12 |
--------------------------------------------------------------------------------
/g/asm_wasm.s:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | #include "funcdata.h"
5 | #include "go_asm.h"
6 | #include "textflag.h"
7 |
8 | TEXT ·getg(SB), NOSPLIT, $0-8
9 | MOVD g, R8
10 | MOVD R8, ret+0(FP)
11 | RET
12 |
--------------------------------------------------------------------------------
/g/g.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | //go:build !routinex
5 |
6 | package g
7 |
8 | import (
9 | "reflect"
10 | "unsafe"
11 | )
12 |
13 | // getg returns the pointer to the current runtime.g.
14 | //
15 | //go:nosplit
16 | func getg() unsafe.Pointer
17 |
18 | // getgp returns the pointer to the current runtime.g.
19 | //
20 | //go:nosplit
21 | //go:linkname getgp runtime.getgp
22 | func getgp() unsafe.Pointer {
23 | return getg()
24 | }
25 |
26 | // getgt returns the type of runtime.g.
27 | //
28 | //go:nosplit
29 | //go:linkname getgt runtime.getgt
30 | func getgt() reflect.Type {
31 | return typeByString("runtime.g")
32 | }
33 |
--------------------------------------------------------------------------------
/g/g_link.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | //go:build routinex
5 |
6 | package g
7 |
8 | import (
9 | "reflect"
10 | "unsafe"
11 | )
12 |
13 | // getg0 returns the value of runtime.g0.
14 | //
15 | //go:nosplit
16 | //go:linkname getg0 runtime.getg0
17 | func getg0() any
18 |
19 | // getgp returns the pointer to the current runtime.g.
20 | //
21 | //go:nosplit
22 | //go:linkname getgp runtime.getgp
23 | func getgp() unsafe.Pointer
24 |
25 | // getgt returns the type of runtime.g.
26 | //
27 | //go:nosplit
28 | //go:linkname getgt runtime.getgt
29 | func getgt() reflect.Type {
30 | return reflect.TypeOf(getg0())
31 | }
32 |
--------------------------------------------------------------------------------
/g/g_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | package g
5 |
6 | import (
7 | "reflect"
8 | "runtime"
9 | "sync"
10 | "testing"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestGetgp(t *testing.T) {
16 | gp0 := getgp()
17 | runtime.GC()
18 | assert.NotNil(t, gp0)
19 | //
20 | runTest(t, func() {
21 | gp := getgp()
22 | runtime.GC()
23 | assert.NotNil(t, gp)
24 | assert.NotEqual(t, gp0, gp)
25 | })
26 | }
27 |
28 | func TestGetgt(t *testing.T) {
29 | runTest(t, func() {
30 | gt := getgt()
31 | runtime.GC()
32 | assert.Equal(t, "g", gt.Name())
33 | //
34 | assert.Greater(t, gt.NumField(), 20)
35 | })
36 | }
37 |
38 | func TestGetg(t *testing.T) {
39 | runTest(t, func() {
40 | g := packEface(getgt(), getgp())
41 | runtime.GC()
42 | stackguard0 := reflect.ValueOf(g).FieldByName("stackguard0")
43 | assert.Greater(t, stackguard0.Uint(), uint64(0))
44 | })
45 | }
46 |
47 | func runTest(t *testing.T, fun func()) {
48 | run := false
49 | wg := &sync.WaitGroup{}
50 | wg.Add(1)
51 | go func() {
52 | fun()
53 | run = true
54 | wg.Done()
55 | }()
56 | wg.Wait()
57 | assert.True(t, run)
58 | }
59 |
--------------------------------------------------------------------------------
/g/go_tls.h:
--------------------------------------------------------------------------------
1 | // Copyright 2014 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | #ifdef GOARCH_arm
6 | #define LR R14
7 | #endif
8 |
9 | #ifdef GOARCH_amd64
10 | #define get_tls(r) MOVQ TLS, r
11 | #define g(r) 0(r)(TLS*1)
12 | #endif
13 |
14 | #ifdef GOARCH_386
15 | #define get_tls(r) MOVL TLS, r
16 | #define g(r) 0(r)(TLS*1)
17 | #endif
18 |
--------------------------------------------------------------------------------
/g/reflect.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | package g
5 |
6 | import (
7 | "reflect"
8 | "unsafe"
9 | )
10 |
11 | // eface The empty interface struct.
12 | type eface struct {
13 | _type unsafe.Pointer
14 | data unsafe.Pointer
15 | }
16 |
17 | // iface The interface struct.
18 | type iface struct {
19 | tab unsafe.Pointer
20 | data unsafe.Pointer
21 | }
22 |
23 | // typelinks returns a slice of the sections in each module,
24 | // and a slice of *rtype offsets in each module.
25 | // The types in each module are sorted by string.
26 | //
27 | //go:linkname typelinks reflect.typelinks
28 | func typelinks() (sections []unsafe.Pointer, offset [][]int32)
29 |
30 | // resolveTypeOff resolves an *rtype offset from a base type.
31 | //
32 | //go:linkname resolveTypeOff reflect.resolveTypeOff
33 | func resolveTypeOff(rtype unsafe.Pointer, off int32) unsafe.Pointer
34 |
35 | // isNil returns the data field of eface value is nil or not.
36 | //
37 | //go:linkname isNil routine.isNil
38 | func isNil(i any) bool {
39 | return (*eface)(unsafe.Pointer(&i)).data == nil
40 | }
41 |
42 | // packEface returns an empty interface representing a value of the specified type,
43 | // using p as the pointer to the data.
44 | //
45 | //go:linkname packEface routine.packEface
46 | func packEface(typ reflect.Type, p unsafe.Pointer) (i any) {
47 | t := (*iface)(unsafe.Pointer(&typ))
48 | e := (*eface)(unsafe.Pointer(&i))
49 | e._type = t.data
50 | e.data = p
51 | return
52 | }
53 |
54 | // typeByString returns the type whose 'String' property equals to the given string,
55 | // or nil if not found.
56 | //
57 | //go:linkname typeByString routine.typeByString
58 | func typeByString(str string) reflect.Type {
59 | // The s is search target
60 | s := str
61 | if len(str) == 0 || str[0] != '*' {
62 | s = "*" + s
63 | }
64 | // The typ is a struct iface{tab(ptr->reflect.Type), data(ptr->rtype)}
65 | typ := reflect.TypeOf(0)
66 | face := (*iface)(unsafe.Pointer(&typ))
67 | // Find the specified target through binary search algorithm
68 | sections, offset := typelinks()
69 | for offsI, offs := range offset {
70 | section := sections[offsI]
71 | // We are looking for the first index i where the string becomes >= s.
72 | // This is a copy of sort.Search, with f(h) replaced by (*typ[h].String() >= s).
73 | i, j := 0, len(offs)
74 | for i < j {
75 | h := int(uint(i+j) >> 1) // avoid overflow when computing h
76 | // i ≤ h < j
77 | face.data = resolveTypeOff(section, offs[h])
78 | if !(typ.String() >= s) {
79 | i = h + 1 // preserves f(i-1) == false
80 | } else {
81 | j = h // preserves f(j) == true
82 | }
83 | }
84 | // i == j, f(i-1) == false, and f(j) (= f(i)) == true => answer is i.
85 | // Having found the first, linear scan forward to find the last.
86 | // We could do a second binary search, but the caller is going
87 | // to do a linear scan anyway.
88 | if i < len(offs) {
89 | face.data = resolveTypeOff(section, offs[i])
90 | if typ.Kind() == reflect.Ptr {
91 | if typ.String() == str {
92 | return typ
93 | }
94 | elem := typ.Elem()
95 | if elem.String() == str {
96 | return elem
97 | }
98 | }
99 | }
100 | }
101 | return nil
102 | }
103 |
--------------------------------------------------------------------------------
/g/reflect_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2021-2025 TimAndy. All rights reserved.
2 | // Licensed under the Apache-2.0 license that can be found in the LICENSE file.
3 |
4 | package g
5 |
6 | import (
7 | "fmt"
8 | "reflect"
9 | "strings"
10 | "testing"
11 | "unsafe"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestIsNil(t *testing.T) {
17 | var value fmt.Stringer
18 | assert.True(t, value == nil)
19 | assert.True(t, isNil(value))
20 | //
21 | value = (*strings.Builder)(nil) //nolint:staticcheck
22 | assert.True(t, value != nil) //nolint:staticcheck
23 | assert.True(t, isNil(value))
24 | //
25 | value = &strings.Builder{} //nolint:staticcheck
26 | assert.True(t, value != nil) //nolint:staticcheck
27 | assert.True(t, !isNil(value))
28 | }
29 |
30 | func TestPackEface(t *testing.T) {
31 | value := 1
32 | valueInterface := packEface(reflect.TypeOf(0), unsafe.Pointer(&value))
33 | assert.Equal(t, value, valueInterface)
34 | //
35 | value = 2
36 | assert.Equal(t, value, valueInterface)
37 | }
38 |
39 | func TestTypeByString(t *testing.T) {
40 | gt := typeByString("runtime.g")
41 | assert.NotNil(t, gt)
42 | assert.Equal(t, "runtime.g", gt.String())
43 | fGoid, ok := gt.FieldByName("goid")
44 | assert.True(t, ok)
45 | assert.Greater(t, int(fGoid.Offset), 0)
46 | //
47 | gt2 := typeByString("*runtime.g")
48 | assert.NotNil(t, gt2)
49 | assert.Equal(t, "*runtime.g", gt2.String())
50 | fGoid2, ok2 := gt2.Elem().FieldByName("goid")
51 | assert.True(t, ok2)
52 | assert.Greater(t, int(fGoid2.Offset), 0)
53 | assert.Equal(t, fGoid.Offset, fGoid2.Offset)
54 | //
55 | assert.Nil(t, typeByString("runtime.Pointer"))
56 | }
57 |
--------------------------------------------------------------------------------
/g_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "reflect"
7 | "runtime"
8 | "strconv"
9 | "testing"
10 | "unsafe"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | var goroutineSpace = []byte("goroutine ")
16 |
17 | func TestG_Goid(t *testing.T) {
18 | runTest(t, func() {
19 | gp := getg()
20 | runtime.GC()
21 | assert.Equal(t, curGoroutineID(), gp.goid())
22 | })
23 | }
24 |
25 | func TestG_Gopc(t *testing.T) {
26 | runTest(t, func() {
27 | gp := getg()
28 | runtime.GC()
29 | assert.Greater(t, int64(gp.gopc()), int64(0))
30 | })
31 | }
32 |
33 | func TestG_PanicOnFault(t *testing.T) {
34 | runTest(t, func() {
35 | gp := getg()
36 | runtime.GC()
37 | //read-1
38 | assert.False(t, setPanicOnFault(false))
39 | assert.False(t, gp.getPanicOnFault())
40 | //read-2
41 | setPanicOnFault(true)
42 | assert.True(t, gp.getPanicOnFault())
43 | //write-1
44 | gp.setPanicOnFault(false)
45 | assert.False(t, setPanicOnFault(false))
46 | //write-2
47 | gp.setPanicOnFault(true)
48 | assert.True(t, setPanicOnFault(true))
49 | //write-read-1
50 | gp.setPanicOnFault(false)
51 | assert.False(t, gp.getPanicOnFault())
52 | //write-read-2
53 | gp.setPanicOnFault(true)
54 | assert.True(t, gp.getPanicOnFault())
55 | //restore
56 | gp.setPanicOnFault(false)
57 | })
58 | }
59 |
60 | func TestG_ProfLabel(t *testing.T) {
61 | runTest(t, func() {
62 | ptr := unsafe.Pointer(&struct{}{})
63 | null := unsafe.Pointer(nil)
64 | assert.NotEqual(t, ptr, null)
65 | //
66 | gp := getg()
67 | runtime.GC()
68 | //read-1
69 | assert.Equal(t, null, getProfLabel())
70 | assert.Equal(t, null, gp.getLabels())
71 | //read-2
72 | setProfLabel(ptr)
73 | assert.Equal(t, ptr, gp.getLabels())
74 | //write-1
75 | gp.setLabels(nil)
76 | assert.Equal(t, null, getProfLabel())
77 | //write-2
78 | gp.setLabels(ptr)
79 | assert.Equal(t, ptr, getProfLabel())
80 | //write-read-1
81 | gp.setLabels(nil)
82 | assert.Equal(t, null, gp.getLabels())
83 | //write-read-2
84 | gp.setLabels(ptr)
85 | assert.Equal(t, ptr, gp.getLabels())
86 | //restore
87 | gp.setLabels(null)
88 | })
89 | }
90 |
91 | func TestOffset(t *testing.T) {
92 | runTest(t, func() {
93 | assert.Panics(t, func() {
94 | gt := reflect.TypeOf(0)
95 | offset(gt, "hello")
96 | })
97 | assert.PanicsWithValue(t, "No such field 'hello' of struct 'runtime.g'.", func() {
98 | gt := getgt()
99 | offset(gt, "hello")
100 | })
101 | })
102 | }
103 |
104 | // curGoroutineID parse the current g's goid from caller stack.
105 | func curGoroutineID() uint64 {
106 | b := make([]byte, 64)
107 | b = b[:runtime.Stack(b, false)]
108 | // Parse the 4707 out of "goroutine 4707 ["
109 | b = bytes.TrimPrefix(b, goroutineSpace)
110 | i := bytes.IndexByte(b, ' ')
111 | if i < 0 {
112 | panic(fmt.Sprintf("No space found in %q", b))
113 | }
114 | b = b[:i]
115 | n, err := strconv.ParseUint(string(b), 10, 64)
116 | if err != nil {
117 | panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err))
118 | }
119 | return n
120 | }
121 |
122 | // setPanicOnFault controls the runtime's behavior when a program faults at an unexpected (non-nil) address.
123 | //
124 | //go:linkname setPanicOnFault runtime/debug.setPanicOnFault
125 | func setPanicOnFault(new bool) (old bool)
126 |
127 | // getProfLabel get current g's labels which will be inherited by new goroutine.
128 | //
129 | //go:linkname getProfLabel runtime/pprof.runtime_getProfLabel
130 | func getProfLabel() unsafe.Pointer
131 |
132 | // setProfLabel set current g's labels which will be inherited by new goroutine.
133 | //
134 | //go:linkname setProfLabel runtime/pprof.runtime_setProfLabel
135 | func setProfLabel(labels unsafe.Pointer)
136 |
137 | //===
138 |
139 | // BenchmarkGohack-8 258425366 4.808 ns/op 0 B/op 0 allocs/op
140 | func BenchmarkGohack(b *testing.B) {
141 | _ = getg()
142 | b.ReportAllocs()
143 | b.ResetTimer()
144 | for i := 0; i < b.N; i++ {
145 | gp := getg()
146 | _ = gp.goid()
147 | _ = gp.gopc()
148 | _ = gp.getLabels()
149 | _ = gp.getPanicOnFault()
150 | gp.setLabels(nil)
151 | gp.setPanicOnFault(false)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/g_x.go:
--------------------------------------------------------------------------------
1 | //go:build !routinex
2 |
3 | package routine
4 |
5 | const routinexEnabled = false
6 |
7 | var (
8 | offsetGoid uintptr
9 | offsetPaniconfault uintptr
10 | offsetGopc uintptr
11 | offsetLabels uintptr
12 | )
13 |
14 | func init() {
15 | gt := getgt()
16 | offsetGoid = offset(gt, "goid")
17 | offsetPaniconfault = offset(gt, "paniconfault")
18 | offsetGopc = offset(gt, "gopc")
19 | offsetLabels = offset(gt, "labels")
20 | }
21 |
--------------------------------------------------------------------------------
/g_x_link.go:
--------------------------------------------------------------------------------
1 | //go:build routinex
2 |
3 | package routine
4 |
5 | const routinexEnabled = true
6 |
7 | var (
8 | offsetGoid uintptr
9 | offsetPaniconfault uintptr
10 | offsetGopc uintptr
11 | offsetLabels uintptr
12 | offsetThreadLocals uintptr
13 | )
14 |
15 | func init() {
16 | gt := getgt()
17 | offsetGoid = offset(gt, "goid")
18 | offsetPaniconfault = offset(gt, "paniconfault")
19 | offsetGopc = offset(gt, "gopc")
20 | offsetLabels = offset(gt, "labels")
21 | offsetThreadLocals = offset(gt, "threadLocals")
22 | }
23 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/timandy/routine
2 |
3 | go 1.18
4 |
5 | require github.com/stretchr/testify v1.10.0
6 |
7 | require (
8 | github.com/davecgh/go-spew v1.1.1 // indirect
9 | github.com/pmezard/go-difflib v1.0.0 // indirect
10 | gopkg.in/yaml.v3 v3.0.1 // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11 |
--------------------------------------------------------------------------------
/pprof_label_go118.go:
--------------------------------------------------------------------------------
1 | //go:build !go1.24
2 |
3 | package routine
4 |
5 | type labelMap map[string]string
6 |
--------------------------------------------------------------------------------
/pprof_label_go124.go:
--------------------------------------------------------------------------------
1 | //go:build go1.24
2 |
3 | package routine
4 |
5 | type labelMap []any
6 |
--------------------------------------------------------------------------------
/pprof_label_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestLabelMap_Nil(t *testing.T) {
10 | var labels labelMap
11 | assert.Nil(t, labels)
12 | //
13 | labels = labelMap{}
14 | assert.NotNil(t, labels)
15 | }
16 |
17 | func TestLabelMap_Empty(t *testing.T) {
18 | var labels labelMap
19 | assert.Empty(t, labels)
20 | //
21 | labels = labelMap{}
22 | assert.Empty(t, labels)
23 | }
24 |
--------------------------------------------------------------------------------
/reflect.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "reflect"
5 | "unsafe"
6 | )
7 |
8 | // isNil returns the data field of eface value is nil or not.
9 | //
10 | //go:linkname isNil routine.isNil
11 | func isNil(i any) bool
12 |
13 | // packEface returns an empty interface representing a value of the specified type,
14 | // using p as the pointer to the data.
15 | //
16 | //go:linkname packEface routine.packEface
17 | func packEface(typ reflect.Type, p unsafe.Pointer) (i any)
18 |
19 | // typeByString returns the type whose 'String' property equals to the given string,
20 | // or nil if not found.
21 | //
22 | //go:linkname typeByString routine.typeByString
23 | func typeByString(str string) reflect.Type
24 |
--------------------------------------------------------------------------------
/reflect_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "strings"
7 | "testing"
8 | "unsafe"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestIsNil(t *testing.T) {
14 | var value fmt.Stringer
15 | assert.True(t, value == nil)
16 | assert.True(t, isNil(value))
17 | //
18 | value = (*strings.Builder)(nil) //nolint:staticcheck
19 | assert.True(t, value != nil) //nolint:staticcheck
20 | assert.True(t, isNil(value))
21 | //
22 | value = &strings.Builder{} //nolint:staticcheck
23 | assert.True(t, value != nil) //nolint:staticcheck
24 | assert.True(t, !isNil(value))
25 | }
26 |
27 | func TestPackEface(t *testing.T) {
28 | value := 1
29 | valueInterface := packEface(reflect.TypeOf(0), unsafe.Pointer(&value))
30 | assert.Equal(t, value, valueInterface)
31 | //
32 | value = 2
33 | assert.Equal(t, value, valueInterface)
34 | }
35 |
36 | func TestTypeByString(t *testing.T) {
37 | gt := typeByString("runtime.g")
38 | assert.NotNil(t, gt)
39 | assert.Equal(t, "runtime.g", gt.String())
40 | fGoid, ok := gt.FieldByName("goid")
41 | assert.True(t, ok)
42 | assert.Greater(t, int(fGoid.Offset), 0)
43 | //
44 | gt2 := typeByString("*runtime.g")
45 | assert.NotNil(t, gt2)
46 | assert.Equal(t, "*runtime.g", gt2.String())
47 | fGoid2, ok2 := gt2.Elem().FieldByName("goid")
48 | assert.True(t, ok2)
49 | assert.Greater(t, int(fGoid2.Offset), 0)
50 | assert.Equal(t, fGoid.Offset, fGoid2.Offset)
51 | //
52 | assert.Nil(t, typeByString("runtime.Pointer"))
53 | }
54 |
--------------------------------------------------------------------------------
/routine.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import "fmt"
4 |
5 | type inherited struct {
6 | }
7 |
8 | //go:norace
9 | func (inherited) reset() {
10 | t := currentThread(false)
11 | if t != nil {
12 | t.threadLocals = nil
13 | t.inheritableThreadLocals = nil
14 | }
15 | }
16 |
17 | //go:norace
18 | func (inherited) restore(t *thread, threadLocalsBackup, inheritableThreadLocalsBackup *threadLocalMap) {
19 | t.threadLocals = threadLocalsBackup
20 | t.inheritableThreadLocals = inheritableThreadLocalsBackup
21 | }
22 |
23 | type inheritedTask struct {
24 | inherited
25 | context *threadLocalMap
26 | function Runnable
27 | }
28 |
29 | //go:norace
30 | func (it inheritedTask) run(task FutureTask[any]) any {
31 | // catch
32 | defer func() {
33 | if cause := recover(); cause != nil {
34 | task.Fail(cause)
35 | if err := task.(*futureTask[any]).error; err != nil {
36 | fmt.Println(err.Error())
37 | }
38 | }
39 | }()
40 | // restore
41 | t := currentThread(it.context != nil)
42 | if t == nil {
43 | //copied is nil
44 | defer it.reset()
45 | it.function()
46 | return nil
47 | } else {
48 | threadLocalsBackup := t.threadLocals
49 | inheritableThreadLocalsBackup := t.inheritableThreadLocals
50 | defer it.restore(t, threadLocalsBackup, inheritableThreadLocalsBackup)
51 | t.threadLocals = nil
52 | t.inheritableThreadLocals = it.context
53 | it.function()
54 | return nil
55 | }
56 | }
57 |
58 | type inheritedWaitTask struct {
59 | inherited
60 | context *threadLocalMap
61 | function CancelRunnable
62 | }
63 |
64 | //go:norace
65 | func (iwt inheritedWaitTask) run(task FutureTask[any]) any {
66 | // catch
67 | defer func() {
68 | if cause := recover(); cause != nil {
69 | task.Fail(cause)
70 | }
71 | }()
72 | // restore
73 | t := currentThread(iwt.context != nil)
74 | if t == nil {
75 | //copied is nil
76 | defer iwt.reset()
77 | iwt.function(task)
78 | return nil
79 | } else {
80 | threadLocalsBackup := t.threadLocals
81 | inheritableThreadLocalsBackup := t.inheritableThreadLocals
82 | defer iwt.restore(t, threadLocalsBackup, inheritableThreadLocalsBackup)
83 | t.threadLocals = nil
84 | t.inheritableThreadLocals = iwt.context
85 | iwt.function(task)
86 | return nil
87 | }
88 | }
89 |
90 | type inheritedWaitResultTask[TResult any] struct {
91 | inherited
92 | context *threadLocalMap
93 | function CancelCallable[TResult]
94 | }
95 |
96 | //go:norace
97 | func (iwrt inheritedWaitResultTask[TResult]) run(task FutureTask[TResult]) TResult {
98 | // catch
99 | defer func() {
100 | if cause := recover(); cause != nil {
101 | task.Fail(cause)
102 | }
103 | }()
104 | // restore
105 | t := currentThread(iwrt.context != nil)
106 | if t == nil {
107 | //copied is nil
108 | defer iwrt.reset()
109 | return iwrt.function(task)
110 | } else {
111 | threadLocalsBackup := t.threadLocals
112 | inheritableThreadLocalsBackup := t.inheritableThreadLocals
113 | defer iwrt.restore(t, threadLocalsBackup, inheritableThreadLocalsBackup)
114 | t.threadLocals = nil
115 | t.inheritableThreadLocals = iwrt.context
116 | return iwrt.function(task)
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/routine_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestInheritedTask(t *testing.T) {
10 | tls := NewInheritableThreadLocal[string]()
11 | it := inheritedTask{context: nil, function: func() {
12 | assert.Equal(t, "", tls.Get())
13 | }}
14 | task := NewFutureTask(it.run)
15 | go task.Run()
16 | assert.Nil(t, task.Get())
17 | assert.True(t, task.IsDone())
18 | //
19 | it2 := inheritedTask{context: nil, function: func() {
20 | assert.Equal(t, "", tls.Get())
21 | }}
22 | task2 := NewFutureTask(it2.run)
23 | go func() {
24 | tls.Set("hello")
25 | task2.Run()
26 | }()
27 | assert.Nil(t, task2.Get())
28 | assert.True(t, task2.IsDone())
29 | //
30 | tls.Set("world")
31 | it3 := inheritedTask{context: createInheritedMap(), function: func() {
32 | assert.Equal(t, "world", tls.Get())
33 | }}
34 | task3 := NewFutureTask(it3.run)
35 | go task3.Run()
36 | assert.Nil(t, task3.Get())
37 | assert.True(t, task3.IsDone())
38 | //
39 | it4 := inheritedTask{context: createInheritedMap(), function: func() {
40 | assert.Equal(t, "world", tls.Get())
41 | }}
42 | task4 := NewFutureTask(it4.run)
43 | go func() {
44 | tls.Set("hello")
45 | task4.Run()
46 | }()
47 | assert.Nil(t, task4.Get())
48 | assert.True(t, task4.IsDone())
49 | }
50 |
51 | func TestInheritedWaitTask(t *testing.T) {
52 | tls := NewInheritableThreadLocal[string]()
53 | it := inheritedWaitTask{context: nil, function: func(token CancelToken) {
54 | assert.Equal(t, "", tls.Get())
55 | }}
56 | task := NewFutureTask(it.run)
57 | go task.Run()
58 | assert.Nil(t, task.Get())
59 | assert.True(t, task.IsDone())
60 | //
61 | it2 := inheritedWaitTask{context: nil, function: func(token CancelToken) {
62 | assert.Equal(t, "", tls.Get())
63 | }}
64 | task2 := NewFutureTask(it2.run)
65 | go func() {
66 | tls.Set("hello")
67 | task2.Run()
68 | }()
69 | assert.Nil(t, task2.Get())
70 | assert.True(t, task2.IsDone())
71 | //
72 | tls.Set("world")
73 | it3 := inheritedWaitTask{context: createInheritedMap(), function: func(token CancelToken) {
74 | assert.Equal(t, "world", tls.Get())
75 | }}
76 | task3 := NewFutureTask(it3.run)
77 | go task3.Run()
78 | assert.Nil(t, task3.Get())
79 | assert.True(t, task3.IsDone())
80 | //
81 | it4 := inheritedWaitTask{context: createInheritedMap(), function: func(token CancelToken) {
82 | assert.Equal(t, "world", tls.Get())
83 | }}
84 | task4 := NewFutureTask(it4.run)
85 | go func() {
86 | tls.Set("hello")
87 | task4.Run()
88 | }()
89 | assert.Nil(t, task4.Get())
90 | assert.True(t, task4.IsDone())
91 | }
92 |
93 | func TestInheritedWaitResultTask(t *testing.T) {
94 | tls := NewInheritableThreadLocal[string]()
95 | it := inheritedWaitResultTask[string]{context: nil, function: func(token CancelToken) string {
96 | assert.Equal(t, "", tls.Get())
97 | return tls.Get()
98 | }}
99 | task := NewFutureTask(it.run)
100 | go task.Run()
101 | assert.Equal(t, "", task.Get())
102 | assert.True(t, task.IsDone())
103 | //
104 | it2 := inheritedWaitResultTask[string]{context: nil, function: func(token CancelToken) string {
105 | assert.Equal(t, "", tls.Get())
106 | return tls.Get()
107 | }}
108 | task2 := NewFutureTask(it2.run)
109 | go func() {
110 | tls.Set("hello")
111 | task2.Run()
112 | }()
113 | assert.Equal(t, "", task2.Get())
114 | assert.True(t, task2.IsDone())
115 | //
116 | tls.Set("world")
117 | it3 := inheritedWaitResultTask[string]{context: createInheritedMap(), function: func(token CancelToken) string {
118 | assert.Equal(t, "world", tls.Get())
119 | return tls.Get()
120 | }}
121 | task3 := NewFutureTask(it3.run)
122 | go task3.Run()
123 | assert.Equal(t, "world", task3.Get())
124 | assert.True(t, task3.IsDone())
125 | //
126 | it4 := inheritedWaitResultTask[string]{context: createInheritedMap(), function: func(token CancelToken) string {
127 | assert.Equal(t, "world", tls.Get())
128 | return tls.Get()
129 | }}
130 | task4 := NewFutureTask(it4.run)
131 | go func() {
132 | tls.Set("hello")
133 | task4.Run()
134 | }()
135 | assert.Equal(t, "world", task4.Get())
136 | assert.True(t, task4.IsDone())
137 | }
138 |
--------------------------------------------------------------------------------
/runtime.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "reflect"
5 | _ "unsafe"
6 |
7 | _ "github.com/timandy/routine/g"
8 | )
9 |
10 | // getgp returns the pointer to the current runtime.g.
11 | //
12 | //go:linkname getgp runtime.getgp
13 | func getgp() *g
14 |
15 | // getgt returns the type of runtime.g.
16 | //
17 | //go:linkname getgt runtime.getgt
18 | func getgt() reflect.Type
19 |
--------------------------------------------------------------------------------
/runtime_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "reflect"
7 | "runtime"
8 | "sync"
9 | "sync/atomic"
10 | "testing"
11 | "unsafe"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestGetgp(t *testing.T) {
17 | gp0 := getgp()
18 | runtime.GC()
19 | assert.NotNil(t, gp0)
20 | //
21 | runTest(t, func() {
22 | gp := getgp()
23 | runtime.GC()
24 | assert.NotNil(t, gp)
25 | assert.NotEqual(t, unsafe.Pointer(gp0), unsafe.Pointer(gp))
26 | })
27 | }
28 |
29 | func TestGetgt(t *testing.T) {
30 | fmt.Println("*** GOOS:", runtime.GOOS, "***")
31 | fmt.Println("*** GOARCH:", runtime.GOARCH, "***")
32 | if GOARM := os.Getenv("GOARM"); len(GOARM) > 0 {
33 | fmt.Println("*** GOARM:", GOARM, "***")
34 | }
35 | if GOMIPS := os.Getenv("GOMIPS"); len(GOMIPS) > 0 {
36 | fmt.Println("*** GOMIPS:", GOMIPS, "***")
37 | }
38 | //
39 | gt := getgt()
40 | runtime.GC()
41 | assert.Equal(t, "g", gt.Name())
42 | //
43 | numField := gt.NumField()
44 | //
45 | fmt.Println("#numField:", numField)
46 | fmt.Println("#offsetGoid:", offsetGoid)
47 | fmt.Println("#offsetPaniconfault:", offsetPaniconfault)
48 | fmt.Println("#offsetGopc:", offsetGopc)
49 | fmt.Println("#offsetLabels:", offsetLabels)
50 | //
51 | assert.Greater(t, numField, 20)
52 | assert.Greater(t, int(offsetGoid), 0)
53 | assert.Greater(t, int(offsetPaniconfault), 0)
54 | assert.Greater(t, int(offsetGopc), 0)
55 | assert.Greater(t, int(offsetLabels), 0)
56 | //
57 | runTest(t, func() {
58 | tt := getgt()
59 | runtime.GC()
60 | assert.Equal(t, numField, tt.NumField())
61 | assert.Equal(t, offsetGoid, offset(tt, "goid"))
62 | assert.Equal(t, offsetPaniconfault, offset(tt, "paniconfault"))
63 | assert.Equal(t, offsetGopc, offset(tt, "gopc"))
64 | assert.Equal(t, offsetLabels, offset(tt, "labels"))
65 | })
66 | }
67 |
68 | func TestGetg(t *testing.T) {
69 | runTest(t, func() {
70 | g0 := packEface(getgt(), unsafe.Pointer(getgp()))
71 | runtime.GC()
72 | stackguard0 := reflect.ValueOf(g0).FieldByName("stackguard0")
73 | assert.Greater(t, stackguard0.Uint(), uint64(0))
74 | })
75 | }
76 |
77 | func runTest(t *testing.T, fun func()) {
78 | var count int32
79 | wg := &sync.WaitGroup{}
80 | wg.Add(10)
81 | for i := 0; i < 10; i++ {
82 | go func() {
83 | for j := 0; j < 10; j++ {
84 | fun()
85 | }
86 | atomic.AddInt32(&count, 1)
87 | wg.Done()
88 | }()
89 | }
90 | wg.Wait()
91 | assert.Equal(t, 10, int(count))
92 | }
93 |
--------------------------------------------------------------------------------
/stack.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "runtime"
5 | "strings"
6 | )
7 |
8 | const (
9 | runtimePkgPrefix = "runtime."
10 | runtimePanic = "panic"
11 | )
12 |
13 | func captureStackTrace(skip int, depth int) []uintptr {
14 | pcs := make([]uintptr, depth)
15 | return pcs[:runtime.Callers(skip+2, pcs)]
16 | }
17 |
18 | func showFrame(name string) bool {
19 | return strings.IndexByte(name, '.') >= 0 && (!strings.HasPrefix(name, runtimePkgPrefix) || isExportedRuntime(name))
20 | }
21 |
22 | func skipFrame(name string, skipped bool) bool {
23 | return !skipped && isPanicRuntime(name)
24 | }
25 |
26 | func isExportedRuntime(name string) bool {
27 | const n = len(runtimePkgPrefix)
28 | return len(name) > n && name[:n] == runtimePkgPrefix && 'A' <= name[n] && name[n] <= 'Z'
29 | }
30 |
31 | func isPanicRuntime(name string) bool {
32 | const n = len(runtimePkgPrefix)
33 | return len(name) > n && name[:n] == runtimePkgPrefix && strings.Contains(strings.ToLower(name[n:]), runtimePanic)
34 | }
35 |
--------------------------------------------------------------------------------
/stack_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "runtime"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestCaptureStackTrace(t *testing.T) {
11 | stackTrace := captureStackTrace(0, 10)
12 | assert.Greater(t, len(stackTrace), 2)
13 | frame, _ := runtime.CallersFrames(stackTrace).Next()
14 | assert.Equal(t, "github.com/timandy/routine.TestCaptureStackTrace", frame.Function)
15 | assert.Equal(t, 11, frame.Line)
16 | //
17 | stackTrace2 := captureStackSkip(1)
18 | assert.Greater(t, len(stackTrace2), 2)
19 | frame2, _ := runtime.CallersFrames(stackTrace2).Next()
20 | assert.Equal(t, "github.com/timandy/routine.TestCaptureStackTrace", frame2.Function)
21 | assert.Equal(t, 17, frame2.Line)
22 | }
23 |
24 | func TestCaptureStackTrace_Deep(t *testing.T) {
25 | stackTrace := captureStackDeep(20)
26 | assert.Greater(t, len(stackTrace), 20)
27 | frames := runtime.CallersFrames(stackTrace)
28 | //
29 | frame, more := frames.Next()
30 | assert.True(t, more)
31 | assert.Equal(t, "github.com/timandy/routine.captureStackDeepRecursive", frame.Function)
32 | assert.Equal(t, 93, frame.Line)
33 | //
34 | frame2, more2 := frames.Next()
35 | assert.True(t, more2)
36 | assert.Equal(t, "github.com/timandy/routine.captureStackDeepRecursive", frame2.Function)
37 | assert.Equal(t, 91, frame2.Line)
38 | }
39 |
40 | func TestCaptureStackTrace_Overflow(t *testing.T) {
41 | stackTrace := captureStackDeep(200)
42 | assert.Equal(t, 100, len(stackTrace))
43 | }
44 |
45 | func TestShowFrame(t *testing.T) {
46 | assert.False(t, showFrame("make"))
47 | assert.True(t, showFrame("strings.equal"))
48 | assert.True(t, showFrame("strings.Equal"))
49 | assert.False(t, showFrame("runtime.hello"))
50 | assert.True(t, showFrame("runtime.Hello"))
51 | }
52 |
53 | func TestSkipFrame(t *testing.T) {
54 | assert.False(t, skipFrame("runtime.a", true))
55 | assert.False(t, skipFrame("runtime.gopanic", true))
56 | assert.False(t, skipFrame("runtime.a", false))
57 | assert.True(t, skipFrame("runtime.gopanic", false))
58 | }
59 |
60 | func TestIsExportedRuntime(t *testing.T) {
61 | assert.False(t, isExportedRuntime(""))
62 | assert.False(t, isExportedRuntime("runtime."))
63 | assert.False(t, isExportedRuntime("hello_world"))
64 | assert.False(t, isExportedRuntime("runtime._"))
65 | assert.False(t, isExportedRuntime("runtime.a"))
66 | assert.True(t, isExportedRuntime("runtime.Hello"))
67 | assert.True(t, isExportedRuntime("runtime.Panic"))
68 | }
69 |
70 | func TestIsPanicRuntime(t *testing.T) {
71 | assert.False(t, isPanicRuntime(""))
72 | assert.False(t, isPanicRuntime("runtime."))
73 | assert.False(t, isPanicRuntime("hello_world"))
74 | assert.False(t, isPanicRuntime("runtime.a"))
75 | assert.True(t, isPanicRuntime("runtime.goPanicIndex"))
76 | assert.True(t, isPanicRuntime("runtime.gopanic"))
77 | assert.True(t, isPanicRuntime("runtime.panicshift"))
78 | }
79 |
80 | func captureStackSkip(skip int) []uintptr {
81 | return captureStackTrace(skip, 100)
82 | }
83 |
84 | func captureStackDeep(deep int) []uintptr {
85 | return captureStackDeepRecursive(1, deep)
86 | }
87 |
88 | func captureStackDeepRecursive(cur int, deep int) []uintptr {
89 | if cur < deep {
90 | cur++
91 | return captureStackDeepRecursive(cur, deep)
92 | }
93 | return captureStackTrace(0, 100)
94 | }
95 |
--------------------------------------------------------------------------------
/thread.go:
--------------------------------------------------------------------------------
1 | //go:build !routinex
2 |
3 | package routine
4 |
5 | import (
6 | "runtime"
7 | "unsafe"
8 | )
9 |
10 | const threadMagic = uint64('r')<<48 |
11 | uint64('o')<<40 |
12 | uint64('u')<<32 |
13 | uint64('t')<<24 |
14 | uint64('i')<<16 |
15 | uint64('n')<<8 |
16 | uint64('e')
17 |
18 | type thread struct {
19 | labels labelMap //pprof
20 | magic uint64 //mark
21 | id uint64 //goid
22 | threadLocals *threadLocalMap
23 | inheritableThreadLocals *threadLocalMap
24 | }
25 |
26 | // finalize reset thread's memory.
27 | func (t *thread) finalize() {
28 | t.labels = nil
29 | t.magic = 0
30 | t.id = 0
31 | t.threadLocals = nil
32 | t.inheritableThreadLocals = nil
33 | }
34 |
35 | // currentThread returns a pointer to the currently executing goroutine's thread struct.
36 | //
37 | //go:norace
38 | //go:nocheckptr
39 | func currentThread(create bool) *thread {
40 | gp := getg()
41 | goid := gp.goid()
42 | label := gp.getLabels()
43 | //nothing inherited
44 | if label == nil {
45 | if create {
46 | newt := &thread{labels: nil, magic: threadMagic, id: goid}
47 | runtime.SetFinalizer(newt, (*thread).finalize)
48 | gp.setLabels(unsafe.Pointer(newt))
49 | return newt
50 | }
51 | return nil
52 | }
53 | //inherited map then create
54 | t, magic, id := extractThread(gp, label)
55 | if magic != threadMagic {
56 | if create {
57 | mp := *(*labelMap)(label)
58 | newt := &thread{labels: mp, magic: threadMagic, id: goid}
59 | runtime.SetFinalizer(newt, (*thread).finalize)
60 | gp.setLabels(unsafe.Pointer(newt))
61 | return newt
62 | }
63 | return nil
64 | }
65 | //inherited thread then recreate
66 | if id != goid {
67 | if create || t.labels != nil {
68 | newt := &thread{labels: t.labels, magic: threadMagic, id: goid}
69 | runtime.SetFinalizer(newt, (*thread).finalize)
70 | gp.setLabels(unsafe.Pointer(newt))
71 | return newt
72 | }
73 | gp.setLabels(nil)
74 | return nil
75 | }
76 | //all is ok
77 | return t
78 | }
79 |
80 | // extractThread extract thread from unsafe.Pointer and catch fault error.
81 | //
82 | //go:norace
83 | //go:nocheckptr
84 | func extractThread(gp *g, label unsafe.Pointer) (t *thread, magic uint64, id uint64) {
85 | old := gp.setPanicOnFault(true)
86 | defer func() {
87 | gp.setPanicOnFault(old)
88 | recover() //nolint:errcheck
89 | }()
90 | t = (*thread)(label)
91 | return t, t.magic, t.id
92 | }
93 |
--------------------------------------------------------------------------------
/thread_link.go:
--------------------------------------------------------------------------------
1 | //go:build routinex
2 |
3 | package routine
4 |
5 | import "unsafe"
6 |
7 | type thread struct {
8 | threadLocals *threadLocalMap
9 | inheritableThreadLocals *threadLocalMap
10 | }
11 |
12 | // currentThread returns a pointer to the currently executing goroutine's thread struct.
13 | //
14 | //go:norace
15 | //go:nocheckptr
16 | func currentThread(create bool) *thread {
17 | gp := getg()
18 | return (*thread)(add(unsafe.Pointer(gp), offsetThreadLocals))
19 | }
20 |
--------------------------------------------------------------------------------
/thread_local.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import "sync/atomic"
4 |
5 | var threadLocalIndex int32 = -1
6 |
7 | func nextThreadLocalIndex() int {
8 | index := atomic.AddInt32(&threadLocalIndex, 1)
9 | if index < 0 {
10 | atomic.AddInt32(&threadLocalIndex, -1)
11 | panic("too many thread-local indexed variables")
12 | }
13 | return int(index)
14 | }
15 |
16 | type threadLocal[T any] struct {
17 | index int
18 | supplier Supplier[T]
19 | }
20 |
21 | func (tls *threadLocal[T]) Get() T {
22 | t := currentThread(true)
23 | mp := tls.getMap(t)
24 | if mp != nil {
25 | v := mp.get(tls.index)
26 | if v != unset {
27 | return entryValue[T](v)
28 | }
29 | }
30 | return tls.setInitialValue(t)
31 | }
32 |
33 | func (tls *threadLocal[T]) Set(value T) {
34 | t := currentThread(true)
35 | mp := tls.getMap(t)
36 | if mp != nil {
37 | mp.set(tls.index, entry(value))
38 | } else {
39 | tls.createMap(t, value)
40 | }
41 | }
42 |
43 | func (tls *threadLocal[T]) Remove() {
44 | t := currentThread(false)
45 | if t == nil {
46 | return
47 | }
48 | mp := tls.getMap(t)
49 | if mp != nil {
50 | mp.remove(tls.index)
51 | }
52 | }
53 |
54 | //go:norace
55 | func (tls *threadLocal[T]) getMap(t *thread) *threadLocalMap {
56 | return t.threadLocals
57 | }
58 |
59 | //go:norace
60 | func (tls *threadLocal[T]) createMap(t *thread, firstValue T) {
61 | mp := &threadLocalMap{}
62 | mp.set(tls.index, entry(firstValue))
63 | t.threadLocals = mp
64 | }
65 |
66 | func (tls *threadLocal[T]) setInitialValue(t *thread) T {
67 | value := tls.initialValue()
68 | mp := tls.getMap(t)
69 | if mp != nil {
70 | mp.set(tls.index, entry(value))
71 | } else {
72 | tls.createMap(t, value)
73 | }
74 | return value
75 | }
76 |
77 | func (tls *threadLocal[T]) initialValue() T {
78 | if tls.supplier == nil {
79 | var defaultValue T
80 | return defaultValue
81 | }
82 | return tls.supplier()
83 | }
84 |
--------------------------------------------------------------------------------
/thread_local_inheritable.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import "sync/atomic"
4 |
5 | var inheritableThreadLocalIndex int32 = -1
6 |
7 | func nextInheritableThreadLocalIndex() int {
8 | index := atomic.AddInt32(&inheritableThreadLocalIndex, 1)
9 | if index < 0 {
10 | atomic.AddInt32(&inheritableThreadLocalIndex, -1)
11 | panic("too many inheritable-thread-local indexed variables")
12 | }
13 | return int(index)
14 | }
15 |
16 | type inheritableThreadLocal[T any] struct {
17 | index int
18 | supplier Supplier[T]
19 | }
20 |
21 | func (tls *inheritableThreadLocal[T]) Get() T {
22 | t := currentThread(true)
23 | mp := tls.getMap(t)
24 | if mp != nil {
25 | v := mp.get(tls.index)
26 | if v != unset {
27 | return entryValue[T](v)
28 | }
29 | }
30 | return tls.setInitialValue(t)
31 | }
32 |
33 | func (tls *inheritableThreadLocal[T]) Set(value T) {
34 | t := currentThread(true)
35 | mp := tls.getMap(t)
36 | if mp != nil {
37 | mp.set(tls.index, entry(value))
38 | } else {
39 | tls.createMap(t, value)
40 | }
41 | }
42 |
43 | func (tls *inheritableThreadLocal[T]) Remove() {
44 | t := currentThread(false)
45 | if t == nil {
46 | return
47 | }
48 | mp := tls.getMap(t)
49 | if mp != nil {
50 | mp.remove(tls.index)
51 | }
52 | }
53 |
54 | //go:norace
55 | func (tls *inheritableThreadLocal[T]) getMap(t *thread) *threadLocalMap {
56 | return t.inheritableThreadLocals
57 | }
58 |
59 | //go:norace
60 | func (tls *inheritableThreadLocal[T]) createMap(t *thread, firstValue T) {
61 | mp := &threadLocalMap{}
62 | mp.set(tls.index, entry(firstValue))
63 | t.inheritableThreadLocals = mp
64 | }
65 |
66 | func (tls *inheritableThreadLocal[T]) setInitialValue(t *thread) T {
67 | value := tls.initialValue()
68 | mp := tls.getMap(t)
69 | if mp != nil {
70 | mp.set(tls.index, entry(value))
71 | } else {
72 | tls.createMap(t, value)
73 | }
74 | return value
75 | }
76 |
77 | func (tls *inheritableThreadLocal[T]) initialValue() T {
78 | if tls.supplier == nil {
79 | var defaultValue T
80 | return defaultValue
81 | }
82 | return tls.supplier()
83 | }
84 |
--------------------------------------------------------------------------------
/thread_local_inheritable_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "math"
5 | "sync"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestInheritableThreadLocal_Index(t *testing.T) {
12 | tls := NewInheritableThreadLocal[string]()
13 | assert.GreaterOrEqual(t, tls.(*inheritableThreadLocal[string]).index, 0)
14 | tls2 := NewInheritableThreadLocalWithInitial[string](func() string {
15 | return "Hello"
16 | })
17 | assert.Greater(t, tls2.(*inheritableThreadLocal[string]).index, tls.(*inheritableThreadLocal[string]).index)
18 | }
19 |
20 | func TestInheritableThreadLocal_NextIndex(t *testing.T) {
21 | backup := inheritableThreadLocalIndex
22 | defer func() {
23 | inheritableThreadLocalIndex = backup
24 | }()
25 | //
26 | inheritableThreadLocalIndex = math.MaxInt32
27 | assert.Panics(t, func() {
28 | nextInheritableThreadLocalIndex()
29 | })
30 | assert.Equal(t, math.MaxInt32, int(inheritableThreadLocalIndex))
31 | }
32 |
33 | func TestInheritableThreadLocal_Common(t *testing.T) {
34 | tls := NewInheritableThreadLocal[int]()
35 | tls2 := NewInheritableThreadLocal[string]()
36 | tls.Remove()
37 | tls2.Remove()
38 | assert.Equal(t, 0, tls.Get())
39 | assert.Equal(t, "", tls2.Get())
40 | //
41 | tls.Set(1)
42 | tls2.Set("World")
43 | assert.Equal(t, 1, tls.Get())
44 | assert.Equal(t, "World", tls2.Get())
45 | //
46 | tls.Set(0)
47 | tls2.Set("")
48 | assert.Equal(t, 0, tls.Get())
49 | assert.Equal(t, "", tls2.Get())
50 | //
51 | tls.Set(2)
52 | tls2.Set("!")
53 | assert.Equal(t, 2, tls.Get())
54 | assert.Equal(t, "!", tls2.Get())
55 | //
56 | tls.Remove()
57 | tls2.Remove()
58 | assert.Equal(t, 0, tls.Get())
59 | assert.Equal(t, "", tls2.Get())
60 | //
61 | tls.Set(2)
62 | tls2.Set("!")
63 | assert.Equal(t, 2, tls.Get())
64 | assert.Equal(t, "!", tls2.Get())
65 | wg := &sync.WaitGroup{}
66 | wg.Add(100)
67 | for i := 0; i < 100; i++ {
68 | Go(func() {
69 | assert.Equal(t, 2, tls.Get())
70 | assert.Equal(t, "!", tls2.Get())
71 | wg.Done()
72 | })
73 | }
74 | wg.Wait()
75 | assert.Equal(t, 2, tls.Get())
76 | assert.Equal(t, "!", tls2.Get())
77 | }
78 |
79 | func TestInheritableThreadLocal_Mixed(t *testing.T) {
80 | tls := NewInheritableThreadLocal[int]()
81 | tls2 := NewInheritableThreadLocalWithInitial[string](func() string {
82 | return "Hello"
83 | })
84 | assert.Equal(t, 0, tls.Get())
85 | assert.Equal(t, "Hello", tls2.Get())
86 | //
87 | tls.Set(1)
88 | tls2.Set("World")
89 | assert.Equal(t, 1, tls.Get())
90 | assert.Equal(t, "World", tls2.Get())
91 | //
92 | tls.Set(0)
93 | tls2.Set("")
94 | assert.Equal(t, 0, tls.Get())
95 | assert.Equal(t, "", tls2.Get())
96 | //
97 | tls.Set(2)
98 | tls2.Set("!")
99 | assert.Equal(t, 2, tls.Get())
100 | assert.Equal(t, "!", tls2.Get())
101 | //
102 | tls.Remove()
103 | tls2.Remove()
104 | assert.Equal(t, 0, tls.Get())
105 | assert.Equal(t, "Hello", tls2.Get())
106 | //
107 | tls.Set(2)
108 | tls2.Set("!")
109 | assert.Equal(t, 2, tls.Get())
110 | assert.Equal(t, "!", tls2.Get())
111 | wg := &sync.WaitGroup{}
112 | wg.Add(100)
113 | for i := 0; i < 100; i++ {
114 | Go(func() {
115 | assert.Equal(t, 2, tls.Get())
116 | assert.Equal(t, "!", tls2.Get())
117 | wg.Done()
118 | })
119 | }
120 | wg.Wait()
121 | assert.Equal(t, 2, tls.Get())
122 | assert.Equal(t, "!", tls2.Get())
123 | }
124 |
125 | func TestInheritableThreadLocal_WithInitial(t *testing.T) {
126 | src := &person{Id: 1, Name: "Tim"}
127 | tls := NewInheritableThreadLocalWithInitial[*person](nil)
128 | tls2 := NewInheritableThreadLocalWithInitial[*person](func() *person {
129 | var value *person
130 | return value
131 | })
132 | tls3 := NewInheritableThreadLocalWithInitial[*person](func() *person {
133 | return src
134 | })
135 | tls4 := NewInheritableThreadLocalWithInitial[person](func() person {
136 | return *src
137 | })
138 |
139 | for i := 0; i < 100; i++ {
140 | p := tls.Get()
141 | assert.Nil(t, p)
142 | //
143 | p2 := tls2.Get()
144 | assert.Nil(t, p2)
145 | //
146 | p3 := tls3.Get()
147 | assert.Same(t, src, p3)
148 |
149 | p4 := tls4.Get()
150 | assert.NotSame(t, src, &p4)
151 | assert.Equal(t, *src, p4)
152 |
153 | wg := &sync.WaitGroup{}
154 | wg.Add(1)
155 | Go(func() {
156 | assert.Same(t, src, tls3.Get())
157 | p5 := tls4.Get()
158 | assert.NotSame(t, src, &p5)
159 | assert.Equal(t, *src, p5)
160 | //
161 | wg.Done()
162 | })
163 | wg.Wait()
164 | }
165 |
166 | tls3.Set(nil)
167 | tls4.Set(person{})
168 | assert.Nil(t, tls3.Get())
169 | assert.Equal(t, person{}, tls4.Get())
170 |
171 | tls3.Remove()
172 | tls4.Remove()
173 | assert.Same(t, src, tls3.Get())
174 | p6 := tls4.Get()
175 | assert.NotSame(t, src, &p6)
176 | assert.Equal(t, *src, p6)
177 | }
178 |
179 | func TestInheritableThreadLocal_CrossCoroutine(t *testing.T) {
180 | tls := NewInheritableThreadLocal[string]()
181 | tls.Set("Hello")
182 | assert.Equal(t, "Hello", tls.Get())
183 | subWait := &sync.WaitGroup{}
184 | subWait.Add(2)
185 | finishWait := &sync.WaitGroup{}
186 | finishWait.Add(2)
187 | go func() {
188 | subWait.Wait()
189 | assert.Equal(t, "", tls.Get())
190 | finishWait.Done()
191 | }()
192 | Go(func() {
193 | subWait.Wait()
194 | assert.Equal(t, "Hello", tls.Get())
195 | finishWait.Done()
196 | })
197 | tls.Remove() //remove in parent goroutine should not affect child goroutine
198 | subWait.Done() //allow sub goroutine run
199 | subWait.Done() //allow sub goroutine run
200 | finishWait.Wait() //wait sub goroutine done
201 | finishWait.Wait() //wait sub goroutine done
202 | }
203 |
204 | func TestInheritableThreadLocal_CreateBatch(t *testing.T) {
205 | const count = 128
206 | tlsList := make([]ThreadLocal[int], count)
207 | for i := 0; i < count; i++ {
208 | value := i
209 | tlsList[i] = NewInheritableThreadLocalWithInitial[int](func() int { return value })
210 | }
211 | for i := 0; i < count; i++ {
212 | assert.Equal(t, i, tlsList[i].Get())
213 | }
214 | }
215 |
216 | func TestInheritableThreadLocal_Copy(t *testing.T) {
217 | tls := NewInheritableThreadLocalWithInitial[*person](func() *person {
218 | return &person{Id: 1, Name: "Tim"}
219 | })
220 | tls2 := NewInheritableThreadLocalWithInitial[person](func() person {
221 | return person{Id: 2, Name: "Andy"}
222 | })
223 |
224 | p1 := tls.Get()
225 | assert.Equal(t, 1, p1.Id)
226 | assert.Equal(t, "Tim", p1.Name)
227 | p2 := tls2.Get()
228 | assert.Equal(t, 2, p2.Id)
229 | assert.Equal(t, "Andy", p2.Name)
230 | //
231 | task := GoWait(func(token CancelToken) {
232 | p3 := tls.Get()
233 | assert.Same(t, p1, p3)
234 | assert.Equal(t, 1, p3.Id)
235 | assert.Equal(t, "Tim", p1.Name)
236 | p4 := tls2.Get()
237 | assert.NotSame(t, &p2, &p4)
238 | assert.Equal(t, p2, p4)
239 | assert.Equal(t, 2, p4.Id)
240 | assert.Equal(t, "Andy", p4.Name)
241 | //
242 | p3.Name = "Tim2"
243 | p4.Name = "Andy2"
244 | })
245 | task.Get()
246 | //
247 | p5 := tls.Get()
248 | assert.Same(t, p1, p5)
249 | assert.Equal(t, 1, p5.Id)
250 | assert.Equal(t, "Tim2", p5.Name)
251 | p6 := tls2.Get()
252 | assert.NotSame(t, &p2, &p6)
253 | assert.Equal(t, p2, p6)
254 | assert.Equal(t, 2, p6.Id)
255 | assert.Equal(t, "Andy", p6.Name)
256 | }
257 |
258 | func TestInheritableThreadLocal_Cloneable(t *testing.T) {
259 | tls := NewInheritableThreadLocalWithInitial[*personCloneable](func() *personCloneable {
260 | return &personCloneable{Id: 1, Name: "Tim"}
261 | })
262 | tls2 := NewInheritableThreadLocalWithInitial[personCloneable](func() personCloneable {
263 | return personCloneable{Id: 2, Name: "Andy"}
264 | })
265 |
266 | p1 := tls.Get()
267 | assert.Equal(t, 1, p1.Id)
268 | assert.Equal(t, "Tim", p1.Name)
269 | p2 := tls2.Get()
270 | assert.Equal(t, 2, p2.Id)
271 | assert.Equal(t, "Andy", p2.Name)
272 | //
273 | task := GoWait(func(token CancelToken) {
274 | p3 := tls.Get() //p3 is clone from p1
275 | assert.NotSame(t, p1, p3)
276 | assert.Equal(t, 1, p3.Id)
277 | assert.Equal(t, "Tim", p1.Name)
278 | p4 := tls2.Get()
279 | assert.NotSame(t, &p2, &p4)
280 | assert.Equal(t, p2, p4)
281 | assert.Equal(t, 2, p4.Id)
282 | assert.Equal(t, "Andy", p4.Name)
283 | //
284 | p3.Name = "Tim2"
285 | p4.Name = "Andy2"
286 | })
287 | task.Get()
288 | //
289 | p5 := tls.Get()
290 | assert.Same(t, p1, p5)
291 | assert.Equal(t, 1, p5.Id)
292 | assert.Equal(t, "Tim", p5.Name)
293 | p6 := tls2.Get()
294 | assert.NotSame(t, &p2, &p6)
295 | assert.Equal(t, p2, p6)
296 | assert.Equal(t, 2, p6.Id)
297 | assert.Equal(t, "Andy", p6.Name)
298 | }
299 |
--------------------------------------------------------------------------------
/thread_local_map.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | var unset entry = &object{}
4 |
5 | type object struct {
6 | none bool //nolint:unused
7 | }
8 |
9 | type threadLocalMap struct {
10 | table []entry
11 | }
12 |
13 | func (mp *threadLocalMap) get(index int) entry {
14 | lookup := mp.table
15 | if index < len(lookup) {
16 | return lookup[index]
17 | }
18 | return unset
19 | }
20 |
21 | func (mp *threadLocalMap) set(index int, value entry) {
22 | lookup := mp.table
23 | if index < len(lookup) {
24 | lookup[index] = value
25 | return
26 | }
27 | mp.expandAndSet(index, value)
28 | }
29 |
30 | func (mp *threadLocalMap) remove(index int) {
31 | lookup := mp.table
32 | if index < len(lookup) {
33 | lookup[index] = unset
34 | }
35 | }
36 |
37 | func (mp *threadLocalMap) expandAndSet(index int, value entry) {
38 | oldArray := mp.table
39 | oldCapacity := len(oldArray)
40 | newCapacity := index
41 | newCapacity |= newCapacity >> 1
42 | newCapacity |= newCapacity >> 2
43 | newCapacity |= newCapacity >> 4
44 | newCapacity |= newCapacity >> 8
45 | newCapacity |= newCapacity >> 16
46 | newCapacity++
47 |
48 | newArray := make([]entry, newCapacity)
49 | copy(newArray, oldArray)
50 | fill(newArray, oldCapacity, newCapacity, unset)
51 | newArray[index] = value
52 | mp.table = newArray
53 | }
54 |
55 | //go:norace
56 | func createInheritedMap() *threadLocalMap {
57 | parent := currentThread(false)
58 | if parent == nil {
59 | return nil
60 | }
61 | parentMap := parent.inheritableThreadLocals
62 | if parentMap == nil {
63 | return nil
64 | }
65 | lookup := parentMap.table
66 | if lookup == nil {
67 | return nil
68 | }
69 | table := make([]entry, len(lookup))
70 | copy(table, lookup)
71 | for i := 0; i < len(table); i++ {
72 | if c, ok := entryAssert[Cloneable](table[i]); ok && !isNil(c) {
73 | table[i] = entry(c.Clone())
74 | }
75 | }
76 | return &threadLocalMap{table: table}
77 | }
78 |
79 | func fill[T any](a []T, fromIndex int, toIndex int, val T) {
80 | for i := fromIndex; i < toIndex; i++ {
81 | a[i] = val
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/thread_local_map_entry.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | type entry any
4 |
5 | func entryValue[T any](e entry) T {
6 | if e == nil {
7 | var defaultValue T
8 | return defaultValue
9 | }
10 | return e.(T)
11 | }
12 |
13 | func entryAssert[T any](e entry) (T, bool) {
14 | v, ok := e.(T)
15 | return v, ok
16 | }
17 |
--------------------------------------------------------------------------------
/thread_local_map_entry_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestEntry_Clone(t *testing.T) {
10 | expect := &personCloneable{Id: 1, Name: "Hello"}
11 | e := entry(expect)
12 | value := entryValue[*personCloneable](e)
13 | assert.NotNil(t, value)
14 | assert.Equal(t, *expect, *value)
15 | //
16 | c, ok := entryAssert[Cloneable](e)
17 | assert.True(t, ok)
18 | assert.Equal(t, *expect, *c.(*personCloneable))
19 | //
20 | copied := entry(c.Clone())
21 | value2 := entryValue[*personCloneable](copied)
22 | assert.NotNil(t, value2)
23 | assert.Equal(t, *expect, *value2)
24 | //
25 | c3, ok2 := entryAssert[Cloneable](copied)
26 | assert.True(t, ok2)
27 | assert.Equal(t, *expect, *c3.(*personCloneable))
28 | }
29 |
30 | //===
31 |
32 | func TestEntry_Value_Nil(t *testing.T) {
33 | e := entry(nil)
34 | value := entryValue[any](e)
35 | assert.Nil(t, value)
36 | }
37 |
38 | func TestEntry_Value_NotNil(t *testing.T) {
39 | expect := 1
40 | e := entry(expect)
41 | value := entryValue[int](e)
42 | assert.Equal(t, expect, value)
43 | }
44 |
45 | func TestEntry_Value_Default(t *testing.T) {
46 | expect := 0
47 | e := entry(expect)
48 | value := entryValue[int](e)
49 | assert.Equal(t, expect, value)
50 | }
51 |
52 | func TestEntry_Value_Interface_Nil(t *testing.T) {
53 | var expect Cloneable
54 | e := entry(expect)
55 | value := entryValue[Cloneable](e)
56 | assert.Nil(t, value)
57 | }
58 |
59 | func TestEntry_Value_Interface_NotNil(t *testing.T) {
60 | var expect Cloneable = &personCloneable{Id: 1, Name: "Hello"}
61 | e := entry(expect)
62 | value := entryValue[Cloneable](e)
63 | assert.Same(t, expect, value)
64 | }
65 |
66 | func TestEntry_Value_Pointer_Nil(t *testing.T) {
67 | var expect *personCloneable
68 | e := entry(expect)
69 | value := entryValue[*personCloneable](e)
70 | assert.Nil(t, value)
71 | }
72 |
73 | func TestEntry_Value_Pointer_NotNil(t *testing.T) {
74 | expect := &personCloneable{Id: 1, Name: "Hello"}
75 | e := entry(expect)
76 | value := entryValue[*personCloneable](e)
77 | assert.Same(t, expect, value)
78 | }
79 |
80 | func TestEntry_Value_Struct_Default(t *testing.T) {
81 | expect := personCloneable{}
82 | e := entry(expect)
83 | value := entryValue[personCloneable](e)
84 | assert.Equal(t, expect, value)
85 | }
86 |
87 | func TestEntry_Value_Struct_NotDefault(t *testing.T) {
88 | expect := personCloneable{Id: 1, Name: "Hello"}
89 | e := entry(expect)
90 | value := entryValue[personCloneable](e)
91 | assert.Equal(t, expect, value)
92 | }
93 |
94 | //===
95 |
96 | func TestEntry_Assert_Nil(t *testing.T) {
97 | e := entry(nil)
98 | value, ok := entryAssert[any](e)
99 | assert.False(t, ok)
100 | assert.Nil(t, value)
101 | }
102 |
103 | func TestEntry_Assert_NotNil(t *testing.T) {
104 | expect := 1
105 | e := entry(expect)
106 | value, ok := entryAssert[int](e)
107 | assert.True(t, ok)
108 | assert.Equal(t, expect, value)
109 | }
110 |
111 | func TestEntry_Assert_Default(t *testing.T) {
112 | expect := 0
113 | e := entry(expect)
114 | value, ok := entryAssert[int](e)
115 | assert.True(t, ok)
116 | assert.Equal(t, expect, value)
117 | }
118 |
119 | func TestEntry_Assert_Interface_Nil(t *testing.T) {
120 | var expect Cloneable
121 | e := entry(expect)
122 | value, ok := entryAssert[Cloneable](e)
123 | assert.False(t, ok)
124 | assert.Nil(t, value)
125 | }
126 |
127 | func TestEntry_Assert_Interface_NotNil(t *testing.T) {
128 | var expect Cloneable = &personCloneable{Id: 1, Name: "Hello"}
129 | e := entry(expect)
130 | value, ok := entryAssert[Cloneable](e)
131 | assert.True(t, ok)
132 | assert.Same(t, expect, value)
133 | }
134 |
135 | func TestEntry_Assert_Pointer_Nil(t *testing.T) {
136 | var expect *personCloneable
137 | e := entry(expect)
138 | value, ok := entryAssert[*personCloneable](e)
139 | assert.True(t, ok)
140 | assert.Nil(t, value)
141 | }
142 |
143 | func TestEntry_Assert_Pointer_NotNil(t *testing.T) {
144 | expect := &personCloneable{Id: 1, Name: "Hello"}
145 | e := entry(expect)
146 | value, ok := entryAssert[*personCloneable](e)
147 | assert.True(t, ok)
148 | assert.Same(t, expect, value)
149 | }
150 |
151 | func TestEntry_Assert_Struct_Default(t *testing.T) {
152 | expect := personCloneable{}
153 | e := entry(expect)
154 | value, ok := entryAssert[personCloneable](e)
155 | assert.True(t, ok)
156 | assert.Equal(t, expect, value)
157 | }
158 |
159 | func TestEntry_Assert_Struct_NotDefault(t *testing.T) {
160 | expect := personCloneable{Id: 1, Name: "Hello"}
161 | e := entry(expect)
162 | value, ok := entryAssert[personCloneable](e)
163 | assert.True(t, ok)
164 | assert.Equal(t, expect, value)
165 | }
166 |
--------------------------------------------------------------------------------
/thread_local_map_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "math/rand"
5 | "sync"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestObject(t *testing.T) {
12 | var value entry = &object{}
13 | assert.NotSame(t, unset, value)
14 | //
15 | var value2 entry = &object{}
16 | assert.NotSame(t, value, value2)
17 | //
18 | var value3 any = unset
19 | assert.Same(t, unset, value3)
20 | }
21 |
22 | func TestCreateInheritedMap(t *testing.T) {
23 | wg := &sync.WaitGroup{}
24 | wg.Add(1)
25 | go func() {
26 | thd := currentThread(true)
27 | assert.NotNil(t, thd)
28 | assert.Nil(t, thd.inheritableThreadLocals)
29 | thd.inheritableThreadLocals = &threadLocalMap{}
30 | assert.Nil(t, thd.inheritableThreadLocals.table)
31 | assert.Nil(t, createInheritedMap())
32 | //
33 | wg.Done()
34 | }()
35 | wg.Wait()
36 | }
37 |
38 | func TestCreateInheritedMap_Nil(t *testing.T) {
39 | tls := NewInheritableThreadLocal[string]()
40 | tls.Set("")
41 | srcValue := tls.Get()
42 | assert.Equal(t, "", srcValue)
43 | assert.True(t, srcValue == "")
44 |
45 | mp := createInheritedMap()
46 | assert.NotNil(t, mp)
47 | getValue := entryValue[string](mp.get(tls.(*inheritableThreadLocal[string]).index))
48 | assert.Equal(t, "", getValue)
49 | assert.True(t, getValue == "")
50 |
51 | mp2 := createInheritedMap()
52 | assert.NotNil(t, mp2)
53 | assert.NotSame(t, mp, mp2)
54 | getValue2 := entryValue[string](mp2.get(tls.(*inheritableThreadLocal[string]).index))
55 | assert.Equal(t, "", getValue2)
56 | assert.True(t, getValue2 == "")
57 | }
58 |
59 | func TestCreateInheritedMap_Value(t *testing.T) {
60 | tls := NewInheritableThreadLocal[uint64]()
61 | value := rand.Uint64()
62 | tls.Set(value)
63 | srcValue := tls.Get()
64 | assert.NotSame(t, &value, &srcValue)
65 | assert.Equal(t, value, srcValue)
66 |
67 | mp := createInheritedMap()
68 | assert.NotNil(t, mp)
69 | getValue := entryValue[uint64](mp.get(tls.(*inheritableThreadLocal[uint64]).index))
70 | assert.NotSame(t, &value, &getValue)
71 | assert.Equal(t, value, getValue)
72 |
73 | mp2 := createInheritedMap()
74 | assert.NotNil(t, mp2)
75 | assert.NotSame(t, mp, mp2)
76 | getValue2 := entryValue[uint64](mp2.get(tls.(*inheritableThreadLocal[uint64]).index))
77 | assert.NotSame(t, &value, &getValue2)
78 | assert.Equal(t, value, getValue2)
79 | }
80 |
81 | func TestCreateInheritedMap_Struct(t *testing.T) {
82 | tls := NewInheritableThreadLocal[personCloneable]()
83 | value := personCloneable{Id: 1, Name: "Hello"}
84 | tls.Set(value)
85 | srcValue := tls.Get()
86 | assert.NotSame(t, &value, &srcValue)
87 | assert.Equal(t, value, srcValue)
88 |
89 | mp := createInheritedMap()
90 | assert.NotNil(t, mp)
91 | getValue := entryValue[personCloneable](mp.get(tls.(*inheritableThreadLocal[personCloneable]).index))
92 | assert.NotSame(t, &value, &getValue)
93 | assert.Equal(t, value, getValue)
94 |
95 | mp2 := createInheritedMap()
96 | assert.NotNil(t, mp2)
97 | assert.NotSame(t, mp, mp2)
98 | getValue2 := entryValue[personCloneable](mp2.get(tls.(*inheritableThreadLocal[personCloneable]).index))
99 | assert.NotSame(t, &value, &getValue2)
100 | assert.Equal(t, value, getValue2)
101 | }
102 |
103 | func TestCreateInheritedMap_Pointer(t *testing.T) {
104 | tls := NewInheritableThreadLocal[*person]()
105 | value := &person{Id: 1, Name: "Hello"}
106 | tls.Set(value)
107 | srcValue := tls.Get()
108 | assert.Same(t, value, srcValue)
109 | assert.Equal(t, *value, *srcValue)
110 |
111 | mp := createInheritedMap()
112 | assert.NotNil(t, mp)
113 | getValue := entryValue[*person](mp.get(tls.(*inheritableThreadLocal[*person]).index))
114 | assert.Same(t, value, getValue)
115 | assert.Equal(t, *value, *getValue)
116 |
117 | mp2 := createInheritedMap()
118 | assert.NotNil(t, mp2)
119 | assert.NotSame(t, mp, mp2)
120 | getValue2 := entryValue[*person](mp2.get(tls.(*inheritableThreadLocal[*person]).index))
121 | assert.Same(t, value, getValue2)
122 | assert.Equal(t, *value, *getValue2)
123 | }
124 |
125 | func TestCreateInheritedMap_Cloneable(t *testing.T) {
126 | tls := NewInheritableThreadLocal[*personCloneable]()
127 | value := &personCloneable{Id: 1, Name: "Hello"}
128 | tls.Set(value)
129 | srcValue := tls.Get()
130 | assert.Same(t, value, srcValue)
131 | assert.Equal(t, *value, *srcValue)
132 |
133 | mp := createInheritedMap()
134 | assert.NotNil(t, mp)
135 | getValue := entryValue[*personCloneable](mp.get(tls.(*inheritableThreadLocal[*personCloneable]).index))
136 | assert.NotSame(t, value, getValue)
137 | assert.Equal(t, *value, *getValue)
138 |
139 | mp2 := createInheritedMap()
140 | assert.NotNil(t, mp2)
141 | assert.NotSame(t, mp, mp2)
142 | getValue2 := entryValue[*personCloneable](mp2.get(tls.(*inheritableThreadLocal[*personCloneable]).index))
143 | assert.NotSame(t, value, getValue2)
144 | assert.Equal(t, *value, *getValue2)
145 | }
146 |
147 | func TestFill(t *testing.T) {
148 | a := make([]entry, 6)
149 | fill(a, 4, 5, unset)
150 | for i := 0; i < 6; i++ {
151 | if i == 4 {
152 | assert.True(t, a[i] == unset)
153 | } else {
154 | assert.Nil(t, a[i])
155 | assert.True(t, a[i] != unset)
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/thread_local_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "math"
5 | "sync"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestThreadLocal_Index(t *testing.T) {
12 | tls := NewThreadLocal[string]()
13 | assert.GreaterOrEqual(t, tls.(*threadLocal[string]).index, 0)
14 | tls2 := NewThreadLocalWithInitial[string](func() string {
15 | return "Hello"
16 | })
17 | assert.Greater(t, tls2.(*threadLocal[string]).index, tls.(*threadLocal[string]).index)
18 | }
19 |
20 | func TestThreadLocal_NextIndex(t *testing.T) {
21 | backup := threadLocalIndex
22 | defer func() {
23 | threadLocalIndex = backup
24 | }()
25 | //
26 | threadLocalIndex = math.MaxInt32
27 | assert.Panics(t, func() {
28 | nextThreadLocalIndex()
29 | })
30 | assert.Equal(t, math.MaxInt32, int(threadLocalIndex))
31 | }
32 |
33 | func TestThreadLocal_Common(t *testing.T) {
34 | tls := NewThreadLocal[int]()
35 | tls2 := NewThreadLocal[string]()
36 | tls.Remove()
37 | tls2.Remove()
38 | assert.Equal(t, 0, tls.Get())
39 | assert.Equal(t, "", tls2.Get())
40 | //
41 | tls.Set(1)
42 | tls2.Set("World")
43 | assert.Equal(t, 1, tls.Get())
44 | assert.Equal(t, "World", tls2.Get())
45 | //
46 | tls.Set(0)
47 | tls2.Set("")
48 | assert.Equal(t, 0, tls.Get())
49 | assert.Equal(t, "", tls2.Get())
50 | //
51 | tls.Set(2)
52 | tls2.Set("!")
53 | assert.Equal(t, 2, tls.Get())
54 | assert.Equal(t, "!", tls2.Get())
55 | //
56 | tls.Remove()
57 | tls2.Remove()
58 | assert.Equal(t, 0, tls.Get())
59 | assert.Equal(t, "", tls2.Get())
60 | //
61 | tls.Set(2)
62 | tls2.Set("!")
63 | assert.Equal(t, 2, tls.Get())
64 | assert.Equal(t, "!", tls2.Get())
65 | wg := &sync.WaitGroup{}
66 | wg.Add(100)
67 | for i := 0; i < 100; i++ {
68 | Go(func() {
69 | assert.Equal(t, 0, tls.Get())
70 | assert.Equal(t, "", tls2.Get())
71 | wg.Done()
72 | })
73 | }
74 | wg.Wait()
75 | assert.Equal(t, 2, tls.Get())
76 | assert.Equal(t, "!", tls2.Get())
77 | }
78 |
79 | func TestThreadLocal_Mixed(t *testing.T) {
80 | tls := NewThreadLocal[int]()
81 | tls2 := NewThreadLocalWithInitial[string](func() string {
82 | return "Hello"
83 | })
84 | assert.Equal(t, 0, tls.Get())
85 | assert.Equal(t, "Hello", tls2.Get())
86 | //
87 | tls.Set(1)
88 | tls2.Set("World")
89 | assert.Equal(t, 1, tls.Get())
90 | assert.Equal(t, "World", tls2.Get())
91 | //
92 | tls.Set(0)
93 | tls2.Set("")
94 | assert.Equal(t, 0, tls.Get())
95 | assert.Equal(t, "", tls2.Get())
96 | //
97 | tls.Set(2)
98 | tls2.Set("!")
99 | assert.Equal(t, 2, tls.Get())
100 | assert.Equal(t, "!", tls2.Get())
101 | //
102 | tls.Remove()
103 | tls2.Remove()
104 | assert.Equal(t, 0, tls.Get())
105 | assert.Equal(t, "Hello", tls2.Get())
106 | //
107 | tls.Set(2)
108 | tls2.Set("!")
109 | assert.Equal(t, 2, tls.Get())
110 | assert.Equal(t, "!", tls2.Get())
111 | wg := &sync.WaitGroup{}
112 | wg.Add(100)
113 | for i := 0; i < 100; i++ {
114 | Go(func() {
115 | assert.Equal(t, 0, tls.Get())
116 | assert.Equal(t, "Hello", tls2.Get())
117 | wg.Done()
118 | })
119 | }
120 | wg.Wait()
121 | assert.Equal(t, 2, tls.Get())
122 | assert.Equal(t, "!", tls2.Get())
123 | }
124 |
125 | func TestThreadLocal_WithInitial(t *testing.T) {
126 | src := &person{Id: 1, Name: "Tim"}
127 | tls := NewThreadLocalWithInitial[*person](nil)
128 | tls2 := NewThreadLocalWithInitial[*person](func() *person {
129 | var value *person
130 | return value
131 | })
132 | tls3 := NewThreadLocalWithInitial[*person](func() *person {
133 | return src
134 | })
135 | tls4 := NewThreadLocalWithInitial[person](func() person {
136 | return *src
137 | })
138 |
139 | for i := 0; i < 100; i++ {
140 | p := tls.Get()
141 | assert.Nil(t, p)
142 | //
143 | p2 := tls2.Get()
144 | assert.Nil(t, p2)
145 | //
146 | p3 := tls3.Get()
147 | assert.Same(t, src, p3)
148 |
149 | p4 := tls4.Get()
150 | assert.NotSame(t, src, &p4)
151 | assert.Equal(t, *src, p4)
152 |
153 | wg := &sync.WaitGroup{}
154 | wg.Add(1)
155 | Go(func() {
156 | assert.Same(t, src, tls3.Get())
157 | p5 := tls4.Get()
158 | assert.NotSame(t, src, &p5)
159 | assert.Equal(t, *src, p5)
160 | //
161 | wg.Done()
162 | })
163 | wg.Wait()
164 | }
165 |
166 | tls3.Set(nil)
167 | tls4.Set(person{})
168 | assert.Nil(t, tls3.Get())
169 | assert.Equal(t, person{}, tls4.Get())
170 |
171 | tls3.Remove()
172 | tls4.Remove()
173 | assert.Same(t, src, tls3.Get())
174 | p6 := tls4.Get()
175 | assert.NotSame(t, src, &p6)
176 | assert.Equal(t, *src, p6)
177 | }
178 |
179 | func TestThreadLocal_CrossCoroutine(t *testing.T) {
180 | tls := NewThreadLocal[string]()
181 | tls.Set("Hello")
182 | assert.Equal(t, "Hello", tls.Get())
183 | subWait := &sync.WaitGroup{}
184 | subWait.Add(2)
185 | finishWait := &sync.WaitGroup{}
186 | finishWait.Add(2)
187 | go func() {
188 | subWait.Wait()
189 | assert.Equal(t, "", tls.Get())
190 | finishWait.Done()
191 | }()
192 | Go(func() {
193 | subWait.Wait()
194 | assert.Equal(t, "", tls.Get())
195 | finishWait.Done()
196 | })
197 | tls.Remove() //remove in parent goroutine should not affect child goroutine
198 | subWait.Done() //allow sub goroutine run
199 | subWait.Done() //allow sub goroutine run
200 | finishWait.Wait() //wait sub goroutine done
201 | finishWait.Wait() //wait sub goroutine done
202 | }
203 |
204 | func TestThreadLocal_CreateBatch(t *testing.T) {
205 | const count = 128
206 | tlsList := make([]ThreadLocal[int], count)
207 | for i := 0; i < count; i++ {
208 | value := i
209 | tlsList[i] = NewThreadLocalWithInitial[int](func() int { return value })
210 | }
211 | for i := 0; i < count; i++ {
212 | assert.Equal(t, i, tlsList[i].Get())
213 | }
214 | }
215 |
216 | type person struct {
217 | Id int
218 | Name string
219 | }
220 |
--------------------------------------------------------------------------------
/thread_test.go:
--------------------------------------------------------------------------------
1 | package routine
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "runtime"
7 | "runtime/pprof"
8 | "sync"
9 | "testing"
10 | "time"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestCurrentThread(t *testing.T) {
16 | assert.NotNil(t, currentThread(true))
17 | assert.Same(t, currentThread(true), currentThread(true))
18 | }
19 |
20 | func TestPProf(t *testing.T) {
21 | const concurrency = 10
22 | const loopTimes = 10
23 | tls := NewThreadLocal[any]()
24 | tls.Set("你好")
25 | wg := &sync.WaitGroup{}
26 | wg.Add(concurrency)
27 | for i := 0; i < concurrency; i++ {
28 | tmp := i
29 | go func() {
30 | for j := 0; j < loopTimes; j++ {
31 | time.Sleep(100 * time.Millisecond)
32 | tls.Set(tmp)
33 | assert.Equal(t, tmp, tls.Get())
34 | pprof.Do(context.Background(), pprof.Labels("key", "value"), func(ctx context.Context) {
35 | if routinexEnabled {
36 | assert.Equal(t, tmp, tls.Get())
37 | } else {
38 | assert.Nil(t, currentThread(false))
39 | assert.Nil(t, tls.Get())
40 | }
41 | tls.Set("hi")
42 | //
43 | label, find := pprof.Label(ctx, "key")
44 | assert.True(t, find)
45 | assert.Equal(t, "value", label)
46 | //
47 | assert.Equal(t, "hi", tls.Get())
48 | //
49 | label2, find2 := pprof.Label(ctx, "key")
50 | assert.True(t, find2)
51 | assert.Equal(t, "value", label2)
52 | })
53 | if routinexEnabled {
54 | assert.Equal(t, "hi", tls.Get())
55 | } else {
56 | assert.Nil(t, tls.Get())
57 | }
58 | }
59 | wg.Done()
60 | }()
61 | }
62 | assert.Nil(t, pprof.StartCPUProfile(&bytes.Buffer{}))
63 | wg.Wait()
64 | pprof.StopCPUProfile()
65 | assert.Equal(t, "你好", tls.Get())
66 | }
67 |
68 | func TestThreadGC(t *testing.T) {
69 | const allocSize = 10_000_000
70 | tls := NewThreadLocal[[]byte]()
71 | tls2 := NewInheritableThreadLocal[[]byte]()
72 | allocWait := &sync.WaitGroup{}
73 | allocWait.Add(1)
74 | gatherWait := &sync.WaitGroup{}
75 | gatherWait.Add(1)
76 | gcWait := &sync.WaitGroup{}
77 | gcWait.Add(1)
78 | //=========Init
79 | heapInit, numInit := getMemStats()
80 | printMemStats("Init", heapInit, numInit)
81 | //
82 | task := GoWait(func(token CancelToken) {
83 | tls.Set(make([]byte, allocSize))
84 | tls2.Set(make([]byte, allocSize))
85 | go func() {
86 | gcWait.Wait()
87 | }()
88 | task2 := GoWaitResult(func(token CancelToken) int {
89 | return 1
90 | })
91 | assert.Equal(t, 1, task2.Get())
92 | allocWait.Done() //alloc ok, release main thread
93 | gatherWait.Wait() //wait gather heap info
94 | })
95 | //=========Alloc
96 | allocWait.Wait() //wait alloc done
97 | heapAlloc, numAlloc := getMemStats()
98 | printMemStats("Alloc", heapAlloc, numAlloc)
99 | assert.Greater(t, heapAlloc, heapInit+allocSize*2*0.9)
100 | assert.Greater(t, numAlloc, numInit)
101 | //=========GC
102 | gatherWait.Done() //gather ok, release sub thread
103 | task.Get() //wait sub thread finish
104 | time.Sleep(500 * time.Millisecond)
105 | heapGC, numGC := getMemStats()
106 | printMemStats("AfterGC", heapGC, numGC)
107 | gcWait.Done()
108 | //=========Summary
109 | heapRelease := heapAlloc - heapGC
110 | numRelease := numAlloc - numGC
111 | printMemStats("Summary", heapRelease, numRelease)
112 | assert.Greater(t, int(heapRelease), int(allocSize*2*0.9))
113 | assert.Equal(t, 1, numRelease)
114 | }
115 |
116 | func getMemStats() (uint64, int) {
117 | stats := runtime.MemStats{}
118 | runtime.GC()
119 | runtime.ReadMemStats(&stats)
120 | return stats.HeapAlloc, runtime.NumGoroutine()
121 | }
122 |
123 | func printMemStats(section string, heapAlloc uint64, numGoroutine int) {
124 | //fmt.Printf("%v\n", section)
125 | //fmt.Printf("HeapAlloc = %v\n", heapAlloc)
126 | //fmt.Printf("NumGoroutine = %v\n", numGoroutine)
127 | //fmt.Printf("===\n")
128 | }
129 |
--------------------------------------------------------------------------------