Golang 并发Groutine实例解读(二)

go提供了sync包和channel机制来解决协程间的同步与通讯。golang

1、sync.WaitGroup数据结构

sync包中的WaitGroup实现了一个相似任务队列的结构,你能够向队列中加入任务,任务完成后就把任务从队列中移除,若是队列中的任务没有所有完成,队列就会触发阻塞以阻止程序继续运行,具体用法参考以下代码:函数

package main
import (
    "fmt"
    "sync"
)
var waitgroup sync.WaitGroup
func Afunction(shownum int) {
    fmt.Println(shownum)
    waitgroup.Done() //任务完成,将任务队列中的任务数量-1,其实.Done就是.Add(-1)
}
 
func main() {
    for i := 0; i < 10; i++ {
        waitgroup.Add(1) //每建立一个goroutine,就把任务队列中任务的数量+1
        go Afunction(i)
    }
    waitgroup.Wait() //.Wait()这里会发生阻塞,直到队列中全部的任务结束就会解除阻塞
}

咱们能够利用sync.WaitGroup来知足这样的状况:spa

        ▲某个地方须要建立多个goroutine,而且必定要等它们都执行完毕后再继续执行接下来的操做。.net

是的,WaitGroup最大的优势就是.Wait()能够阻塞到队列中的任务都完毕后才解除阻塞。code

2、channel协程

channel是一种golang内置的类型,英语的直译为"通道",其实,它真的就是一根管道,并且是一个先进先出的数据结构。blog

咱们能对channel进行的操做只有4种:队列

(1) 建立chennel (经过make()函数)同步

(2) 放入数据 (经过 channel <- data 操做) 

(3) 取出数据 (经过 <-channel 操做)

(4)  关闭channel (经过close()函数)

可是channel有一些很是给力的性质须要你牢记,请必定要记住并理解好它们:

(1) channel是一种阻塞管道,是自动阻塞的。意思就是,若是管道满了,一个对channel放入数据的操做就会阻塞,直到有某个routine从channel中取出数据,这个放入数据的操做才会执行。相反同理,若是管道是空的,一个从channel取出数据的操做就会阻塞,直到某个routine向这个channel中放入数据,这个取出数据的操做才会执行。这是channel最重要的一个性质,没有之一。

package main
func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 1
    ch <- 1
    ch <- 1 //这一行操做就会发生阻塞,由于前三行的放入数据的操做已经把channel填满了
package main
func main() {
    ch := make(chan int, 3)
    <-ch //这一行会发生阻塞,由于channel才刚建立,是空的,没有东西能够取出
}

(2)channel分为有缓冲的channel和无缓冲的channel。两种channel的建立方法以下:

ch := make(chan int) //无缓冲的channel,同等于make(chan int, 0)
ch := make(chan int, 5) //一个缓冲区大小为5的channel

操做一个channel时必定要注意其是否带有缓冲,由于有些操做会触发channel的阻塞致使死锁。下面就来解释这些须要注意的情景。

首先来看一个一个例子,这个例子是两段只有主函数不一样的代码:

package main
 
import "fmt"
 
func Afuntion(ch chan int) {
    fmt.Println("finish")
    <-ch
}
 
func main() {
    ch := make(chan int) //无缓冲的channel
    go Afuntion(ch)
    ch <- 1
    
    // 输出结果:
    // finish
}
package main
 
import "fmt"
 
func Afuntion(ch chan int) {
    fmt.Println("finish")
    <-ch
}
 
func main() {
    ch := make(chan int) //无缓冲的channel
    //只是把这两行的代码顺序对调一下
    ch <- 1
    go Afuntion(ch)
 
    // 输出结果:
    // 死锁,无结果
}

前一段代码最终会输出"finish"并正常结束,可是后一段代码会发生死锁。为何会出现这种现象呢,我们把上面两段代码的逻辑跑一下。

 

