WaitGroup
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done() //我们向WaitGroup 表明我们已经退出了
fmt.Println("1st goroutine sleeping...")
time.Sleep(1)
}()
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("2nd goroutine sleeping...")
time.Sleep(2)
}()
wg.Wait() //这将阻塞main goroutine, 直到所有goroutine 表明它们已经退出。
fmt.Println("All goroutines complete.")
}
//2nd goroutine sleeping...
//1st goroutine sleeping...
//All goroutines complete.
可以将WaitGroup 视为一个并发-安全的计数器;通过传入的整数执行add 方法增加计数器的增量,并调用Done方法对计数器进行递减。Wait阻塞,直到
计数器为零。
注意,添加的调用(wg.Add(1)) 写在 goroutine之外完成的。如果写在goroutine 之内,就会引入一种竞争条件,因为不确定goroutine 何时会被调度,
WaitGroup来追踪一组goroutine :
func main(){
hello := func(wg *sync.WaitGroup, id int) {
defer wg.Done()
fmt.Printf("hello from %v!\n", id)
}
const numGreeters = 5
var wg sync.WaitGroup
wg.Add(numGreeters)
for i:= 0; i< numGreeters; i++{
go hello(&wg, i+1)
}
wg.Wait()
}
//hello from 2!
//hello from 5!
//hello from 3!
//hello from 1!
//hello from 4!
互斥锁 :
Mutex 是 "互斥"的意思, 是保护程序中临界区的一种方式。Mutex 提供了一种安全的方式来表示对这些 共享资源(临界区) 的独占访问
为了访问/使用 一个资源:
channel 通过通信共享内存, 而Mutex 通过开发人员的约定 同步访问 共享内存
func main() {
var count int
var lock sync.Mutex
increment := func() {
lock.Lock()
defer lock.Unlock()
count++
fmt.Printf("Incrementing: %d\n",count)
}
decrement := func() {
lock.Lock()
defer lock.Unlock()
count--
fmt.Printf("Decrementing:%d\n", count)
}
//增量
var arithmetic sync.WaitGroup
for i:=0; i<=100; i++{ //5
arithmetic.Add(1)
go func() {
defer arithmetic.Done()
increment()
}()
}
//减量
for i:=0; i <= 100; i++{ //5
arithmetic.Add(1)
go func() {
defer arithmetic.Done()
decrement()
}()
}
arithmetic.Wait()
fmt.Println("Arithmetic complete")
fmt.Printf("count's value is :%d\n", count)
}
/**
如果不加锁: 某次运行最后 Incrementing:3 ; count's value is :-1
加锁,总是能保证 Incrementing:0
我们总是在defer语句中调用 Unlock。
*/
读写锁 :?? todo;
//通常建议使用RWMutex, 而不是Mutex, 因为它在逻辑上更加合理
等待条件:
通常情况下,在goroutine 继续执行之间, 需要等待其中一个信号。
如何在没有Cond类型的情况下实现这一目标,一个简单的方法就是使用无限循环:
for conditionTrue() == false {
}
分析: 这将消耗一个CPU的 所有周期。稍微改进如下:
for conditionTrue() == false{
time.Sleep(1*time.Millisecond
}
分析: 这比上一个好,但是仍然很低效,而且你必须弄清楚要等待多久:太长,会人为的降低性能,太短,会不必要地消耗太多的CPU时间。
使用cond的方式更高效,如下:
func main(){
c := sync.NewCond(&sync.Mutex{})
c.L.Lock() //这里必须上锁, 因为在进入Locker的时候,执行Wait会自动执行Unlock
for conditionTrue() == false{
c.Wait() //等待通知。 这里会阻塞,goroutine 将被暂停
}
c.L.Unlock() //4,为这个条件解决。这是必要的;因为当执行Wait 退出操作的时候,它会在Locker 上调用Lock方法
}
注意: 调用Wait 不只是阻塞, 它挂起了当前的goroutine,允许其他goroutine运行。
当调用wait时,会发生一些其他事情: 进入Wait后,在Cond 变量的Locker上调用Unlock方法,在退出Wait时,在Cond变量的Locker上执行Lock方法。
看起来我们在等待条件发生的时候一直持有这个锁,但事实并非如此。
有一个固定长度为2的队列,还有10个我们想推送到队列中的项目,在队列中有空间时能立即得到通知。
func main() {
c := sync.NewCond(&sync.Mutex{})
queue := make([]interface{},0, 10) //接下来,我们创建一个长度为0的切片。因为我们最终会添加10个项目
removeFromQueue := func(delay time.Duration) {
time.Sleep(delay)
c.L.Lock()
queue = queue[1:]
fmt.Println("Removed from queue")
c.L.Unlock()
c.Signal() //我们让一个正在等待的goroutine知道发生了什么事情。
}
for i:=0;i<10;i++{
c.L.Lock() //通过在条件的锁存器上调用锁来进入临界区。
for len(queue) == 2{
c.Wait()
}
fmt.Println("Adding to queue")
queue = append(queue, struct {}{})
go removeFromQueue(2*time.Second)
c.L.Unlock() //退出条件的临界区
}
}
cond类型提供的两种通知方法:
Signal
Broadcast 。
运行时内部维护一个FIFO列表,等待接收信号,
Signal发现等待时间最长的goroutine并通知它
而Broadcast 向所有等待的goroutine发送信号
与channel相比,Cond类型的性能要高很多。
使用Broadcast的例子:
假设创建一个带有按钮的GUI 应用程序。 我们想注册任意数量的函数,当该按钮被点击时,它将运行。
代码如下:
func main(){
type Button struct {
Clicked *sync.Cond
}
button := Button{Clicked:sync.NewCond(&sync.Mutex{})} //包含一个条件, Clicked
//定义一个构造函数,允许注册函数 处理来自条件 的信号
subscribe := func(c *sync.Cond, fn func()) {
var goroutineRunning sync.WaitGroup
goroutineRunning.Add(1)
go func() {
goroutineRunning.Done()
c.L.Lock()
defer c.L.Unlock()
c.Wait()
fn()
}()
goroutineRunning.Wait()
}
var clickRegistered sync.WaitGroup
clickRegistered.Add(3)
subscribe(button.Clicked, func() {
fmt.Println("Maximing window.")
clickRegistered.Done()
})
subscribe(button.Clicked, func() {
fmt.Println("Displaying annoying dialog box!")
clickRegistered.Done()
})
subscribe(button.Clicked, func() {
fmt.Println("Mouse clicked.")
clickRegistered.Done()
})
//接下来,我们模拟一个用户通过单击应用程序的按钮 来单击鼠标按键。
button.Clicked.Broadcast()
clickRegistered.Wait()
}
//Cond的使用最好被限制在一个紧凑的范围中,或者通过封装它的类型来暴露在更大范围内。
once :
sync.Once 是一种类型,它在内部使用一些sync原语,以确保即使在不同的goroutine上,也只会调用一次Do方法传进来的函数。
检查Go语言的标准库,看看使用这个原语的频率。
grep -ir sync.Once $(go env GOROOT)/src | wc -l //70
func main(){
var count int
increment := func() {
count++
}
var once sync.Once
var increments sync.WaitGroup
increments.Add(100)
for i:=0; i<100; i++{
go func() {
defer increments.Done()
once.Do(increment)
}()
}
increments.Wait()
fmt.Printf("Count is %d\n", count) //Count is 1
//var count int
//increment := func() {count++}
//decrement := func() {count--}
//
//var once sync.Once
//once.Do(increment)
//once.Do(decrement)
//fmt.Printf("Count:%d\n", count) //Count:1
}
池 / Pool
Pool模式是一种创建和提供可供使用的固定数量实例或Pool实例的方法。它通常用于约束创建 贵的场景(如数据库连接)。
对于Go语言的sync.Pool,这种数据类型可以被多个goroutine安全地使用。
func main(){
myPool := &sync.Pool{
New : func() interface{}{
fmt.Println("Creating new instance.")
return struct {}{}
},
}
//myPool.Get() //在这里调用Pool的get方法。这将调用Pool中定义的new 函数,因为实例还没有实例化。
instance := myPool.Get()
myPool.Put(instance) //用完把实例放进池中,这就增加了实例的可用数量。
myPool.Get()
}
func main(){
var numCalcsCreated int
calcPool := &sync.Pool{
New : func() interface{} {
numCalcsCreated += 1
mem := make([]byte, 1024)
return &mem
},
}
//用4kB 初始化pool
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
calcPool.Put(calcPool.New())
const numWorkers = 1024*1024
var wg sync.WaitGroup
wg.Add(numWorkers)
for i:= numWorkers; i>0; i--{
go func() {
defer wg.Done()
mem := calcPool.Get().(*[]byte)
defer calcPool.Put(mem)
//假设,用这个内存。
}()
}
wg.Wait()
fmt.Printf("%d calculators were created. ",numCalcsCreated)
}
//8 calculators were created. 为什么不是4个,而是8个??
//如果没有 sync.Pool运行这个例子,,在最坏情况下要分配 1024*1024 * 1k (1G)字节内存。
使用Pool情况: 一 : 创建和提供可供使用的固定数量实例或Pool实例的方法。它通常用于约束创建 贵的场景(如数据库连接)。 二: 用Pool 来尽可能快地将预先分配的对象缓存加载启动。这种情况下通过提前加载获取引用, 来节省消费者的时间。这在编写 高吞吐量网络服务器时十分常见,服务器试图快速响应请求。
func connectToService() interface{}{
time.Sleep(1 *time.Second)
return struct {}{}
}
func startNetworkDaemon() *sync.WaitGroup{
var wg sync.WaitGroup
wg.Add(1)
go func() {
server, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatalf("cannot listen : %v", err)
}
defer server.Close()
wg.Done()
for {
conn, err := server.Accept()
if err != nil {
log.Printf("cannot accept connection: %v", err)
continue
}
connectToService()
fmt.Fprintln(conn, "")
conn.Close()
}
}()
return &wg
}
// 1004423605 ns/op
func warmServiceConnCache() * sync.Pool {
p := &sync.Pool{
New: connectToService,
}
for i:=0; i<10; i++ {
p.Put(p.New())
}
return p
}
func warmStartNetworkDaemon() *sync.WaitGroup{
var wg sync.WaitGroup
wg.Add(1)
go func(){
connPool := warmServiceConnCache()
server, err := net.Listen("tcp", "localhost:8080")
if err != nil{
log.Fatalf("cannot listen: %v", err)
}
defer server.Close()
wg.Done()
for{
conn, err := server.Accept()
if err != nil{
log.Printf("cannot accept connection: %v", err)
continue
}
svcConn := connPool.Get()
fmt.Fprintln(conn, "")
connPool.Put(svcConn)
conn.Close()
}
}()
return &wg
}
pool_test.go 如下:
func init(){
daemonStarted := startNetworkDaemon() // //1005524308 ns/op
//daemonStarted := warmStartNetworkDaemon() //1006360645 ns/op
daemonStarted.Wait()
}
func BenchmarkNetworkRequest(b *testing.B){
for i:=0; i< b.N; i++{
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
b.Fatalf("cannot dial host : %v", err)
}
if _, err := ioutil.ReadAll(conn); err != nil{
b.Fatalf("cannot read: %v", err)
}
conn.Close()
}
}
// 1002034255 ns/op // benchmark 并没有快三个数量级 这是为什么 /** 什么情况下用Pool 不合适? 如果你使用Pool 代码所需要的东西不是大概同质的, 那么从Pool中转化检索到所需要的内容的时间可能比重新实例化内容要花费的时间更多。 当你使用Pool工作时,记住以下几点: 1。 当实例化 sync.Pool ,使用 new 方法创建一个成员变量,在调用时是线程安全的。 2。 当你收到一个来自Get的实例时,不要对所接收的对象的状态做任何假设 3。 当你用完了一个从Pool中取出来的对象时,一定要调用Put,否则,Pool就无法复用这个实例了。通常情况下,这是调用defer完成的。