diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c160c7a..e9440f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout scm - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v5 @@ -25,14 +25,14 @@ jobs: cache: false - name: Lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 CodeQL: needs: Lint runs-on: ubuntu-latest steps: - name: Checkout scm - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v5 @@ -191,9 +191,6 @@ jobs: runs-on: ubuntu-latest - os: windows runs-on: windows-latest - - os: windows - go: 1.18 - runs-on: windows-2019 - os: freebsd runs-on: ubuntu-latest - os: js @@ -201,7 +198,7 @@ jobs: steps: - name: Checkout scm - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v5 @@ -300,6 +297,14 @@ jobs: GOARCH: ${{ matrix.arch }} run: go test -v -coverprofile='coverage.txt' -covermode=atomic ./... + - name: 'Downgrade gcc for go1.18 on [windows] arch [amd64]' + if: ${{ matrix.os == 'windows' && contains(fromJson('["amd64"]'), matrix.arch) && matrix.go == '1.18' }} + run: | + Invoke-WebRequest -Uri 'https://jaist.dl.sourceforge.net/project/mingw-w64/Toolchains%20targetting%20Win64/Personal%20Builds/mingw-builds/8.1.0/threads-win32/sjlj/x86_64-8.1.0-release-win32-sjlj-rt_v6-rev0.7z?viasf=1' -OutFile gcc8.7z + Remove-Item 'C:\mingw64' -Recurse -Force + 7z x gcc8.7z -oC:\ -y + gcc --version + - name: 'Test on [windows] arch [amd64]' if: ${{ matrix.os == 'windows' && contains(fromJson('["amd64"]'), matrix.arch) }} env: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 2e5dca7..757646c 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout scm - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v5 @@ -25,14 +25,14 @@ jobs: cache: false - name: Lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v8 CodeQL: needs: Lint runs-on: ubuntu-latest steps: - name: Checkout scm - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v5 @@ -191,9 +191,6 @@ jobs: runs-on: ubuntu-latest - os: windows runs-on: windows-latest - - os: windows - go: 1.18 - runs-on: windows-2019 - os: freebsd runs-on: ubuntu-latest - os: js @@ -201,7 +198,7 @@ jobs: steps: - name: Checkout scm - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go uses: actions/setup-go@v5 @@ -304,6 +301,14 @@ jobs: GOARCH: ${{ matrix.arch }} run: go test -v -coverprofile='coverage.txt' -covermode=atomic -a -toolexec='routinex -v' ./... + - name: 'Downgrade gcc for go1.18 on [windows] arch [amd64]' + if: ${{ matrix.os == 'windows' && contains(fromJson('["amd64"]'), matrix.arch) && matrix.go == '1.18' }} + run: | + Invoke-WebRequest -Uri 'https://jaist.dl.sourceforge.net/project/mingw-w64/Toolchains%20targetting%20Win64/Personal%20Builds/mingw-builds/8.1.0/threads-win32/sjlj/x86_64-8.1.0-release-win32-sjlj-rt_v6-rev0.7z?viasf=1' -OutFile gcc8.7z + Remove-Item 'C:\mingw64' -Recurse -Force + 7z x gcc8.7z -oC:\ -y + gcc --version + - name: 'Test on [windows] arch [amd64]' if: ${{ matrix.os == 'windows' && contains(fromJson('["amd64"]'), matrix.arch) }} env: diff --git a/api_routine_test.go b/api_routine_test.go index 8244cbc..b9ebc1d 100644 --- a/api_routine_test.go +++ b/api_routine_test.go @@ -137,7 +137,7 @@ func TestWrapTask_HasContext(t *testing.T) { } func TestWrapTask_Complete_ThenFail(t *testing.T) { - tracker := NewFileTracker(&os.Stdout) + tracker := NewFileTracker(os.Stdout) tracker.Begin() defer tracker.End() // @@ -343,7 +343,7 @@ func TestWrapWaitTask_HasContext_Cancel(t *testing.T) { } func TestWrapWaitTask_Complete_ThenFail(t *testing.T) { - tracker := NewFileTracker(&os.Stdout) + tracker := NewFileTracker(os.Stdout) tracker.Begin() defer tracker.End() // @@ -553,7 +553,7 @@ func TestWrapWaitResultTask_HasContext_Cancel(t *testing.T) { } func TestWrapWaitResultTask_Complete_ThenFail(t *testing.T) { - tracker := NewFileTracker(&os.Stdout) + tracker := NewFileTracker(os.Stdout) tracker.Begin() defer tracker.End() // @@ -587,7 +587,7 @@ func TestWrapWaitResultTask_Complete_ThenFail(t *testing.T) { } func TestGo_Error(t *testing.T) { - tracker := NewFileTracker(&os.Stdout) + tracker := NewFileTracker(os.Stdout) tracker.Begin() defer tracker.End() // @@ -617,11 +617,7 @@ func TestGo_Error(t *testing.T) { // line = lines[2] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.inheritedTask.run()")) - if routinexEnabled { - assert.True(t, strings.HasSuffix(line, "routine.go:53")) - } else { - assert.True(t, strings.HasSuffix(line, "routine.go:45")) - } + assert.True(t, strings.HasSuffix(line, "routine.go:24")) // line = lines[3] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.(*futureTask[...]).Run()")) @@ -727,15 +723,11 @@ func TestGoWait_Error(t *testing.T) { // line = lines[1] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestGoWait_Error.")) - assert.True(t, strings.HasSuffix(line, "api_routine_test.go:710")) + assert.True(t, strings.HasSuffix(line, "api_routine_test.go:706")) // line = lines[2] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.inheritedWaitTask.run()")) - if routinexEnabled { - assert.True(t, strings.HasSuffix(line, "routine.go:85")) - } else { - assert.True(t, strings.HasSuffix(line, "routine.go:77")) - } + assert.True(t, strings.HasSuffix(line, "routine.go:44")) // line = lines[3] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.(*futureTask[...]).Run()")) @@ -833,15 +825,11 @@ func TestGoWaitResult_Error(t *testing.T) { // line = lines[1] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.TestGoWaitResult_Error.")) - assert.True(t, strings.HasSuffix(line, "api_routine_test.go:814")) + assert.True(t, strings.HasSuffix(line, "api_routine_test.go:806")) // line = lines[2] assert.True(t, strings.HasPrefix(line, " at github.com/timandy/routine.inheritedWaitResultTask[...].run()")) - if routinexEnabled { - assert.True(t, strings.HasSuffix(line, "routine.go:116")) - } else { - assert.True(t, strings.HasSuffix(line, "routine.go:109")) - } + assert.True(t, strings.HasSuffix(line, "routine.go:64")) // lineOffset := 0 if len(lines) == 7 { @@ -927,12 +915,12 @@ func TestGoWaitResult_Cross(t *testing.T) { //=== type FileTracker struct { - target **os.File - oldValue *os.File - tempValue *os.File + target *os.File + oldValue os.File + tempValue os.File } -func NewFileTracker(target **os.File) *FileTracker { +func NewFileTracker(target *os.File) *FileTracker { return &FileTracker{target: target, oldValue: *target} } @@ -941,8 +929,8 @@ func (f *FileTracker) Begin() { if err != nil { panic(err) } - *f.target = file - f.tempValue = file + *f.target = *file + f.tempValue = *file } func (f *FileTracker) End() { @@ -959,7 +947,7 @@ func (f *FileTracker) Value() string { if _, err := f.tempValue.Seek(0, io.SeekStart); err != nil { panic(err) } - buff, err := io.ReadAll(f.tempValue) + buff, err := io.ReadAll(&f.tempValue) if err != nil { panic(err) } @@ -968,10 +956,22 @@ func (f *FileTracker) Value() string { func TestFileTracker(t *testing.T) { origin := os.Stdout - tracker := NewFileTracker(&os.Stdout) + tracker := NewFileTracker(os.Stdout) + tracker.Begin() + _, _ = fmt.Fprintln(os.Stdout, "hello world") + assert.Equal(t, "hello world\n", tracker.Value()) + tracker.End() + assert.Equal(t, "/dev/stdout", origin.Name()) + assert.Same(t, origin, os.Stdout) +} + +func TestFileTrackerRef(t *testing.T) { + origin := os.Stdout + tracker := NewFileTracker(os.Stdout) tracker.Begin() - fmt.Println("hello world") + _, _ = fmt.Fprintln(origin, "hello world") assert.Equal(t, "hello world\n", tracker.Value()) tracker.End() + assert.Equal(t, "/dev/stdout", origin.Name()) assert.Same(t, origin, os.Stdout) } diff --git a/g/reflect.go b/g/reflect.go index 264f6e5..e02bf60 100644 --- a/g/reflect.go +++ b/g/reflect.go @@ -75,7 +75,7 @@ func typeByString(str string) reflect.Type { h := int(uint(i+j) >> 1) // avoid overflow when computing h // i ≤ h < j face.data = resolveTypeOff(section, offs[h]) - if !(typ.String() >= s) { + if !(typ.String() >= s) { //nolint:staticcheck i = h + 1 // preserves f(i-1) == false } else { j = h // preserves f(j) == true diff --git a/routine.go b/routine.go index c43c5eb..39e58f8 100644 --- a/routine.go +++ b/routine.go @@ -2,26 +2,7 @@ package routine import "fmt" -type inherited struct { -} - -//go:norace -func (inherited) reset() { - t := currentThread(false) - if t != nil { - t.threadLocals = nil - t.inheritableThreadLocals = nil - } -} - -//go:norace -func (inherited) restore(t *thread, threadLocalsBackup, inheritableThreadLocalsBackup *threadLocalMap) { - t.threadLocals = threadLocalsBackup - t.inheritableThreadLocals = inheritableThreadLocalsBackup -} - type inheritedTask struct { - inherited context *threadLocalMap function Runnable } @@ -38,25 +19,13 @@ func (it inheritedTask) run(task FutureTask[any]) any { } }() // restore - t := currentThread(it.context != nil) - if t == nil { - //copied is nil - defer it.reset() - it.function() - return nil - } else { - threadLocalsBackup := t.threadLocals - inheritableThreadLocalsBackup := t.inheritableThreadLocals - defer it.restore(t, threadLocalsBackup, inheritableThreadLocalsBackup) - t.threadLocals = nil - t.inheritableThreadLocals = it.context - it.function() - return nil - } + defer restoreInheritedMap(it.context)() + // exec + it.function() + return nil } type inheritedWaitTask struct { - inherited context *threadLocalMap function CancelRunnable } @@ -70,25 +39,13 @@ func (iwt inheritedWaitTask) run(task FutureTask[any]) any { } }() // restore - t := currentThread(iwt.context != nil) - if t == nil { - //copied is nil - defer iwt.reset() - iwt.function(task) - return nil - } else { - threadLocalsBackup := t.threadLocals - inheritableThreadLocalsBackup := t.inheritableThreadLocals - defer iwt.restore(t, threadLocalsBackup, inheritableThreadLocalsBackup) - t.threadLocals = nil - t.inheritableThreadLocals = iwt.context - iwt.function(task) - return nil - } + defer restoreInheritedMap(iwt.context)() + // exec + iwt.function(task) + return nil } type inheritedWaitResultTask[TResult any] struct { - inherited context *threadLocalMap function CancelCallable[TResult] } @@ -102,17 +59,7 @@ func (iwrt inheritedWaitResultTask[TResult]) run(task FutureTask[TResult]) TResu } }() // restore - t := currentThread(iwrt.context != nil) - if t == nil { - //copied is nil - defer iwrt.reset() - return iwrt.function(task) - } else { - threadLocalsBackup := t.threadLocals - inheritableThreadLocalsBackup := t.inheritableThreadLocals - defer iwrt.restore(t, threadLocalsBackup, inheritableThreadLocalsBackup) - t.threadLocals = nil - t.inheritableThreadLocals = iwrt.context - return iwrt.function(task) - } + defer restoreInheritedMap(iwrt.context)() + // exec + return iwrt.function(task) } diff --git a/thread_local_map.go b/thread_local_map.go index e61dbf0..0d4078c 100644 --- a/thread_local_map.go +++ b/thread_local_map.go @@ -1,5 +1,7 @@ package routine +import _ "unsafe" + var unset entry = &object{} type object struct { @@ -53,6 +55,7 @@ func (mp *threadLocalMap) expandAndSet(index int, value entry) { } //go:norace +//go:linkname createInheritedMap routine.createInheritedMap func createInheritedMap() *threadLocalMap { parent := currentThread(false) if parent == nil { @@ -76,6 +79,38 @@ func createInheritedMap() *threadLocalMap { return &threadLocalMap{table: table} } +//go:norace +//go:linkname restoreInheritedMap routine.restoreInheritedMap +func restoreInheritedMap(mp *threadLocalMap) func() { + t := currentThread(mp != nil) + if t == nil { + // mp and t are nil + return clearThread + } + threadLocalsBackup := t.threadLocals + inheritableThreadLocalsBackup := t.inheritableThreadLocals + t.threadLocals = nil + t.inheritableThreadLocals = mp + return func() { + resetThread(t, threadLocalsBackup, inheritableThreadLocalsBackup) + } +} + +//go:norace +func clearThread() { + t := currentThread(false) + if t != nil { + t.threadLocals = nil + t.inheritableThreadLocals = nil + } +} + +//go:norace +func resetThread(t *thread, threadLocals, inheritableThreadLocals *threadLocalMap) { + t.threadLocals = threadLocals + t.inheritableThreadLocals = inheritableThreadLocals +} + func fill[T any](a []T, fromIndex int, toIndex int, val T) { for i := fromIndex; i < toIndex; i++ { a[i] = val diff --git a/thread_local_map_test.go b/thread_local_map_test.go index 7dc7197..775f29c 100644 --- a/thread_local_map_test.go +++ b/thread_local_map_test.go @@ -4,10 +4,17 @@ import ( "math/rand" "sync" "testing" + "unsafe" "github.com/stretchr/testify/assert" ) +//go:linkname createInheritedMapExport routine.createInheritedMap +func createInheritedMapExport() unsafe.Pointer + +//go:linkname restoreInheritedMapExport routine.restoreInheritedMap +func restoreInheritedMapExport(mp unsafe.Pointer) func() + func TestObject(t *testing.T) { var value entry = &object{} assert.NotSame(t, unset, value) @@ -144,6 +151,154 @@ func TestCreateInheritedMap_Cloneable(t *testing.T) { assert.Equal(t, *value, *getValue2) } +func TestRestoreInheritedMap(t *testing.T) { + tls := NewInheritableThreadLocal[*personCloneable]() + value := &personCloneable{Id: 1, Name: "Hello"} + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + mp := createInheritedMap() + assert.Nil(t, mp) + // + go func() { + defer func() { + if routinexEnabled { + assert.NotNil(t, currentThread(false)) + } else { + assert.Nil(t, currentThread(false)) + } + assert.Nil(t, tls.Get()) + wg.Done() + }() + defer restoreInheritedMap(mp)() + }() + wg.Done() + }() + wg.Wait() + // + wg2 := sync.WaitGroup{} + wg2.Add(2) + go func() { + mp := createInheritedMap() + assert.Nil(t, mp) + // + go func() { + defer func() { + assert.NotNil(t, currentThread(false)) + assert.Nil(t, tls.Get()) + wg2.Done() + }() + defer restoreInheritedMap(mp)() + tls.Set(value) + assert.Same(t, value, tls.Get()) + }() + wg2.Done() + }() + wg2.Wait() + // + value2 := &personCloneable{Id: 2, Name: "World"} + wg3 := sync.WaitGroup{} + wg3.Add(2) + go func() { + tls.Set(value) + assert.Same(t, value, tls.Get()) + mp := createInheritedMap() + assert.NotNil(t, mp) + // + go func() { + defer func() { + assert.NotNil(t, currentThread(false)) + assert.Same(t, value2, tls.Get()) + wg3.Done() + }() + tls.Set(value2) + assert.Same(t, value2, tls.Get()) + defer restoreInheritedMap(mp)() + result := tls.Get() + assert.NotNil(t, result) + assert.NotSame(t, value, result) + assert.NotSame(t, value2, result) + assert.Equal(t, *value, *result) + }() + wg3.Done() + }() + wg3.Wait() +} + +func TestRestoreInheritedMap_Export(t *testing.T) { + tls := NewInheritableThreadLocal[*personCloneable]() + value := &personCloneable{Id: 1, Name: "Hello"} + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + mp := createInheritedMapExport() + assert.Nil(t, mp) + // + go func() { + defer func() { + if routinexEnabled { + assert.NotNil(t, currentThread(false)) + } else { + assert.Nil(t, currentThread(false)) + } + assert.Nil(t, tls.Get()) + wg.Done() + }() + defer restoreInheritedMapExport(mp)() + }() + wg.Done() + }() + wg.Wait() + // + wg2 := sync.WaitGroup{} + wg2.Add(2) + go func() { + mp := createInheritedMapExport() + assert.Nil(t, mp) + // + go func() { + defer func() { + assert.NotNil(t, currentThread(false)) + assert.Nil(t, tls.Get()) + wg2.Done() + }() + defer restoreInheritedMapExport(mp)() + tls.Set(value) + assert.Same(t, value, tls.Get()) + }() + wg2.Done() + }() + wg2.Wait() + // + value2 := &personCloneable{Id: 2, Name: "World"} + wg3 := sync.WaitGroup{} + wg3.Add(2) + go func() { + tls.Set(value) + assert.Same(t, value, tls.Get()) + mp := createInheritedMapExport() + assert.NotNil(t, mp) + // + go func() { + defer func() { + assert.NotNil(t, currentThread(false)) + assert.Same(t, value2, tls.Get()) + wg3.Done() + }() + tls.Set(value2) + assert.Same(t, value2, tls.Get()) + defer restoreInheritedMapExport(mp)() + result := tls.Get() + assert.NotNil(t, result) + assert.NotSame(t, value, result) + assert.NotSame(t, value2, result) + assert.Equal(t, *value, *result) + }() + wg3.Done() + }() + wg3.Wait() +} + func TestFill(t *testing.T) { a := make([]entry, 6) fill(a, 4, 5, unset)