blob: ee0f0e68acf78fb2d5e59c210558871b37a4cb84 [file]
// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package poll
import (
"internal/synctest"
"internal/syscall/windows"
"runtime"
"sync"
"sync/atomic"
"syscall"
)
// ioCancelState coordinates cancellation with synchronous I/O workers.
// Its zero value is ready for use. The FD's directional locks permit at most
// one read worker and one write worker at a time.
type ioCancelState struct {
canceling atomic.Bool
cancelDone sync.WaitGroup
// A worker clears its slot once its I/O returns, before sending the result.
workers [2]atomic.Pointer[syncIOWorkerState]
}
// begin publishes the worker before its closing check and returns its slot.
// The worker must call end after I/O returns and before sending the result.
func (s *ioCancelState) begin(worker *syncIOWorkerState) int {
for i := range s.workers {
if s.workers[i].CompareAndSwap(nil, worker) {
return i
}
}
// A caller bypassed a directional lock, or a worker did not clear its slot.
panic("too many synchronous I/O workers")
}
func (s *ioCancelState) end(slot int) {
// Clear the slot before waiting, so cancellation retries can finish.
s.workers[slot].Store(nil)
if s.canceling.Load() {
s.cancelDone.Wait()
}
}
// cancelIO requests cancellation of outstanding I/O. Close waits for completion
// by waiting for the I/O callers to release their FD references.
// The caller must have marked the FD closed and hold a reference to it.
// Only one goroutine may call cancelIO.
func (fd *FD) cancelIO() {
s := &fd.ioCancel
// Workers clear their slots before waiting in end, so retries can finish
// without waiting for result delivery. Add before publishing canceling, so
// a worker seen by the scan cannot return from end before cancellation ends.
s.cancelDone.Add(1)
s.canceling.Store(true)
defer s.cancelDone.Done()
// Workers published after a scan see closing and skip I/O. We only
// need to remember successful cancellation for each slot.
var canceled [2]bool
first := true
for {
var retry bool
for i := range s.workers {
worker := s.workers[i].Load()
if worker == nil || canceled[i] {
continue
}
switch err := windows.CancelSynchronousIo(worker.thread); err {
case nil:
// The thread has only this I/O request to cancel. end keeps
// it alive and unavailable for reuse until cancelIO returns.
canceled[i] = true
case syscall.ERROR_NOT_FOUND:
// I/O may have returned, or may not have been submitted yet.
retry = true
default:
panic(err)
}
}
if first {
first = false
// Also cancel overlapped and externally issued I/O, including I/O
// blocking lazy initialization, even if no worker is published.
// Target workers first so their cancellation is known separately.
if err := syscall.CancelIoEx(fd.Sysfd, nil); err != nil && err != syscall.ERROR_NOT_FOUND {
panic(err)
}
}
if !retry {
return
}
// ERROR_NOT_FOUND may mean a worker passed its closing check but
// has not submitted I/O yet. It can still submit a blocking request,
// so we must retry. Alternatively, I/O has completed but the worker
// has not cleared its slot yet. Yield to let the worker make progress.
runtime.Gosched()
}
}
// runtime_startThread starts a goroutine locked to a fresh OS thread. The
// thread exits when the function returns; it must not call UnlockOSThread.
func runtime_startThread(func())
// runtime_threadHandle returns the runtime-owned handle of the current worker
// thread. It may only be called from a runtime_startThread worker. The handle
// is valid until the worker returns and must not be closed by the caller.
func runtime_threadHandle() syscall.Handle
type syncIORequest struct {
fd *FD
submit func(syscall.Handle, []byte, *uint32, *syscall.Overlapped) error
buf []byte
}
type syncIOResult struct {
qty uint32
err error
}
type syncIOWorker struct {
state *syncIOWorkerState
}
// The running worker references only its state, not the cached owner. This
// lets the owner's cleanup stop the thread when sync.Pool discards it.
type syncIOWorkerState struct {
requests chan syncIORequest
results chan syncIOResult
thread syscall.Handle // borrowed from runtime; set before begin
}
// Reuse idle workers through sync.Pool's per-P caches. Active workers are not
// limited: they may all be blocked in Read while another must run Write.
// Discarded owners shut down their idle workers through a cleanup.
var idleSyncIOWorkers sync.Pool
func acquireSyncIOWorker() *syncIOWorker {
// A worker created in a synctest bubble must exit within that bubble;
// neither its goroutine nor its channels may enter the shared cache.
inBubble := synctest.IsInBubble()
if !inBubble {
if worker := idleSyncIOWorkers.Get(); worker != nil {
return worker.(*syncIOWorker)
}
}
state := &syncIOWorkerState{
requests: make(chan syncIORequest),
results: make(chan syncIOResult),
}
runtime_startThread(state.run)
worker := &syncIOWorker{state: state}
if !inBubble {
runtime.AddCleanup(worker, (*syncIOWorkerState).stop, state)
}
return worker
}
func (worker *syncIOWorker) release() {
if synctest.IsInBubble() {
worker.state.stop()
} else {
idleSyncIOWorkers.Put(worker)
}
runtime.KeepAlive(worker)
}
func (worker *syncIOWorkerState) stop() {
close(worker.requests)
}
func (worker *syncIOWorkerState) run() {
worker.thread = runtime_threadHandle()
// Reuse the result because passing its address to submit makes it escape.
var result syncIOResult
for request := range worker.requests {
slot := request.fd.ioCancel.begin(worker)
if request.fd.closing() {
result.err = errClosing(request.fd.isFile)
} else {
result.err = request.submit(request.fd.Sysfd, request.buf, &result.qty, nil)
}
// Wait for cancellation before the result lets the caller reuse or
// stop this worker.
request.fd.ioCancel.end(slot)
// Do not retain the caller's file or buffer while this worker is idle.
request = syncIORequest{}
worker.results <- result
result = syncIOResult{}
}
}
// execSyncIO queues synchronous pipe I/O to a dedicated worker. The caller
// retains the I/O lock, FD reference, and buffer until the worker returns,
// but does not need to reserve its own OS thread.
func (fd *FD) execSyncIO(submit func(syscall.Handle, []byte, *uint32, *syscall.Overlapped) error, buf []byte) (uint32, error) {
worker := acquireSyncIOWorker()
worker.state.requests <- syncIORequest{fd, submit, buf}
result := <-worker.state.results
worker.release()
return result.qty, result.err
}