怎么使用go牛逼的协程调度写一个自己的同步库?就像标准库里的net

golang可以让TCPConn.Write阻塞协程,但是不阻塞系统线程,并且直接把这个”同步“能力放在了标准库net中。

因为协程调度是go中的一个重要语言级特性,go需要能够把这个特性开放出来给开发者,来帮助实现其他需要详细控制协程调度的一些”同步库“,就像标准库中的net。

所以我一直有个疑问:

开发者怎么使用go中高效的协程调度来实现自己的同步库?

先说结论:(以1.10版本为准)

  • runtime包里只提供了runtime.Gosched()来yield协程,没有精细控制的接口
  • go中的文档有描述怎么hack:

go/src/runtime/HACKING.md

Synchronization
===============

The runtime has multiple synchronization mechanisms. They differ in
semantics and, in particular, in whether they interact with the
goroutine scheduler or the OS scheduler.
...

To interact directly with the goroutine scheduler, use `gopark` and
`goready`. `gopark` parks the current goroutine—putting it in the
"waiting" state and removing it from the scheduler's run queue—and
schedules another goroutine on the current M/P. `goready` puts a
parked goroutine back in the "runnable" state and adds it to the run
queue.

看样子是需要定制runtime。


下面是分析过程,从net.TCPConn.Write()开始的调用栈:

net.go:183


// Write implements the Conn Write method.
  func (c *conn) Write(b []byte) (int, error) {
  	if !c.ok() {
  		return 0, syscall.EINVAL
  	}
  	n, err := c.fd.Write(b)
  	if err != nil {
  		err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
  	}
  	return n, err
  }

internal/poll/fd_unix.go:219

func (fd *netFD) Write(p []byte) (nn int, err error) {
  	nn, err = fd.pfd.Write(p)
  	runtime.KeepAlive(fd)
  	return nn, wrapSyscallError("write", err)
  }

internal/poll/fd_unix.go:241

// Write implements io.Writer.
  func (fd *FD) Write(p []byte) (int, error) {
  	if err := fd.writeLock(); err != nil {
  		return 0, err
  	}
  	defer fd.writeUnlock()
  	if err := fd.pd.prepareWrite(fd.isFile); err != nil {
  		return 0, err
  	}
  	var nn int
  	for {
  		max := len(p)
  		if fd.IsStream && max-nn > maxRW {
  			max = nn + maxRW
  		}
  		n, err := syscall.Write(fd.Sysfd, p[nn:max])
  		if n > 0 {
  			nn += n
  		}
  		if nn == len(p) {
  			return nn, err
  		}
  		if err == syscall.EAGAIN && fd.pd.pollable() {
  			if err = fd.pd.waitWrite(fd.isFile); err == nil {
  				continue
  			}
  		}
  		if err != nil {
  			return nn, err
  		}
  		if n == 0 {
  			return nn, io.ErrUnexpectedEOF
  		}
  	}
  }

internal/poll/fd_poll_runtime.go:93

func (pd *pollDesc) waitWrite(isFile bool) error {
  	return pd.wait('w', isFile)
  }

internal/poll/fd_poll_runtime.go:81

func (pd *pollDesc) wait(mode int, isFile bool) error {
  	if pd.runtimeCtx == 0 {
  		return errors.New("waiting for unsupported file type")
  	}
  	res := runtime_pollWait(pd.runtimeCtx, mode)
  	return convertErr(res, isFile)
  }

internal/poll/fd_poll_runtime.go:23

func runtime_pollWait(ctx uintptr, mode int) int

runtime/netpoll.go:163

//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
	err := netpollcheckerr(pd, int32(mode))
	if err != 0 {
		return err
	}
	// As for now only Solaris uses level-triggered IO.
	if GOOS == "solaris" {
		netpollarm(pd, mode)
	}
	for !netpollblock(pd, int32(mode), false) {
		err = netpollcheckerr(pd, int32(mode))
		if err != 0 {
			return err
		}
		// Can happen if timeout has fired and unblocked us,
		// but before we had a chance to run, timeout has been reset.
		// Pretend it has not happened and retry.
	}
	return 0
}

runtime/netpoll.go:339

// returns true if IO is ready, or false if timedout or closed
// waitio - wait only for completed IO, ignore errors
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
	gpp := &pd.rg
	if mode == 'w' {
		gpp = &pd.wg
	}

	// set the gpp semaphore to WAIT
	for {
		old := *gpp
		if old == pdReady {
			*gpp = 0
			return true
		}
		if old != 0 {
			throw("runtime: double wait")
		}
		if atomic.Casuintptr(gpp, 0, pdWait) {
			break
		}
	}

	// need to recheck error states after setting gpp to WAIT
	// this is necessary because runtime_pollUnblock/runtime_pollSetDeadline/deadlineimpl
	// do the opposite: store to closing/rd/wd, membarrier, load of rg/wg
	if waitio || netpollcheckerr(pd, mode) == 0 {
		gopark(netpollblockcommit, unsafe.Pointer(gpp), "IO wait", traceEvGoBlockNet, 5)
	}
	// be careful to not lose concurrent READY notification
	old := atomic.Xchguintptr(gpp, 0)
	if old > pdWait {
		throw("runtime: corrupted polldesc")
	}
	return old == pdReady
}

runtime/proc.go:269

// Puts the current goroutine into a waiting state and calls unlockf.
// If unlockf returns false, the goroutine is resumed.
// unlockf must not access this G's stack, as it may be moved between
// the call to gopark and the call to unlockf.
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason string, traceEv byte, traceskip int) {
	mp := acquirem()
	gp := mp.curg
	status := readgstatus(gp)
	if status != _Grunning && status != _Gscanrunning {
		throw("gopark: bad g status")
	}
	mp.waitlock = lock
	mp.waitunlockf = *(*unsafe.Pointer)(unsafe.Pointer(&unlockf))
	gp.waitreason = reason
	mp.waittraceev = traceEv
	mp.waittraceskip = traceskip
	releasem(mp)
	// can't do anything that might move the G between Ms here.
	mcall(park_m)
}

猜你喜欢

转载自my.oschina.net/chuqq/blog/1819028
今日推荐