├── .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] <title>" 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 | [![Build Status](https://github.com/timandy/routine/actions/workflows/build.yml/badge.svg)](https://github.com/timandy/routine/actions) 4 | [![Codecov](https://codecov.io/gh/timandy/routine/branch/main/graph/badge.svg)](https://app.codecov.io/gh/timandy/routine) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/timandy/routine)](https://goreportcard.com/report/github.com/timandy/routine) 6 | [![Documentation](https://pkg.go.dev/badge/github.com/timandy/routine.svg)](https://pkg.go.dev/github.com/timandy/routine) 7 | [![Release](https://img.shields.io/github/release/timandy/routine.svg)](https://github.com/timandy/routine/releases) 8 | [![License](https://img.shields.io/github/license/timandy/routine.svg)](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 | [![Build Status](https://github.com/timandy/routine/actions/workflows/build.yml/badge.svg)](https://github.com/timandy/routine/actions) 4 | [![Codecov](https://codecov.io/gh/timandy/routine/branch/main/graph/badge.svg)](https://app.codecov.io/gh/timandy/routine) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/timandy/routine)](https://goreportcard.com/report/github.com/timandy/routine) 6 | [![Documentation](https://pkg.go.dev/badge/github.com/timandy/routine.svg)](https://pkg.go.dev/github.com/timandy/routine) 7 | [![Release](https://img.shields.io/github/release/timandy/routine.svg)](https://github.com/timandy/routine/releases) 8 | [![License](https://img.shields.io/github/license/timandy/routine.svg)](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 | --------------------------------------------------------------------------------