第一段代码:

        1. 建立了一个无缓冲channel

        2. 启动了一个goroutine,这个routine中对channel执行取出操做,可是由于这时候channel为空,因此这个取出操做发生阻塞,可是主routine可没有发生阻塞,它还在继续运行呢

        3. 主goroutine这时候继续执行下一行,往channel中放入了一个数据

        4. 这时阻塞的那个routine检测到了channel中存在数据了,因此接触阻塞,从channel中取出数据,程序就此完毕

 

第二段代码:

        1.  建立了一个无缓冲的channel

        2.  主routine要向channel中放入一个数据,可是由于channel没有缓冲,至关于channel一直都是满的,因此这里会发生阻塞。但是下面的那个goroutine尚未建立呢,主routine在这里一阻塞,整个程序就只能这么一直阻塞下去了,而后。。。而后就没有而后了。。死锁!

 

※从这里能够看出,对于无缓冲的channel,放入操做和取出操做不能再同一个routine中,并且应该是先确保有某个routine对它执行取出操做,而后才能在另外一个routine中执行放入操做。

 

对于带缓冲的channel,就没那么多讲究了,由于有缓冲空间,因此只要缓冲区不满,放入操做就不会阻塞,一样,只要缓冲区不空,取出操做就不会阻塞。并且,带有缓冲的channel的放入和取出能够用在同一个routine中。

可是,并非说有了缓冲就能够随意使用channel的放入和取出了,咱们必定要注意放入和取出的速率问题。下面咱们就举个例子来讲明这种问题:

咱们常常会用利用channel自动阻塞的性质来控制当前运行的goroutine的总数量,以下:

package main
 
import (
    "fmt"
)
 
func Afunction(ch chan int) {
    fmt.Println("finish")
    <-ch //goroutine执行完了就从channel取出一个数据
}
 
func main() {
    ch := make(chan int, 10)
    for i := 0; i < 1000; i++ {
        //每当建立goroutine的时候就向channel中放入一个数据,若是里面已经有10个数据了,就会
        //阻塞,由此咱们将同时运行的goroutine的总数控制在<=10个的范围内
        ch <- 1
        go Afunction(ch)
    }
    // 这里只是示范个例子,固然,接下来应该有些更加周密的同步操做
}

上面这种channel的使用方式几乎常常会用到,可是再看一下接下来这段代码,它和上面这种使用channel的方式几乎同样,可是它会形成问题:

package main
func Afunction(ch chan int) {
    ch <- 1
    ch <- 1
    ch <- 1
    ch <- 1
    ch <- 1
 
    <-ch
}
 
func main() {
    //主routine的操做同上面那段代码
    ch := make(chan int, 10)
    for i := 0; i < 100; i++ {
        ch <- 1
        go Afunction(ch)
    }
 
    // 这段代码运行的结果为死锁
}

上面这段运行和以前那一段基本上原理是同样的,可是运行后却会发生死锁。为何呢?其实总结起来就一句话,"放得太快,取得太慢了"。

按理说,咱们应该在咱们主routine中建立子goroutine并每次向channel中放入数据,而子goroutine负责从channel中取出数据。可是咱们的这段代码在建立了子goroutine后,每一个routine会向channel中放入5个数据。这样,每向channel中放入6个数据才会执行一次取出操做,这样一来就可能会有某一时刻,channel已经满了,可是全部的routine都在执行放入操做(由于它们当前执行放入操做的几率是执行取出操做的6倍),这样一来,全部的routine都阻塞了,从而致使死锁。

在使用带缓冲的channel时必定要注意放入与取出的速率问题。

 (3)关闭后的channel能够取数据,可是不能放数据。并且,channel在执行了close()后并无真的关闭,channel中的数据所有取走以后才会真正关闭。

