Go语言的协程与并发

1 Channel

channel 是Go语言在语言级别提供的 goroutine 间的通信方式。我们可以使用 channel 在两个或多个 goroutine 之间传递消息。
channel可以作为一个先入先出(FIFO)的队列,接收的数据和发送的数据的顺序是一致的。
channel 是类型相关的,也就是说,一个 channel 只能传递一种类型的值,这个类型需要在声明 channel 时指定。如果对 Unix 管道有所了解的话,就不难理解 channel,可以将其认为是一种类型安全的管道。
定义一个 channel 时,也需要定义发送到 channel 的值的类型,注意,必须使用 make 创建 channel,代码如下所示:

ci := make(chan int)
cs := make(chan string)
//可以指定容量,默认为0
cf := make(chan interface{}, 100)

channel有三种类型,默认是双向的:

chan T          // 可以接收和发送类型为 T 的数据
chan<- float64  // 只可以用来发送 float64 类型的数据
<-chan int      // 只可以用来接收 int 类型的数据

chan<- chan int    // 等价 chan<- (chan int)
chan<- <-chan int  // 等价 chan<- (<-chan int)
<-chan <-chan int  // 等价 <-chan (<-chan int)

chan (<-chan int)

发送和接收消息:

//发送消息
ch <- 3
//接收消息
v, ok := <-ch
//for循环也可以处理channel
for i := range c {
    fmt.Println(i)
}

1.1 blocking channel

image.png

1.2 buffered channel

Go语言中有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

缓冲区的实现可以参考channel内部组成结构:


image.png

1.3 select关键字

select处理逻辑

  • 如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。
  • 如果没有case需要处理,则会选择default去处理,如果default case存在的情况下。
  • 如果没有default case,则select语句会阻塞,直到某个case需要处理。
import "fmt"
func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}
func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

select有很重要的一个应用就是超时处理。 因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。
下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1。

import "time"
import "fmt"
func main() {
    c1 := make(chan string, 1)
    go func() {
        time.Sleep(time.Second * 2)
        c1 <- "result 1"
    }()
    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(time.Second * 1):
        fmt.Println("timeout 1")
    }
}

1.4 close函数

内建的close方法可以用来关闭channel。
总结一下channel关闭后sender的receiver操作。
如果channel c已经被关闭,继续往它发送数据会导致panic: send on closed channel。

import "time"
func main() {
    go func() {
        time.Sleep(time.Hour)
    }()
    c := make(chan int, 10)
    c <- 1
    c <- 2
    close(c)
    c <- 3
}

但是从这个关闭的channel中不但可以读取出已发送的数据,还可以不断的读取零值:

c := make(chan int, 10)
c <- 1
c <- 2
close(c)
fmt.Println(<-c) //1
fmt.Println(<-c) //2
fmt.Println(<-c) //0
fmt.Println(<-c) //0

但是如果通过range读取,channel关闭后for循环会跳出:

c := make(chan int, 10)
c <- 1
c <- 2
close(c)
for i := range c {
    fmt.Println(i)
}

通过i, ok := <-c可以查看Channel的状态,判断值是零值还是正常读取的值。

c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

1.5 用法

go社区给出的goroutine开发原则是:

不要通过共享内存来通信,而应该通过通信来共享内存

channel的存在就是为了减少线程间共享内存的操作,而使用通信的方式在实现线程间的同步和通信
下面设计了一个Benchmark测试,用来验证channel和传统互斥锁方式同步的性能差异:

package main

import (
    "errors"
    "sync"
    "testing"
)

type Game struct {
    mtx       sync.Mutex
    bestScore int
    scores    chan int
}

func NewGame() *Game {
    return &Game{
        mtx:       sync.Mutex{},
        bestScore: 0,
        scores:    make(chan int, 10),
    }
}

type Player struct {
    count int
}

func (p *Player) NextScore() (score int, err error) {
    p.count++
    if p.count < 1000000 {
        return p.count, nil
    } else {
        return 0, errors.New("")
    }
}

func (g *Game) run() {
    for score := range g.scores {
        if g.bestScore < score {
            g.bestScore = score
        }
    }
}

func (g *Game) HandlePlayerChannel() error {
    p := Player{}
    for {
        score, err := p.NextScore()
        if err != nil {
            return err
        }
        g.scores <- score
    }
}

