• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

一天搞懂Go语言(5)——goroutine和通道

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

  并发编程表现为程序由若干个自主的活动单元组成。go有两种并发编程风格,一种是goroutine和通道,它们支持通信顺序进程(CSP),CSP是一个并发模式,在不同的执行体(goroutine)之间传递值,但是变量本身局限于单一的执行体。还有一种共享内存多线程的传统模型,它们和在其他主流语言中使用线程类似。

goroutine

  在go里面,每一个并发执行的活动称为goroutine,类似于线程。当一个程序启动时候,只有一个goroutine来调用main函数,称为主goroutine,新的goroutine通过go语句进行创建。

f() //调用f(),等待它返回
go f() //新建一个调用f()的grouting,不用等待

   除了从main返回或者退出程序外,没有程序化的方法让一个goroutine来停止另一个,但是有办法和goroutine通信来要求它自己停止。

示例

并发时钟服务器

package main

import(
	"io"
	"log"
	"net"
	"time"
)

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

func main()  {
	listener,err:=net.Listen("tcp","localhost:8080")
	if err!=nil{
		log.Fatal(err)
	}
	for{
		conn,err:=listener.Accept() //阻塞,直到有连接请求进来
		if err!=nil{
			log.Print(err) //连接中止
			continue
		}
		handleConn(conn) //一次处理一个连接
	}
}

  当运行这段程序,使用nc命令连接本地8080端口时候,客户端会显示每秒从服务器发送的时间。使用killall clock1来终止指定名字的进程

  当第二个客户端需要链接进来的时候必须等第一个客户端结束,因为服务器是顺序的,一次只能处理一个客户请求。让服务器支持并发只需要一个很小的改变:在调用handleConn的地方添加一个go关键字,使它在自己的goroutine内执行。

for{
	conn,err:=listener.Accept() //阻塞,直到有连接请求进来
	if err!=nil{
		log.Print(err) //连接中止
		continue
	}
	go handleConn(conn) //并发处理连接
}

   现在多个客户端可以同时接收到时间。

通道

  如果说goroutine是Go程序并发的执行体,通道就是它们之间的连接,每一个通道是一个具体类型的导管,叫做通道的元素类型。

ch := make(chan int) //ch类型是‘chan int’ ,创建一个无缓冲通道
ch := make(chan int,3) //容量为3的缓冲通道

   像map一样,通道是一个使用make创建的数据结构的引用。和其他引用类型一样,零值为nil。通道由主要两个操作,发送和接收。

ch <- x //发送语句
x = <-ch //接收语句
<-ch //接收,丢弃结果

//第三个操作close,将设置一个标志位来指示这个通道后面没有值了;
close(ch)

   关闭后的发送操作将导致宕机,而接收操作将获取所有已发送的值。

  使用make创建的通道叫做无缓冲通道,后面还有一个可选参数表示通道的容量,如果为0(默认值),则创建一个无缓冲通道

 无缓冲通道

  无缓冲通道,无论是发送操作还是接收操作都会阻塞,直到一方发送完毕或接收完毕。这样会导致两个goroutine同步化,因此无缓冲通道也叫同步通道。通过通道发送消息有一个值,有时候通信本身以及通信发生的时间也很重要,当我们强调这方面的时候,往往把消息称作事件。当事件没有携带额外消息,只是单纯的同步,我们通过使用一个struct{}元素类型通道强调它,尽管通常使用bool或int。

管道

  多个goroutine通过通道连接起来,一个输出是另一个的输入,就组成了管道。下面是三个goroutine,产生整数、求平方、输出。

package main

import "fmt"

func main()  {
	naturals:=make(chan int)
	squares:=make(chan int)
	
	//counter
	go func() {
		for x:=0; ;x++{
			naturals <- x
		}
	}()
	
	//squares
	go func() {
		for{
			x:=<-naturals
			squares<-x*x
		}
	}()
	
	//printer
	for{
		fmt.Println(<-squares)
	}
}

   如果发送方没有数据要发送,我们可以通过close关闭,所有发送操作会宕机,后续接收操作会获取零值。没有一个直接方式来判断通道是否关闭,但是有一个接收操作的变种,他产生两个结果:接收到的通道元素以及一个布尔值:true表示成功接收,false表示通道关闭。

//squares
go func() {
	for{
		x,ok:=<-naturals
		if !ok{
			break //通道关闭
		}
		squares<-x*x
	}
	close(squares)//通道读完关闭
}()

   更为方便的是该语言提供了range循环语法以在通道上迭代,接受完最后一个值后关闭循环。

func main()  {
	naturals:=make(chan int)
	squares:=make(chan int)

	//counter
	go func() {
		for x:=0; ;x++{
			naturals <- x
		}
		close(naturals)
	}()

	//squares
	go func() {
		for x:=range naturals{
			squares<-x*x
		}
		close(squares)//通道读完关闭
	}()

	//printer
	for x:=range squares{
		fmt.Println(x)
	}
}

   结束时关闭每一个通道不是必须的,因为go语言可以通过垃圾回收器根据它是否可以访问来决定是否回收它,而不是根据它是否关闭。(和文件close操作不一样,关闭文件是必须的)。

单向通道

  当一个通道用做函数的形参时,它几乎总是被有意地限制不能发送或接收。

chan<-int //只能发送的通道
<-chan int //只能接收的通道

   因为close操作仅仅在发送方才能调用,所以试图关闭一个仅能接收的通道在编译时会报错

 

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

func main()  {
	naturals:=make(chan int)
	go counter(naturals)
//...
}

   在任何赋值操作中,将双向通道转换为单向通道都是允许的,但反过来不行

缓冲通道

  缓冲通道使得发送方可以无阻塞的发送缓冲容量大小的数据,接收方相反。所以当缓冲通道满,发送操作会阻塞,缓冲通道空,接收操作会阻塞。 

ch := make(chan int,3)

cap(ch) //获取通道容量
len(ch) //获取通道元素个数

  向一个没有goroutine在接收的通道上不断发送会发生goroutine泄漏。

select多路复用

  select是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。select随机执行一个可运行的case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行。

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}
  • 相关语法:
    • 每个case都必须是一个通信
    • 所有channel表达式都会被求值
    • 所有被发送的表达式都会被求值
    • 如果任意某个通信可以进行,它就执行,其他被忽略
    • 如果有多个case都可以运行,select 会随机公平地选出一个执行。其他不会执行
    • 如果没有通道可以执行,则:
      • 如果有default子句,则执行该语句
      • 如果没有default子句,select 将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值

取消

  有时候我们需要让一个goroutine停止它当前的任务,一个goroutine无法直接终止另一个,因为这样会让所有的共享变量状态处于不确定状态。

  当一个通道关闭且已取完所有发送的值后,接下来的接收操作会立即返回,得到零值。我们可以利用它创建一个广播机制:不在通道上发送值,而是关闭它

 


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
[日常]Go语言圣经--接口约定习题2发布时间:2022-07-10
下一篇:
go实现简单的加权分配发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap