golang笔记——并发

  go语言中的main函数也是运行在一个单独的goroutine中的,通常称为 main goroutine,main函数结束时,会打断其它 goroutine 的执行,可是其它 goroutine 不会打断其它的 goroutine 的执行,除非是经过通讯让对方自行停止。缓存

  先来看一个最简单的并发例子,一个时间服务器:(偏题了,不该该使用这个例子,应该突出重点,这个例子能够放到tcp那节)服务器

func main() {
    listener, err := net.Listen("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }   
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Print(err)
            continue
        }   
        go handleConn(conn)
    }   
}

func handleConn(c net.Conn) {
    defer c.Close()
    for {
        _, err := io.WriteString(c, time.Now().Format("2006/01/02 15:04:05\n"))
        if err != nil {
            return
        }   
        time.Sleep(1 * time.Second)
    }   
}

  从这个例子能够看出,使用go开启一个tcp服务很是简洁,此外从该例子中,咱们能够顺便了解一下go中格式化日期和时间的方式是很是奇葩的,格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700,能够这样记:1月2日下午3点4分5秒(200)6年UTC-0700。数据结构

  能够用 nc 或 telnet 来链接这个tcp服务进行测试, 也可使用go实现一个简单的客户端:并发

func main() {
    conn, err := net.Dial("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    mustCopy(os.Stdout, conn)
}

func mustCopy(dst io.Writer, src io.Reader) {
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatal(err)
    }
}

 

goroutine是经过 channel 来通讯的, 包括发送和接收两种操做。能够经过 make 来声明一个channel 如 ch = make(chan int),和 map 相似,channel 也只是对应底层数据结构的引用,因此发送方和接收方都将引用同一份对象,channel是引用类型,因此它的零值是 nil。channel 使用的操做符是 <- ,发送操做是指将数据发送到 channel,接收是指从 channel中接收。channel相似于通常的io系统,能够经过 close(ch) 来关闭一个 channel,关闭后,将不能进行发送操做,但能够接收以前未接收完的数据,若是没有数据可接收,则接收到一个nil。tcp

 

经过 make(chan int) 这种方式建立的 channel 称之为无缓存channel,表示 channel的容量是0,若是要声明一个有缓存的channel,可使用 make(chan int, 100) ,第二个参数表示初始化时的channel容量大小。函数

一个基于无缓存Channels的发送操做将致使发送者goroutine阻塞,直到另外一个goroutine在相同的Channels上执行接收操做,当发送的值经过Channels成功传输以后,两个goroutine能够继续执行后面的语句。反之,若是接收操做先发生,那么接收者goroutine也将阻塞,直到有另外一个goroutine在相同的Channels上执行发送操做。测试

简单的说,就是使用无缓存channel时,一旦通讯发起,必需要等到通讯完成才能够继续执行,不然将阻塞等待。这也就意味着,无缓存channel在通讯时,会强制进行一次同步操做,因此无缓存channel也称之为同步channelspa

下面是一个串联channel的例子:code

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        for x := 1; x <= 10; x++ {
            ch1 <- x
        }   
        close(ch1)
    }() 

    go func() {
        for x := range ch1 {
            ch2 <- x * x 
        }   
        close(ch2)
    }() 

    for x := range ch2 {
        fmt.Println(x)
    }   
}

无论一个channel是否被关闭,当它没有被引用时将会被Go语言的垃圾自动回收器回收。(不要将关闭一个打开文件的操做和关闭一个channel操做混淆。对于每一个打开的文件,都须要在不使用的使用调用对应的Close方法来关闭文件。)试图重复关闭一个channel或者关闭一个nil值的channel都将致使panic异常,此外关闭一个channels还会触发一个广播机制。orm

由于关闭操做只用于断言再也不向channel发送新的数据,因此只有在发送者所在的goroutine才会调用close函数,所以对一个只接收的channel调用close将是一个编译错误。

单方向channel,只发送或只接收,功能明确。类型chan<- int表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int表示一个只接收int的channel,只能接收不能发送。(箭头<-和关键字chan的相对位置代表了channel的方向。)这种限制将在编译期检测。

func counter(out chan<- int) {
    for x := 0; x < 100; x++ {
        out <- x
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for v := range in {
        out <- v * v
    }
    close(out)
}

func printer(in <-chan int) {
    for v := range in {
        fmt.Println(v)
    }
}

func main() {
    naturals := make(chan int)
    squares := make(chan int)
    go counter(naturals)
    go squarer(squares, naturals)
    printer(squares)
}

 

带缓存的 channel,内部持有一个元素队列。队列的最大容量是在调用make函数建立channel时经过第二个参数指定的。向缓存Channel的发送操做就是向内部缓存队列的尾部插入缘由,接收操做则是从队列的头部删除元素。若是内部缓存队列是满的,那么发送操做将阻塞直到因另外一个goroutine执行接收操做而释放了新的队列空间。相反,若是channel是空的,接收操做将阻塞直到有另外一个goroutine执行发送操做而向队列插入元素。

内置的 cap 函数能够查看某个 channel 的容量,len 函数能够查看某个 channel 的当前缓存队列中有效元素的个数。

 

若是咱们使用了无缓存的channel,那么两个慢的goroutines将会由于没有人接收而被永远卡住。这种状况,称为goroutines泄漏,这将是一个BUG。和垃圾变量不一样,泄漏的goroutines并不会被自动回收,所以确保每一个再也不须要的goroutine能正常退出是重要的。

关于无缓存或带缓存channels之间的选择,或者是带缓存channels的容量大小的选择,均可能影响程序的正确性。无缓存channel更强地保证了每一个发送操做与相应的同步接收操做;可是对于带缓存channel,这些操做是解耦的。一样,即便咱们知道将要发送到一个channel的信息的数量上限,建立一个对应容量大小带缓存channel也是不现实的,由于这要求在执行任何接收操做以前缓存全部已经发送的值。若是未能分配足够的缓冲将致使程序死锁。

 

若是生产线的前期阶段一直快于后续阶段,那么它们之间的缓存在大部分时间都将是满的。相反,若是后续阶段比前期阶段更快,那么它们之间的缓存在大部分时间都将是空的。对于这类场景,额外的缓存并无带来任何好处。

 

个人理解是,当通讯双方的工做效率接近时,缓存才有意义,若是一方处理过快,而另外一方过慢,则效率由短板决定,缓存并不能起到提高效率的做用。

相关文章
相关标签/搜索