src/internal/poll/fd_mutex.go | 4 +++- src/os/os_test.go | 28 ++++++++++++++++++++++++++++ diff --git a/src/internal/poll/fd_mutex.go b/src/internal/poll/fd_mutex.go index c1c92976870658efae5829e375729ee7e16b1596..f71c7739a1aa6e1a1eb1962264a9e00644b0a90a 100644 --- a/src/internal/poll/fd_mutex.go +++ b/src/internal/poll/fd_mutex.go @@ -265,7 +265,9 @@ // It also closes fd when the state of fd is set to closed and there // is no remaining reference. func (fd *FD) readWriteUnlock() { fd.fdmu.rwunlock(true) - fd.fdmu.rwunlock(false) + if fd.fdmu.rwunlock(false) { + fd.destroy() + } } // closing returns true if fd is closing. diff --git a/src/os/os_test.go b/src/os/os_test.go index 29f2e6d3b2258c8123d6fc25ced496b8cad022bf..47f4163220e4bbe00d2ef1565de7f83c107910d8 100644 --- a/src/os/os_test.go +++ b/src/os/os_test.go @@ -3468,6 +3468,34 @@ t.Errorf("expected 0 allocs for File.WriteString, got %v", allocs) } } +// Test that it's OK to have parallel I/O and Close on a file. +func TestFileIOCloseRace(t *testing.T) { + t.Parallel() + file, err := Create(filepath.Join(t.TempDir(), "test.txt")) + if err != nil { + t.Fatal(err) + } + var wg sync.WaitGroup + wg.Go(func() { + var tmp [100]byte + if _, err := file.Write(tmp[:]); err != nil && !errors.Is(err, ErrClosed) { + t.Error(err) + } + }) + wg.Go(func() { + var tmp [100]byte + if _, err := file.Read(tmp[:]); err != nil && err != io.EOF && !errors.Is(err, ErrClosed) { + t.Error(err) + } + }) + wg.Go(func() { + if err := file.Close(); err != nil { + t.Error(err) + } + }) + wg.Wait() +} + // Test that it's OK to have parallel I/O and Close on a pipe. func TestPipeIOCloseRace(t *testing.T) { // Skip on wasm, which doesn't have pipes.