func (g *Game) HandlePlayerMutex() error {
    p := Player{}
    for {
        score, err := p.NextScore()
        if err != nil {
            return err
        }
        g.mtx.Lock()
        if g.bestScore < score {
            g.bestScore = score
        }
        g.mtx.Unlock()
    }
}

func BenchmarkChannel(b *testing.B) {
    game := NewGame()
    go game.run()
    b.ResetTimer()
    for i := 0; i <= 200; i++ {
        go game.HandlePlayerChannel()
    }
    b.StopTimer()
}

func BenchmarkMutex(b *testing.B) {
    game := NewGame()
    b.ResetTimer()
    for i := 0; i <= 200; i++ {
        go game.HandlePlayerMutex()
    }
    b.StopTimer()
}
mutex运行结果

buffer为0时channel

buffer为10时channel

buffer为100时channel

整体来看,两者的速度相差不大

2 Runtime与Debug包

2.1 常用函数

runtime.GOMAXPROCS函数
通过runtime.GOMAXPROCS函数,应用程序何以在运行期间设置运行时系统中得P最大数量,但这会引起\color{red}{Stop the Word}。所以应在应用程序最早的调用。并且最好的设置P最大值的方法是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS,而不是在程序中调用runtime.GOMAXPROCS函数。
最后记住,无论我们传递给函数的整数值是什么值,运行时系统的P最大值总会在1~256之间。
runtime.Goexit函数
runtime.Goexit函数被调用后,会立即使调用他的Groution的运行被终止,但其他Goroutine并不会受到影响。runtime.Goexit函数在终止调用它的Goroutine的运行之前会先执行该Groution中还没有执行的defer语句。
runtime.Gosched函数
runtime.Gosched函数的作用是暂停调用他的Goroutine的运行,调用他的Goroutine会被重新置于Gorunnable状态,并被放入调度器可运行G队列中。
runtime.NumGoroutine函数
runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的Goroutine的数量。这里的特指是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。
注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。
runtime.LockOSThread和runtime.UnlockOSThread函数
前者调用会使调用他的Goroutine与当前运行它的M一对一绑定,并且直接由操作系统调度,UnlockOSThread会解除这样的锁定。
注意:

  1. 多次调用前者不会出现任何问题,但最后一次调用的记录会被保留,
  2. 即时之前没有调用前者,对后者的调用也不会产生任何副作用
  3. 绑定后G创建的子G不会被绑定
  4. time.Sleep等操作建议改为等效的syscall操作,否则这些操作效果可能达不到预期

runtime.SetFinalizer函数
类似于Java中的finalize函数,会在对象没有引用且GC准备回收该对象时被调用。

  1. 即使程序正常结束或者发生错误, 但是在对象被 gc 选中并被回收之前,SetFinalizer 都不会执行, 所以不要在SetFinalizer中执行将内存中的内容flush到磁盘这种操作
  2. SetFinalizer 最大的问题是延长了对象生命周期。在第一次回收时执行 Finalizer 函数,且目标对象重新变成可达状态,直到第二次才真正 “销毁”。这对于有大量对象分配的高并发算法,可能会造成很大麻烦
  3. 指针构成的 "循环引⽤" 加上 runtime.SetFinalizer 会导致内存泄露

debug.SetMaxStack函数
debug.SetMaxStack函数的功能是约束单个Groutine所能申请的栈空间的最大尺寸。
debug.SetMaxThreads函数
debug.SetMaxThreads函数的功能是对go语言运行时系统所使用的内核线程的数量(确切的说是M的数量)进行设置

2.2 使用案例

如果限定GOMAXPROCS的值为1,下面的测试用例运行结果是?

func TestRuntime(t *testing.T) {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i)
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    
    fmt.Printf("---main end loop---\n")
    wg.Wait()
    fmt.Printf("---  main exit  ---\n")
}

在Go 1.16中的运行结果

=== RUN   TestRuntime
---main end loop---
B:  9
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
B:  0
B:  1
B:  2
B:  3
B:  4
B:  5
B:  6
B:  7
B:  8
---  main exit  ---
--- PASS: TestRuntime (0.00s)
PASS

在Go 1.4中的运行结果