package main
func main() {
    ch := make(chan int, 5)
    ch <- 1
    ch <- 1
    close(ch)
    ch <- 1 //不能对关闭的channel执行放入操做
        
        // 会触发panic
}
package main
func main() {
    ch := make(chan int, 5)
    ch <- 1
    ch <- 1
    close(ch)
    <-ch //只要channel还有数据,就可能执行取出操做
 
        //正常结束
}
package main
 
import "fmt"
 
func main() {
    ch := make(chan int, 5)
    ch <- 1
    ch <- 1
    ch <- 1
    ch <- 1
    close(ch)  //若是执行了close()就当即关闭channel的话,下面的循环就不会有任何输出了
    for {
        data, ok := <-ch
        if !ok {
            break
        }
        fmt.Println(data)
    }
    
    // 输出:
    // 1
    // 1
    // 1
    // 1
    // 
    // 调用了close()后,只有channel为空时,channel才会真的关闭
}

 

3、使用channel控制goroutine数量

channel的性质到这里就介绍完了,可是看上去,channel的使用彷佛比WaitGroup要注意更多的细节,那么有什么理由必定要用channel来实现同步呢?channel相比WaitGroup有一个很大的优势,就是channel不只能够实现协程的同步,并且能够控制当前正在运行的goroutine的总数。

下面就介绍几种利用channel控制goroutine数量的方法:

1.若是任务数量是固定的:

ackage main
func Afunction(ch chan int) {
    ch <- 1
}
 
func main() {
    var (
        ch        chan int = make(chan int, 20) //能够同时运行的routine数量为20
        dutycount int      = 500
    )
    for i := 0; i < dutycount; i++ {
        go Afunction(ch)
    }
 
    //知道了任务总量,能够像这样利用固定循环次数的循环检测全部的routine是否工做完毕
    for i := 0; i < dutycount; i++ {
        <-ch
    }
}

2.若是任务的数量不固定

package main
 
import (
    "fmt"
)
 
func Afunction(routineControl chan int, feedback chan string) {
    defer func() {
        <-routineControl
        feedback <- "finish"
    }()
 
    // do some process
    // ...
}
 
func main() {
    var (
        routineCtl chan int    = make(chan int, 20)
        feedback   chan string = make(chan string, 10000)
 
        msg      string
        allwork  int
        finished int
    )
    for i := 0; i < 1000; i++ {
        routineCtl <- 1
        allwork++
        go Afunction(routineCtl, feedback)
    }
 
    for {
        msg = <-feedback
        if msg == "finish" {
            finished++
        }
        if finished == allwork {
            break
        }
    }
}

 

4、不要使用无限循环检查goroutine是否完成工做

 在使用goroutine时,咱们常常会写出这样的代码:

package main
 
import (
    "fmt"
)
 
var (
    flag bool
    str  string
)
 
func foo() {
    flag = true
    str = "setup complete!"
}
 
func main() {
    go foo()
    for !flag {
        //按照咱们的本意,foo()执行完毕后,flag=true,循环就会退出。
        //可是其实这个循环永远都不会退出
    }
    fmt.Println(str)
}

运行以后发现main中的无限循环永远也没法退出,因此Go中不要用这种无限轮询的方式来检查goroutine是否完成了工做。

 

咱们能够经过使用channel,让foo()和main()实现通讯,让foo()执行完毕后经过channel发送一个消息给main(),告诉它本身的事儿完成了,而后main()收到消息后继续执行其余操做:

package main
 
import (
    "fmt"
)
 
var (
    flag bool
    str  string
)
 
func foo(ch chan string) {
    flag = true
    str = "setup complete!"
    ch <- "I'm complete." //foo():个人任务完成了,发个消息给你~
}
 
func main() {
    ch := make(chan string)
    go foo(ch)
    <-ch //main():OK,收到你的消息了~
    for !flag {
    }
    fmt.Println(str)
}

 

 

 

 

 

本文转自:http://blog.csdn.net/gophers/article/details/24665419

相关文章
相关标签/搜索