并发安全(竞态问题)

  让一个程序并发安全并不需要其中的每一个具体类型都是并发安全的。实际上并发安全的类型其实是特例而不是普遍存在的,所以仅在文档指出类型是安全的情况下,才可以并发的访问一个变量。与之对应的是,导出的包级别函数通常可以认为是并发安全的。因为包级别的变量无法限制在一个goroutine内。所以那些修改这些变量的函数必须采用互斥机制。

  例如下面代码就会存在竞态问题导致结果与与其不否

var x int64
var wg sync.WaitGroup

func add() {
	for i := 0; i < 5000; i++ {
		x = x + 1
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

  这时引入锁至关重要,在go中sync包提供了锁机制

sync包  

  Sync包同步提供基本的同步原语,如互斥锁。 除了Once和WaitGroup类型之外,大多数类型都是供低级库例程使用的。 通过Channel和沟通可以更好地完成更高级别的同步。并且此包中的值在使用过后不要拷贝。

  sync包中主要有:Locker, Cond, Map, Mutex, Once, Pool,、RWMutex, WaitGroup

互斥锁:sync.Mutex

  互斥锁的模式应用非常广泛,所以sync包有一个单独的Mutex类型来支持这种模式,它的Lock方法用来获取令牌(token,此过程也称为上锁), Unlock方法用于释放令牌。

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	for i := 0; i < 5000; i++ {
		lock.Lock() // 加锁
		x = x + 1
		lock.Unlock() // 解锁
	}
	wg.Done()
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

 在Lock和Unlock之间的代码,可以自由的读取和修改共享变量,这一部分称为临界区域。在锁的持有人调用Unlock之前,其他的goroutine不能获取锁。

读写互斥锁:sync.RWMutex

  在某种情况下,函数只须读取变量的状态,所以多个函数可以安全的并发执行。只要在写入没有同时就行,在这种场景下,就需要一种特殊的安全锁,它只允许读操作可以并发执行,但写操作需要获得安全独享的访问权限。

var (
	x      int64
	wg     sync.WaitGroup
	lock   sync.Mutex
	rwlock sync.RWMutex
)

func write() {
	// lock.Lock()   // 加互斥锁
	rwlock.Lock() // 加写锁
	x = x + 1
	time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
	rwlock.Unlock()                   // 解写锁
	// lock.Unlock()                     // 解互斥锁
	wg.Done()
}

func read() {
	// lock.Lock()                  // 加互斥锁
	rwlock.RLock()               // 加读锁
	time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
	rwlock.RUnlock()             // 解读锁
	// lock.Unlock()                // 解互斥锁
	wg.Done()
}

func main() {
	start := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}

	wg.Wait()
	end := time.Now()
	fmt.Println(end.Sub(start))
}

延迟初始化:sync.Once

  延迟是一个昂贵的初始化步骤到有实际需求的时刻是一个很好的实践。而sync.Once是一个可以被多次调用但是只执行一次,若每次调用Do时传入参数f不同,但是只有第一个才会被执行。

sync.Once有一个 Do方法。示例如下

var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }

 执行虽然调用了10次,但是只执行了1次。BTW:这个东西可以用来写单例。

单例(借助Once)

package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

sync.WaitGroup

  waitgroup 用来等待一组goroutines的结束,在主Goroutine里声明,并且设置要等待的goroutine的个数,每个goroutine执行完成之后调用 Done,最后在主Goroutines 里Wait即可。waitgroup含有三种方法

func (wg *WaitGroup) Add(d int)  //计数器+d
func (wg *WaitGroup) Done()  //计数器-1
func (wg *WaitGroup) Wait()  //阻塞直到计数器变为0

 一个简单的例子

var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	wg.Wait()
}

竞态检测器(race detector) 

  在编写时即使最大的仔细还会出现并发上的错误,幸运的是,go语言在运行时和工具链装备了一个精致并易于使用的动态分析工具:竞态检测器。

简单的把 -race命令行参数加到go build, go run, go test命令里边即可使用该功能。它会让编译器为你的应用或测试构建一个修订后的版本。

  竞态检测器会研究事件流,找到那些有问题的案例,即一个goroutine写入一个变量后,中间没有任何同步的操作,就有另一个goroutine写入了该变量。这种案例表明有对共享变量的并发访问,即数据动态。

  竞态检测器报告所有实际运行了的数据竞态。它只能检测到那些在运行时发生的竞态,无法用来保证肯定不会发生京态。

  有兴趣的可以仔细研究。