=== RUN   TestRuntime
---main end loop---
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
B:  0
B:  1
B:  2
B:  3
B:  4
B:  5
B:  6
B:  7
B:  8
B:  9
---  main exit  ---
--- PASS: TestRuntime (0.00s)
PASS

可以看到---main end loop---总是最先输出,表明在1个操作系统线程的情况下,只有main协程执行到wg.Wait()阻塞等待时,其子协程才能被执行,而子协程的执行顺序正好对应于它们入队列的顺序。

新版本中先打印9是因为新版的线程调度中有一个runnext指针指向下次要执行的goroutine,每次创建goroutine时会把当前goroutine放进去,之前runnext指向的goroutine才会放入队列。goroutine执行的时候,会先取runnext指向的goroutine运行,之后才会从队列中顺序取出。

如果在每次打印前增加一个sleep逻辑:

func TestRuntime(t *testing.T) {
    runtime.GOMAXPROCS(1)
    wg := sync.WaitGroup{}
    wg.Add(20)
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println("A: ", i)
            wg.Done()
        }()
    }
    for i := 0; i < 10; i++ {
        go func(i int) {
            time.Sleep(time.Second)
            fmt.Println("B: ", i)
            wg.Done()
        }(i)
    }
    
    fmt.Printf("---main end loop---\n")
    wg.Wait()
    fmt.Printf("---  main exit  ---\n")
}

输出结果如下:

=== RUN   TestRuntime
---main end loop---
B:  8
B:  9
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
A:  10
B:  0
B:  1
B:  2
B:  3
B:  4
B:  5
B:  6
B:  7
---  main exit  ---
--- PASS: TestRuntime (0.00s)
PASS

原理是Sleep的时候会把所有线程重新放入runnext和Local队列,sleep结束之后协程被依次唤醒并执行

2.3 协程中的异常处理

如果在gouroutine中出现panic:

func main() {
    go func() {
        panic("goroutine panic")
    }()

    log.Println("main done")
}

会导致main函数异常终止:

panic: goroutine panic

    goroutine 6 [running]:
    main.main.func1()
            /Users/chenxiangyu/go/src/github.com/go-flowx/awesomeProject/main.go:7 +0x39
    created by main.main
            /Users/chenxiangyu/go/src/github.com/go-flowx/awesomeProject/main.go:6 +0x35

要用defer func保证在当前goroutine中recover掉panic

func main() {
    go func() {
        defer func() {
            if e := recover(); e != nil {
                log.Printf("recover: %v", e)
            }
        }()
        panic("goroutine panic")
    }()

    log.Println("main done")
}

3 Sync包

3.1 并发控制

3.1.1 Mutex

mutex := &sync.Mutex{}

mutex.Lock()
// Update共享变量 (比如切片,结构体指针等)
mutex.Unlock()

3.1.2 RWMutex

mutex := &sync.RWMutex{}

mutex.Lock()
// Update 共享变量
mutex.Unlock()

mutex.RLock()
// Read 共享变量
mutex.RUnlock()

sync.RWMutex是一个读写互斥锁,允许多个读锁或一个写锁互斥存在,而sync.Mutex允许一个读锁或一个写锁互斥存在。

BenchmarkMutexLock-4       83497579         17.7 ns/op
BenchmarkRWMutexLock-4     35286374         44.3 ns/op
BenchmarkRWMutexRLock-4    89403342         15.3 ns/op

可以看到锁定/解锁sync.RWMutex读锁的速度比锁定/解锁sync.Mutex更快,另一方面,在sync.RWMutex上调用Lock()/ Unlock()是最慢的操作。
因此,只有在频繁读取和不频繁写入的场景里,才应该使用sync.RWMutex

3.1.3 WaitGroup

sync.WaitGroup也是一个经常会用到的同步原语,它的使用场景是在一个goroutine等待一组goroutine执行完成。
sync.WaitGroup的数据结构非常简单,内部拥有一个内部计数器。当计数器等于0时,则Wait()方法会立即返回。否则它将阻塞执行Wait()方法的goroutine直到计数器等于0时为止。

    // 64 位值: 高 32 位用于计数,低 32 位用于等待计数
    // 64 位的原子操作要求 64 位对齐,但 32 位编译器无法保证这个要求
    // 因此分配 12 字节然后将他们对齐,其中 8 字节作为状态,其他 4 字节用于存储原语
    state1 [3]uint32

