| // 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 |
| } |