• 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
    "))
            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。

    通过 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也称之为同步channel

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

    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还会触发一个广播机制。

    因为关闭操作只用于断言不再向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也是不现实的,因为这要求在执行任何接收操作之前缓存所有已经发送的值。如果未能分配足够的缓冲将导致程序死锁。

     

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

     

    我的理解是,当通信双方的工作效率接近时,缓存才有意义,如果一方处理过快,而另一方过慢,则效率由短板决定,缓存并不能起到提升效率的作用。

     

     

  • 相关阅读:
    常见hash算法的原理
    【学习干货】给coder的10个读书建议
    htc one x刷机记录
    Linux 搭建SVN server
    javascript
    USACO comehome Dijkstra
    当设计师、产品经理和程序员去交友站点找女朋友
    Spring3.0 AOP 具体解释
    慕课网Hibernate初探之一对多映射实验及总结
    C++数组引用
  • 原文地址:https://www.cnblogs.com/tianyajuanke/p/5327309.html
Copyright © 2020-2023  润新知