sync.WaitGroup对state1变量的操作都是使用atomic包的原子操作实现的,等待时使用runtime的信号量实现阻塞等待:

// Add将增量(可能为负)添加到WaitGroup计数器中。
// 如果计数器为零,则释放等待时阻塞的所有goroutine。
// 如果计数器变为负数,请添加恐慌。
//
// 请注意,当计数器为 0 时发生的带有正的 delta 的调用必须在 Wait 之前。
// 当计数器大于 0 时,带有负 delta 的调用或带有正 delta 调用可能在任何时候发生。
// 通常,这意味着对Add的调用应在语句之前执行创建要等待的goroutine或其他事件。
// 如果将WaitGroup重用于等待几个独立的事件集,新的Add调用必须在所有先前的Wait调用返回之后发生。
func (wg *WaitGroup) Add(delta int) {
    // 获取counter,waiter,以及semaphore对应的指针
    statep, semap := wg.state()
    ...
    // 将 delta 加到 statep 的前 32 位上,即加到计数器上
    state := atomic.AddUint64(statep, uint64(delta)<<32)
    // 高地址位counter
    v := int32(state >> 32)
    // 低地址为waiter
    w := uint32(state)
    ...
    // 计数器不允许为负数
    if v < 0 {
        panic("sync: negative WaitGroup counter")
    }
    // wait不等于0说明已经执行了Wait,此时不容许Add
    if w != 0 && delta > 0 && v == int32(delta) {
        panic("sync: WaitGroup misuse: Add called concurrently with Wait")
    }
    // 计数器的值大于或者没有waiter在等待,直接返回
    if v > 0 || w == 0 {
        return
    }
    // 运行到这里只有一种情况 v == 0 && w != 0

    // 这时 Goroutine 已经将计数器清零,且等待器大于零(并发调用导致)
    // 这时不允许出现并发使用导致的状态突变,否则就应该 panic
    // - Add 不能与 Wait 并发调用
    // - Wait 在计数器已经归零的情况下,不能再继续增加等待器了
    // 仍然检查来保证 WaitGroup 不会被滥用

    // 这一点很重要,这段代码同时也保证了这是最后的一个需要等待阻塞的goroutine
    // 然后在下面通过runtime_Semrelease,唤醒被信号量semap阻塞的waiter
    if *statep != state {
        panic("sync: WaitGroup misuse: Add called concurrently with Wait")
    }
    // 结束后将等待器清零
    *statep = 0
    for ; w != 0; w-- {
        // 释放信号量,通过runtime_Semacquire唤醒被阻塞的waiter
        runtime_Semrelease(semap, false, 0)
    }
}

// Wait blocks until the WaitGroup counter is zero.
func (wg *WaitGroup) Wait() {
    // 获取counter,waiter,以及semaphore对应的指针
    statep, semap := wg.state()
    ...
    for {
        // 获取对应的counter和waiter数量
        state := atomic.LoadUint64(statep)
        v := int32(state >> 32)
        w := uint32(state)
        // Counter为0,不需要等待
        if v == 0 {
            if race.Enabled {
                race.Enable()
                race.Acquire(unsafe.Pointer(wg))
            }
            return
        }
        // 原子(cas)增加waiter的数量(只会被+1操作一次)
        if atomic.CompareAndSwapUint64(statep, state, state+1) {
            ...
            // 这块用到了,我们上文讲的那个信号量
            // 等待被runtime_Sem release释放的信号量唤醒
            // 如果 *semap > 0 则会减 1,等于0则被阻塞
            runtime_Semacquire(semap)

            // 在这种情况下,如果 *statep 不等于 0 ,则说明使用失误,直接 panic
            if *statep != 0 {
                panic("sync: WaitGroup is reused before previous Wait has returned")
            }
            ...
            return
        }
    }
}

3.1.4 Once

sync.Once是一个简单而强大的原语,可确保一个函数仅执行一次。在下面的示例中,只有一个goroutine会显示输出消息:

once := &sync.Once{}
for i := 0; i < 4; i++ {
    i := i
    go func() {
        once.Do(func() {
            fmt.Printf("first %d\n", i)
        })
    }()
}

3.1.5 Cond

