| // 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. | |
| switch err := syscall.CancelIoEx(fd.Sysfd, nil); err { | |
| case nil, syscall.ERROR_NOT_FOUND: | |
| case windows.ERROR_INVALID_HANDLE: | |
| // The handle may have been closed outside this FD. | |
| // Let destroy report the error from CloseHandle. | |
| default: | |
| 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 | |
| } |