zoukankan      html  css  js  c++  java
  • 一文读懂goroutine和channel

    开源库「go home」聚焦Go语言技术栈与面试题,以协助Gopher登上更大的舞台,欢迎go home~

    背景介绍

    大家都知道进程是操作系统资源分配的基本单位,有独立的内存空间,线程可以共享同一个进程的内存空间,所以线程相对轻量,上下文切换开销也小。虽然线程已经比较轻量了,但还是占近1M的内存,而今天介绍的有“轻量级线程”之称的Goroutine,可以小至几十K甚至几K,切换的开销更小。

    除此之外,在传统Socket编程时,需要维护一个线程池来为每个Socket收发包分配线程,而且需要将CPU与线程数建立对应关系,确保每个任务都能被及时分配给CPU,而Go程序可以智能地将goroutine中的任务分配到CPU。

    如何使用

    我们现在假设一个场景,你是一家公司老总,每天要花两小时处理邮件,六小时开会,那么,程序可以这样编写。

    func main() {
    
    	time.Sleep(time.Hour * 2) //处理邮件
    	time.Sleep(time.Hour * 6) //开会
    
    	fmt.Println("工作完成了")
    }
    

    运行一下

    file

    果然,要8小时才能完成工作,那么怎么简化工作呢?没错,请一个助理小姐姐帮忙,就让她来处理邮件,这样你就只需6小时开会就行了。

    file

    开始写代码吧,先来定义一个助理函数。

    func assistant() {
    	time.Sleep(time.Hour * 2)
    }
    

    然后在主函数用一条神器的命令go调用它,这样助理的耗时久不再占用你的时间了。

    func main() {
    
    	go assistant()
    
    	time.Sleep(time.Hour * 6) //开会
    
    	fmt.Println("工作完成了")
    }
    

    运行一下

    file

    真的只花了六个小时就完成工作了。各位看官们,看到没,这就是协程,只需要go命令加上函数名,就这么简单。

    file

    但是我知道勤奋的你是不会满足于现状的。

    匿名函数

    另外,既然goroutine支持普通函数,当然也就支持匿名函数。

    go func() {
      time.Sleep(time.Hour * 2)
    }()
    

    协程间如何通讯

    虽然我们可以轻松地创建一堆协程,但是不能通信的协程是没有灵魂的。假如助理正在帮你处理邮件时,你突然想请她喝奶茶,那是不是要通知她?

    file

    那怎么通知呢?这就引出了大名鼎鼎的channel,汉译“通道”,顾名思义它的作用就是在协程之间建立通道,一端可以将数据源源不断地传送到通道的另一端。

    file

    而声明方式也非常简单,只需要make一下。拿下方代码为例,它代表初始化一个通道类型变量,并且通道里只能存放string类型的数据。

    ch := make(chan string)
    

    初始化完成后,要想与协程函数建立连接,得先把chan变量传给协程函数。

    go assistant(ch)
    

    当然,协程函数要能接收chan才行,我们纵深到函数内部,看看都干了些什么。

    func assistant(ch chan string) {
    
    	go func() {
    		for {
    			fmt.Println("看了一封邮件")
    			time.Sleep(time.Second * 1)
    		}
    	}()
    
    	msg := <-ch
    	if msg == "喝奶茶去呗" {
    		ch <- "好啊"
    	}
    }
    

    函数内部又起了一个协程专门处理邮件,同时另外一边等待老板通知。细心的你应该看出如何取通道数据了,没错,只需要在通道变量前加上<-符号就可以将值取出,同样的,符号加在后面就是往通道塞数据。

    ch <- "pingyeaa"
    <- ch
    

    file

    如果通道没有数据,消费端就会一直阻塞,直到有数据为止。当然编译器是很聪明的,在编译的时候如果发现没有地方往通道里塞数据,它就会panic,提示死锁。

    fatal error: all goroutines are asleep - deadlock!
    

    继续来看代码,大致意思就是老板如果发“喝奶茶去呗”,就返回“好啊”,因为通道里一开始是没数据的,所以该协程会一直阻塞,直到主函数往通道中写入了消息。

    现在来看下主函数的实现逻辑,声明通道和传入通道变量就不再赘述了,我们只需要等待5秒钟之后往通道里写入喝奶茶消息即可。因为刚才assistant协程接收到消息后会往ch写入“好啊”消息,所以主函数在发完请求之后应该再读取从助理那边传递来的消息。

    ch := make(chan string)
    
    go assistant(ch)
    
    time.Sleep(time.Second * 5)
    ch <- "喝奶茶去呗"
    
    resp := <-ch
    fmt.Println(resp)
    

    同样,主函数的<-ch也会一直阻塞,直到助理回复消息。另外有两点需要注意,第一,如果main函数赶在goroutine之前执行完毕,那么goroutine也会销毁;第二,main也是goroutine。

    最后,关闭通道,其实通道关闭不是必须的,它与文件不同,如果没有goroutine使用到channel,就会自动销毁,而close的作用是用来通知通道的另一端不再发送消息了,另一端可以通过<-ch的第二个参数来获取通道关闭情况。

    close(ch)
    
    data, ok := <-ch
    

    通道的多路复用select

    刚才的示例中的<-ch只能读取通道的一条消息,如果通道里不止一条消息,该怎么读取呢?

    file

    应该很多同学跟我一样想到的是遍历,没错,遍历确实可以拿到通道数据。

    for {
      fmt.Println(<-ch)
    }
    

    也可以这么遍历。

    for d := range ch {
      fmt.Println(d)
    }
    

    但是,如果需要同时接收多个通道数据该怎么办?循环中接收两个通道变量?

    for {
      data, ok := <-ch1
      data, ok := <-ch2
    }
    

    这种方式虽然可以取出数据,但是性能较差,官方给我们提供的select关键词就是专门用来解决多通道数据读取问题的,语法与switch非常相似。select会将多个通道传来的数据分发到不同的处理逻辑中。

    func main() {
    
    	ch1 := make(chan int)
    	ch2 := make(chan int)
    
    	go func() {
    		for {
    			select {
    			case d := <-ch1:
    				fmt.Println("ch1", d)
    			case d := <-ch2:
    				fmt.Println("ch2", d)
    			}
    		}
    	}()
    
    	ch1 <- 1
    	ch1 <- 2
    	ch2 <- 2
    	ch1 <- 3
    }
    

    模拟超时

    除此之外,有些情况下我们不希望通道阻塞太久,假设5秒钟还取不出通道的数据,就超时退出,那我们可以使用time.After方法来实现。time.After会返回一个通道类型,它的作用是传入一个目标时间(比如5s),我们在5秒后就可以通过通道获取预设置的超时通知,这样就达到了定时器的目的。

    func main() {
    
    	ch1 := make(chan int)
    	ch2 := make(chan int)
    
    	go func() {
    		for {
    			select {
    			case d := <-ch1:
    				fmt.Println("ch1", d)
    			case d := <-ch2:
    				fmt.Println("ch2", d)
    			case <-time.After(time.Second * 5):
    				fmt.Println("接收超时")
    			}
    		}
    	}()
    
    	time.Sleep(time.Second * 6)
    }
    

    通道关闭延伸阅读

    已关闭的通道再发送数据会触发panic

    ch := make(chan int)
    close(ch)
    ch <- 1
    
    panic: send on closed channel
    

    通道设置长度

    可以通过make方法设置通道长度,作为缓冲区,通道满时生产者端会阻塞,通道取空后消费端会阻塞。

    ch := make(chan int, 3)
    
    ch <- 1
    ch <- 2
    ch <- 2
    ch <- 2
    
    fmt.Println(len(ch))
    

    已关闭的通道依然可以读取数据

    ch := make(chan int, 3)
    
    ch <- 1
    ch <- 2
    ch <- 2
    
    close(ch)
    
    for d := range ch {
      fmt.Println(d)
    }
    

    感谢大家的观看,如果觉得文章对你有所帮助,欢迎关注公众号「平也」,聚焦Go语言与技术原理。
    关注我

  • 相关阅读:
    Ambiguous mapping. Cannot map 'labelInfoController' method
    在写ssh项目时浏览器页面出现http status 404 – not found
    JS页面出现Uncaught SyntaxError: Unexpected token < 错误
    Data truncation: Truncated incorrect DOUBLE value:
    个人最终总结
    结对编程--黄金点游戏
    Windows操作系统----锁住命令行窗口
    Windows操作系统下给文件夹右键命令菜单添加启动命令行的选项
    命令行下运行 java someClass.class出现 “错误:找不到或无法加载主类someClass ” 的解决方案
    Qt Quick程序的发布
  • 原文地址:https://www.cnblogs.com/pingyeaa/p/12699543.html
Copyright © 2011-2022 走看看