sync.Cond可能是sync包提供的同步原语中最不常用的一个,它用于发出信号(一对一)或广播信号(一对多)到goroutine。让我们考虑一个场景,我们必须向一个goroutine指示共享切片的第一个元素已更新。创建sync.Cond需要sync.Locker对象(sync.Mutexsync.RWMutex):

func main() {
    //创建一个切片
    s := make([]int, 1)
    //创建Cond对象
    cond := sync.NewCond(&sync.Mutex{})
    for i := 0; i < runtime.NumCPU(); i++ {
        go printFirstElement(s, cond)
    }

    i := get()
    //加锁,进入互斥区
    cond.L.Lock()
    s[0] = i
    //通知一个goroutine
    //cond.Signal()
    //通知全部goroutine
    cond.Broadcast()
    //解锁,退出互斥区
    cond.L.Unlock() 
}

func printFirstElement(s []int, cond *sync.Cond) {
    cond.L.Lock()
    //等待信号
    cond.Wait()
    fmt.Printf("%d\n", s[0])
    cond.L.Unlock()
}

3.2 并发容器

3.2.1 Sync.Map

基本用法和Map一致:

m := &sync.Map{}

// 添加元素
m.Store(1, "one")
m.Store(2, "two")

// 获取元素1
value, contains := m.Load(1)
if contains {
  fmt.Printf("%s\n", value.(string))
}

// 返回已存value,否则把指定的键值存储到map中
value, loaded := m.LoadOrStore(3, "three")
if !loaded {
  fmt.Printf("%s\n", value.(string))
}

m.Delete(3)

// 迭代所有元素
m.Range(func(key, value interface{}) bool {
  fmt.Printf("%d: %s\n", key.(int), value.(string))
  return true
})

输出结果如下:

one
three
1: one
2: two

数据结构:



基本原理:

  • 通过read和dirty两个字段将读写分离,读的数据存在于read字段的,最新写的数据位于dirty字段上。
  • 读取时先查询read,不存在时查询dirty,写入时只写入dirty
  • 读取read不需要加锁,而读或写dirty需要加锁
  • 使用misses字段来统计read被穿透的次数,超过一定次数将数据从dirty同步到read上
  • 删除数据通过标记来延迟删除

适用场景:

  • 当我们对map有频繁的读取和不频繁的写入时。
  • 当多个goroutine读取,写入和覆盖不相交的键时。具体是什么意思呢?例如,如果我们有一个分片实现,其中包含一组4个goroutine,每个goroutine负责25%的键(每个负责的键不冲突)。在这种情况下,sync.Map是首选。

3.2.2 Sync.Pool

sync.Pool是一个并发池,负责安全地保存一组对象。
需要注意的是Get()方法会从并发池中随机取出对象,无法保证以固定的顺序获取并发池中存储的对象。

pool := &sync.Pool{}

pool.Put(NewConnection(1))
pool.Put(NewConnection(2))
pool.Put(NewConnection(3))

connection := pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)
connection = pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)
connection = pool.Get().(*Connection)
fmt.Printf("%d\n", connection.id)

还可以为sync.Pool指定一个创建者方法:

pool := &sync.Pool{
  New: func() interface{} {
    return NewConnection()
  },
}

connection := pool.Get().(*Connection)

使用场景:

  • 当我们必须重用共享的和长期存在的对象(例如,数据库连接)时
  • 用于优化内存分配
func writeFile(pool *sync.Pool, filename string) error {
    buf := pool.Get().(*bytes.Buffer)

  defer pool.Put(buf)

    // Reset 缓存区,不然会连接上次调用时保存在缓存区里的字符串foo
    // 编程foofoo 以此类推
    buf.Reset()

    buf.WriteString("foo")

    return ioutil.WriteFile(filename, buf.Bytes(), 0644)
}

4 额外命令

4.1 compile

go:noinline
表示不做内联。
好处:

  • 减少函数调用的开销,提高执行速度
  • 复制后的更大函数体为其他编译优化带来可能性,如过程间优化
  • 消除分支,并改善空间局部性和指令顺序性,同样可以提高性能

坏处:

  • 代码复制带来的空间增长。
  • 如果有大量重复代码,反而会降低缓存命中率,尤其对 CPU 缓存是致命的

