Golang并发编程组件

选择channel和互斥量

我们一般根据以下原则选择:

Y
N
Y
N
Y
N
Y
N
性能要求很高的临界区
使用传统的锁
转让数据所有权
使用channel
保护结构的内部状态
协调多个逻辑片段

协程简介

golang的协程模型,是基于M->N的绿色协程映射的。意思是有M个编程语言级别的绿色线程,运行在N个操作系统的线程上,操作系统的线程调度绿色线程,所以 M ≥ N M\ge N MN。之后,协程运行在这些绿色线程上,协程是不可抢占的,每个协程都有自己的时间片,但是不能被抢占。

goroutine的代价非常小,一般来说,我们不需要考虑它的代价。需要注意,GC不回收协程,因此我们需要注意协程泄露的情况。

启动协程需要时间,Go语言经常利用闭包作为协程启动的基本单元,但是需要注意,闭包直接捕获外部循环变量时,可能存在不准确的情况,改进的做法是,使用把外部变量作为参数传入闭包:

package main

import (
	"fmt"
	"time"
)

func main() {
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Printf("%v ", i)
		}()
	}
	time.Sleep(time.Second)
	fmt.Println("\n============")
	for i := 0; i < 10; i++ {
		go func(n int) {
			fmt.Printf("%v ", n)
		}(i)
	}
	time.Sleep(time.Second)
}
/*
输出结果
10 10 10 10 10 10 2 10 10 10 
============
0 1 2 6 5 7 3 8 4 9 
*/

WaitGroup

该类型作为协程之间同步使用的,一般来说,我们需要等待一组并发协程归并时,需要借助这个工具。Wait()是等待归并,Add(n)是增加n个数值,Done()减少一个数据,当数据是0时,Wait()停止阻塞。给出代码示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	N := 5
	wg.Add(N)  // 添加数据
	fmt.Println("Start")
	for i := 0; i < N; i++ {
		go func() {
			defer wg.Done()  // 一般在defer中Done操作
			fmt.Println("hello world")
			time.Sleep(time.Second)
		}()
	}
	fmt.Println("Waiting")
	wg.Wait()  // 这里阻塞
	fmt.Println("Done !")
}

/*
Start
Waiting
hello world
hello world
hello world
hello world
hello world
Done !
*/

互斥锁和读写锁

互斥锁

互斥锁用于读写不确定的情况,而读写锁用于已知读写的情况。前者速度比后者慢,后者是有写锁时互斥的。

package main

import (
    "fmt"
    "sync"
)

func main() {
    N := 0
    var wg sync.WaitGroup
    wg.Add(20)
    for i := 0; i < 20; i++ {
        go func() {
            defer wg.Done()
            for j := 0; j < 100; j++ {
                N += 1
            }
        }()
    }
    wg.Wait()
    fmt.Printf("Without lock, N=%d\n", N)

    M := 0
    wg.Add(20)
    var lock sync.Mutex
    for i := 0; i < 20; i++ {
        go func(){
            defer wg.Done()
            for j := 0; j < 100; j++ {
                lock.Lock()
                M += 1
                lock.Unlock()
            }
        }()
    }
    wg.Wait()
    fmt.Printf("With lock, M=%d\n", M)
}

可能的结果输出:

Without lock, N=1802
With lock, M=2000

读写锁

效率更高的一种锁,给出代码示例:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.RWMutex
    var wg sync.WaitGroup
    M := 0
    wg.Add(3)
    go func() {
        defer wg.Done()
        mu.RLock() # 读锁
        fmt.Println("1", time.Now(), "M=", M)
        time.Sleep(time.Second * 3)
        mu.RUnlock()
    }()

    go func() {
        defer wg.Done()
        mu.RLock()
        fmt.Println("2", time.Now(), "M=", M)
        mu.RUnlock()
    }()
    
    go func() {
        defer wg.Done()
        mu.Lock()
        M ++
        mu.UnLock()
    }
    
    wg.Wait()
}

条件变量Cond

条件变量用于wait & signal原语操作,给出代码示例:

package main

import (
        "fmt"
        "sync"
        "time"
)

func main() {
    c := sync.NewCond(&sync.Mutex{})
    queue := make([]interface{}, 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()
    }

    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(time.Second * 1)
            c.L.Unlock()
    }
}

再给出一个广播的例子,可以向一个按键点击行为注册不同的事件,借助sync.Cond实现,代码如下:

package main

import (
    "fmt"
    "sync"
)

type Button struct {  // 模拟按键
    Clicked *sync.Cond
}

func main() {
    button := Button{ Clicked: sync.NewCond(&sync.Mutex{}) }
    subscribe := func(c *sync.Cond, fn func()) { // 订阅,收到c的消息就执行对应的函数
        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("Maximizing 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()
}

相对于channel,该方式的优势在于,Boardcast()方法可以调用多次,行为可以响应多次。注意一点,使用sync.Cond时,最好在一个紧凑的范围内部,否则容易造成混乱。上述方式提供了一个消息注册的基本思路。

once

该方式保证函数只在全局调用一次,可以用作单例模式,给出代码示例:

package main

import (
    "fmt"
    "sync"
)

var once sync.Once  // 注意需要声明

func foo() {
    fmt.Println("foo")
}

func test() {
    fmt.Println("test")
    once.Do(foo)  // 全局唯一执行
}

func main() {
    for i := 0; i < 3; i++ {
        test()
    }
}
/*
输出结果:
test
foo
test
test
*/

注意一点,sync.Once指计算调用Do方法的次数,而不是多少次唯一调用Do方法,举个例子:

package main

import (
    "fmt"
    "sync"
)

var once sync.Once
var count int

func Inc() { count++ }
func Dec() { count-- }

func main() {
    once.Do(Inc)
    once.Do(Dec)
    fmt.Println(count)
}

上述代码输出1,因为once变量的Do只记录自身调用的次数,不是记录某个函数调用的次数!!

单例模式可以利用Do方式唯一初始化。

Pool

这里是指资源池。首先,应该明确什么是资源池,为什么用资源池。某些资源申请需要花费较长的时间,为了可以快速获取这些资源,我们可以提前申请一定数量的资源放在“资源池”中,当需要资源时,可以从资源池中快速获取已经存在的资源;当使用完资源后,会把资源放回池中。

注意,一般来说资源池,不会限制申请资源的数量。比如资源池的容量是10,我们同时申请了15个资源,则有10个会快速从资源池中获取,有5个会重新创建。同样的,当放回的资源数超过10的时候,多的资源会被释放掉。

给出golang中资源池的使用方式:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var numCalsCreated int
    calcPool := &sync.Pool{
        New: func() interface{} {
            numCalsCreated += 1
            mem := make([]byte, 1024)
            return &mem  // 注意这里返回的地址
        },
    }

    for i := 0; i < 4; i++ {
        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.Println(numCalsCreated)  // 8
}

虽然启动了1024 * 1024个协程,但是因为存在资源池,所以实际也就用到了8 * 1024字节的内存。

Map

并发安全的map结构,这里不再赘述,直接参考文档即可。

https://golang.org/pkg/sync/#Map

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值