锋行_THU_SJTU

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Golang在语言级别支持了协程,由runtime进行管理。

在Golang中并发执行某个函数非常简单:

func Add(x, y int) {
    fmt.Println(x + y)
}

func RunRoutine() {
    for i := 0; i < 10; i++ {
        go Add(i, i)
    }
}

但是输出为空。

因为虽然新建了协程调用Add函数,但是该协程还没有来得及执行,程序就结束了。所以输出为空。

如果想让代码按预想的方式运行,就需要让主函数等待所有goroutine退出后再结束。这就引出了goroutine间通信的问题。

首先,我们先用最简单粗暴,也是传统的Lock来解决。

这时的思路是:我们加一个全局的变量count。每次调用了Add,我们就count++。这样,当count=10的时候,就说明10个goroutine都执行完成了。

但是,全局变量的访问需要加锁,这样才能保证count的访问是安全的。

代码如下:

var (
    lock  *sync.Mutex
    count int
)

func Add(x, y int) {
    lock.Lock()
    defer lock.Unlock()
    fmt.Println(x + y)
    count++
}

func RunRoutine() {
    lock = &sync.Mutex{}
    count = 0
    for i := 0; i < 10; i++ {
        go Add(i, i)
    }
    for {
        lock.Lock()
        temp := count
        if temp >= 10 {
            break
        }
        lock.Unlock()
    }
}

这样,执行结果如下:

2
4
18
10
12
14
6
16
0
8

根据结果来看,10个协程都执行结束了,并且10个协程的执行顺序也是随机的。

但是,事情貌似变得糟糕了。我们为了实现一个简单的功能,却写出了非常复杂的代码。

如果用Golang来解决呢,这时我们考虑用channel来解决问题。

channel是Golang提供的goroutine间的通信方式。我们可以使用channel在多个goroutine间传递消息。当然,channel是进程内的通信方式,如果需要进程间通信,可能Socket或者HTTP通信协议更合适。

先看看,如果用channel,如果解决上面的问题。

var (
    chs []chan int
)

func Add(x, y int) {
    fmt.Println(x + y)
    chs[x] <- 1
}

func RunRoutine() {
    chs = make([]chan int, 10)
    for i := 0; i < 10; i++ {
        chs[i] = make(chan int)
        go Add(i, i)
    }
    for _, ch := range chs {
        <-ch
    }
}

这里,我们定义了一个10个元素的channel数组。每次调用Add时,我们在对应的channel中写入一个数据。最后,我们在RunRoutine中遍历了整个数组,当所有的channel都读取完数据,说明10个goroutine都运行结束了。

现在,我们看看channel的语法:

channel的声明:

var chanName chan ElementType

例如: var ch chan int

channel的初始化:

可以利用make对channel进行初始化:

ch = make(chan int)

ch = make(chan int, 1)

前者初始化了一个无缓冲的channel。无缓冲即当某个协程在channel中写入了数据,就马上被阻塞,要等到其他协程消费了该数据,协程才会继续执行。

后者初始化了一个有缓冲的channel,缓冲长度为1。即ch为空时,当某个协程往ch中写入数据,并不会马上阻塞。当ch内有1个数据时,再往ch中写入数据,即会马上阻塞,知道协程消费了数据,使ch内的数据小于等于其缓冲量。

有缓冲的channel可以用range进行读取。

channel的读写:

ch <- 1

i := <- ch

总之就是用箭头来进行读写操作,很直观。

需要注意的就是读写操作带来的阻塞。

select:

select是类似switch的,用来处理channel异步IO的问题。

    select {
    case <- ch:
    case ch <- 1:
    default:
    }    

需要注意的是,多个case同时符合的时候,switch是按顺序执行的,select是随机选择一个分支执行的。

channel的超时机制:

Golang的channel并没有自带的超时机制,但是可以用select来实现。

考虑以下的几种方法:

    timeout := make(chan bool, 1)
    go func() {
        time.Sleep(time.Second)
        timeout <- true
    }()
    select {
    case <- ch:
    case <- timeout:
    }

该方法提供了一个timeout,利用协程sleep 1s之后向timeout中写入一个true。当select中的ch在1秒内没有读出数据时,timeout将读出数据。

    select {
    case <- ch:
    case <-time.After(time.Second):
    }

该方法利用了time.After。该方法的问题在于,这个计时器在select执行之后仍在在runtime中存在。这在高并发的应用场景下会产生性能问题。

to := time.NewTimer(time.Second)
for {
    to.Reset(time.Second)
    select {
    case <-c:
    case <-to.C:
    }
}

该方法算是上一种方法的改进。该方法利用了一个全局的timer,这样可以避免高并发下的计时器的滥用。

单向channel:

    var (
        ch1 chan int
        ch2 chan<- int
        ch3 <-chan int
    )

ch1是普通channel,ch2是只写channel,ch3是只读channel。

    ch4 := make(chan int)
    ch5 := <-chan int(ch4)
    ch6 := chan<- int(ch4)

上面是各种channel之间的类型转换。

关闭channel:

close(ch)

好像并没有什么其他可说的。唯一一点是,我们可以利用多返回值,在读取channel的时候检查channel是否被关闭。

val, ok := <-ch

多核并行:

可以利用如下命令进行设置。但是Golang是否真的可以利用多个核心,还需要实际验证。

runtime.GOMAXPROCS(16)

同步锁:

最开始的时候已经利用锁实现了功能。需要注意的是Lock和Unlock的对应。

唯一性操作:

sync.Once(funcName)

思考了一下,大概可以用来实现单例模式。

 

posted on 2018-10-10 18:45  锋行_THU_SJTU  阅读(154)  评论(0编辑  收藏  举报