go:nosplit
跳过栈溢出检测。
显然地,不执行栈溢出检查,可以提高性能,但同时也有可能发生 stack overflow 而导致编译失败。
go:noescape
不进行逃逸分析
最显而易见的好处是,GC 压力变小了。 因为它已经告诉编译器,下面的函数无论如何都不会逃逸,那么当函数返回时,其中的资源也会一并都被销毁。 不过,这么做代表会绕过编译器的逃逸检查,一旦进入运行时,就有可能导致严重的错误及后果。
go:norace
跳过竞态检测,减少编译时间。

4.2 runtime

go:systemstack
go:systemstack 表明一个函数必须在系统栈上运行,这个会通过一个特殊的函数前引(prologue)动态地验证。
go:nowritebarrier
go:nowritebarrier 告知编译器如果以下函数包含了写屏障,触发一个错误(这不会阻止写屏障的生成,只是单纯一个假设)。
一般情况下你应该使用 go:nowritebarrierrec。go:nowritebarrier 当且仅当 “最好不要” 写屏障,但是非正确性必须的情况下使用。
go:nowritebarrierrec 与 go:yeswritebarrierrec
go:nowritebarrierrec 告知编译器如果以下函数以及它调用的函数(递归下去),直到一个 go:yeswritebarrierrec 为止,包含了一个写屏障的话,触发一个错误。
逻辑上,编译器会在生成的调用图上从每个 go:nowritebarrierrec 函数出发,直到遇到了 go:yeswritebarrierrec 的函数(或者结束)为止。如果其中遇到一个函数包含写屏障,那么就会产生一个错误。
go:nowritebarrierrec 主要用来实现写屏障自身,用来避免死循环。
这两种编译指令都在调度器中所使用。写屏障需要一个活跃的 P(getg().m.p != nil),然而调度器相关代码有可能在没有一个活跃的 P 的情况下运行。在这种情况下,go:nowritebarrierrec 会用在一些释放 P 或者没有 P 的函数上运行,go:yeswritebarrierrec 会用在重新获取到了 P 的代码上。因为这些都是函数级别的注释,所以释放 P 和获取 P 的代码必须被拆分成两个函数。
go:notinheap
go:notinheap 适用于类型声明,表明了一个类型必须不被分配在 GC 堆上。特别的,指向该类型的指针总是应当在 runtime.inheap 判断中失败。这个类型可能被用于全局变量、栈上变量,或者堆外内存上的对象(比如通过 sysAlloc、persistentalloc、fixalloc 或者其它手动管理的 span 进行分配)。特别的:
● new(T)、make([]T)、append([]T, ...) 和隐式的对于 T 的堆上分配是不允许的(尽管隐式的分配在 runtime 中是从来不被允许的)。
● 一个指向普通类型的指针(除了 unsafe.Pointer)不能被转换成一个指向 go:notinheap 类型的指针,就算它们有相同的底层类型(underlying type)。
● 任何一个包含了 go:notinheap 类型的类型自身也是 go:notinheap 的。如果结构体和数组包含 go:notinheap 的元素,那么它们自身也是 go:notinheap 类型。map 和 channel 不允许有 go:notinheap 类型。为了使得事情更加清晰,任何隐式的 go:notinheap 类型都应该显式地标明 go:notinheap。
● 指向 go:notinheap 类型的指针的写屏障可以被忽略。
最后一点是 go:notinheap 类型真正的好处。runtime 在底层结构中使用这个来避免调度器和内存分配器的内存屏障以避免非法检查或者单纯提高性能。这种方法是适度的安全(reasonably safe)的并且不会使得 runtime 的可读性降低。
go:linkname localname linkname
编译时将外部包私有成员函数或变量连接到当前类,常量不可用。

//go:linkname sayTest a.say 
func sayTest(name string) string
func Greet(name string) string {
    return sayTest(name)
}

noCopy
noCopy是一个golang基础包内部的结构体,用于禁止sync结构体被复制

type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

type DoNotCopyMe struct {
  noCopy
}

var d DoNotCopyMe
d2 := d // 这里发生了拷贝

// 假设我们上面的代码写在main.go文件中
go vet main.go
// 会看到如下输出
./main.go:616:11: assignment copies lock value to d2: command-line-arguments.DoNotCopyMe contains command-line-arguments.noCopy
./main.go:618:23: call of fmt.Printf copies lock value: command-line-arguments.DoNotCopyMe contains command-line-arguments.noCopy
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容