Source file src/internal/poll/fd_sync_windows.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package poll
     6  
     7  import (
     8  	"internal/synctest"
     9  	"internal/syscall/windows"
    10  	"runtime"
    11  	"sync"
    12  	"sync/atomic"
    13  	"syscall"
    14  )
    15  
    16  // ioCancelState coordinates cancellation with synchronous I/O workers.
    17  // Its zero value is ready for use. The FD's directional locks permit at most
    18  // one read worker and one write worker at a time.
    19  type ioCancelState struct {
    20  	canceling  atomic.Bool
    21  	cancelDone sync.WaitGroup
    22  	// A worker clears its slot once its I/O returns, before sending the result.
    23  	workers [2]atomic.Pointer[syncIOWorkerState]
    24  }
    25  
    26  // begin publishes the worker before its closing check and returns its slot.
    27  // The worker must call end after I/O returns and before sending the result.
    28  func (s *ioCancelState) begin(worker *syncIOWorkerState) int {
    29  	for i := range s.workers {
    30  		if s.workers[i].CompareAndSwap(nil, worker) {
    31  			return i
    32  		}
    33  	}
    34  	// A caller bypassed a directional lock, or a worker did not clear its slot.
    35  	panic("too many synchronous I/O workers")
    36  }
    37  
    38  func (s *ioCancelState) end(slot int) {
    39  	// Clear the slot before waiting, so cancellation retries can finish.
    40  	s.workers[slot].Store(nil)
    41  	if s.canceling.Load() {
    42  		s.cancelDone.Wait()
    43  	}
    44  }
    45  
    46  // cancelIO requests cancellation of outstanding I/O. Close waits for completion
    47  // by waiting for the I/O callers to release their FD references.
    48  // The caller must have marked the FD closed and hold a reference to it.
    49  // Only one goroutine may call cancelIO.
    50  func (fd *FD) cancelIO() {
    51  	s := &fd.ioCancel
    52  	// Workers clear their slots before waiting in end, so retries can finish
    53  	// without waiting for result delivery. Add before publishing canceling, so
    54  	// a worker seen by the scan cannot return from end before cancellation ends.
    55  	s.cancelDone.Add(1)
    56  	s.canceling.Store(true)
    57  	defer s.cancelDone.Done()
    58  
    59  	// Workers published after a scan see closing and skip I/O. We only
    60  	// need to remember successful cancellation for each slot.
    61  	var canceled [2]bool
    62  	first := true
    63  	for {
    64  		var retry bool
    65  		for i := range s.workers {
    66  			worker := s.workers[i].Load()
    67  			if worker == nil || canceled[i] {
    68  				continue
    69  			}
    70  			switch err := windows.CancelSynchronousIo(worker.thread); err {
    71  			case nil:
    72  				// The thread has only this I/O request to cancel. end keeps
    73  				// it alive and unavailable for reuse until cancelIO returns.
    74  				canceled[i] = true
    75  			case syscall.ERROR_NOT_FOUND:
    76  				// I/O may have returned, or may not have been submitted yet.
    77  				retry = true
    78  			default:
    79  				panic(err)
    80  			}
    81  		}
    82  		if first {
    83  			first = false
    84  			// Also cancel overlapped and externally issued I/O, including I/O
    85  			// blocking lazy initialization, even if no worker is published.
    86  			// Target workers first so their cancellation is known separately.
    87  			if err := syscall.CancelIoEx(fd.Sysfd, nil); err != nil && err != syscall.ERROR_NOT_FOUND {
    88  				panic(err)
    89  			}
    90  		}
    91  		if !retry {
    92  			return
    93  		}
    94  		// ERROR_NOT_FOUND may mean a worker passed its closing check but
    95  		// has not submitted I/O yet. It can still submit a blocking request,
    96  		// so we must retry. Alternatively, I/O has completed but the worker
    97  		// has not cleared its slot yet. Yield to let the worker make progress.
    98  		runtime.Gosched()
    99  	}
   100  }
   101  
   102  // runtime_startThread starts a goroutine locked to a fresh OS thread. The
   103  // thread exits when the function returns; it must not call UnlockOSThread.
   104  func runtime_startThread(func())
   105  
   106  // runtime_threadHandle returns the runtime-owned handle of the current worker
   107  // thread. It may only be called from a runtime_startThread worker. The handle
   108  // is valid until the worker returns and must not be closed by the caller.
   109  func runtime_threadHandle() syscall.Handle
   110  
   111  type syncIORequest struct {
   112  	fd     *FD
   113  	submit func(syscall.Handle, []byte, *uint32, *syscall.Overlapped) error
   114  	buf    []byte
   115  }
   116  
   117  type syncIOResult struct {
   118  	qty uint32
   119  	err error
   120  }
   121  
   122  type syncIOWorker struct {
   123  	state *syncIOWorkerState
   124  }
   125  
   126  // The running worker references only its state, not the cached owner. This
   127  // lets the owner's cleanup stop the thread when sync.Pool discards it.
   128  type syncIOWorkerState struct {
   129  	requests chan syncIORequest
   130  	results  chan syncIOResult
   131  	thread   syscall.Handle // borrowed from runtime; set before begin
   132  }
   133  
   134  // Reuse idle workers through sync.Pool's per-P caches. Active workers are not
   135  // limited: they may all be blocked in Read while another must run Write.
   136  // Discarded owners shut down their idle workers through a cleanup.
   137  var idleSyncIOWorkers sync.Pool
   138  
   139  func acquireSyncIOWorker() *syncIOWorker {
   140  	// A worker created in a synctest bubble must exit within that bubble;
   141  	// neither its goroutine nor its channels may enter the shared cache.
   142  	inBubble := synctest.IsInBubble()
   143  	if !inBubble {
   144  		if worker := idleSyncIOWorkers.Get(); worker != nil {
   145  			return worker.(*syncIOWorker)
   146  		}
   147  	}
   148  	state := &syncIOWorkerState{
   149  		requests: make(chan syncIORequest),
   150  		results:  make(chan syncIOResult),
   151  	}
   152  	runtime_startThread(state.run)
   153  	worker := &syncIOWorker{state: state}
   154  	if !inBubble {
   155  		runtime.AddCleanup(worker, (*syncIOWorkerState).stop, state)
   156  	}
   157  	return worker
   158  }
   159  
   160  func (worker *syncIOWorker) release() {
   161  	if synctest.IsInBubble() {
   162  		worker.state.stop()
   163  	} else {
   164  		idleSyncIOWorkers.Put(worker)
   165  	}
   166  	runtime.KeepAlive(worker)
   167  }
   168  
   169  func (worker *syncIOWorkerState) stop() {
   170  	close(worker.requests)
   171  }
   172  
   173  func (worker *syncIOWorkerState) run() {
   174  	worker.thread = runtime_threadHandle()
   175  
   176  	// Reuse the result because passing its address to submit makes it escape.
   177  	var result syncIOResult
   178  	for request := range worker.requests {
   179  		slot := request.fd.ioCancel.begin(worker)
   180  		if request.fd.closing() {
   181  			result.err = errClosing(request.fd.isFile)
   182  		} else {
   183  			result.err = request.submit(request.fd.Sysfd, request.buf, &result.qty, nil)
   184  		}
   185  		// Wait for cancellation before the result lets the caller reuse or
   186  		// stop this worker.
   187  		request.fd.ioCancel.end(slot)
   188  		// Do not retain the caller's file or buffer while this worker is idle.
   189  		request = syncIORequest{}
   190  		worker.results <- result
   191  		result = syncIOResult{}
   192  	}
   193  }
   194  
   195  // execSyncIO queues synchronous pipe I/O to a dedicated worker. The caller
   196  // retains the I/O lock, FD reference, and buffer until the worker returns,
   197  // but does not need to reserve its own OS thread.
   198  func (fd *FD) execSyncIO(submit func(syscall.Handle, []byte, *uint32, *syscall.Overlapped) error, buf []byte) (uint32, error) {
   199  	worker := acquireSyncIOWorker()
   200  	worker.state.requests <- syncIORequest{fd, submit, buf}
   201  	result := <-worker.state.results
   202  	worker.release()
   203  	return result.qty, result.err
   204  }
   205  

View as plain text