详解golang中的Goroutines和Channels

Goroutines

在go语言中,每一个并发执行的单元都叫做goroutine,类似其他语言中并发执行的最小单位——线程,它与线程的区别将在本文后面进行解释。

当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。go语句会使其语句中的函数在一个新创建的goroutine中运行。而go语句本身会迅速地完成。

主函数返回时,所有的goroutine都会被直接打断,程序退出。除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个goroutine来打断另一个的执行,但是之后可以看到一种方式来实现这个目的,通过goroutine之间的通信来让一个goroutine请求其它的goroutine,并让被请求的goroutine自行结束执行。

并发clock测试

这里实现一个顺序执行的时钟服务器,它会每隔一秒钟将当前时间写到客户端:

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

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)
	}
}

当前程序只允许一个客户端进行访问,其他客户端连接必须阻塞等待前的连接结束。

使用nc连接测试如下:

使用go语句进行并发处理:

for {
		conn,err := listener.Accept()
		if err != nil {
			log.Fatal(err)
			continue
		}
		go handleConn(conn)
	}

效果如下:

客户端即可进行并发访问。

Channel

goroutine作为Go语言中的并发单元,那么不同的goroutine之间需要传递信息的媒介,而channel,就是goroutine之间的通信机制,

一个channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。channel的零值同样是 nil。

使用内置的make函数,我们可以创建一个channel:

ch := make(chan int) // ch has type 'chan int'

 channel的操作主要有发送以及接收,在接收语句中,<-运算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。

ch <- x  // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch     // a receive statement; result is discarded

Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。

以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。

ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

不带缓存的Channels 

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作。

因此,不带缓存的channel可以用来进行两个goroutine之间的同步。

串联的Channels(Pipeline)

Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。下面的程序用两个channels将三个goroutine串联起来。

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

	go func() {
		for i:=0; ;i++ {
			naturals <- i
		}
	}()

	go func() {
		for {
			n := <- naturals
			squares <- n * n
		}
	}()

	for {
		fmt.Println(<-squares)
	}
}

在下面的改进中,我们的计数器goroutine只生成100个含数字的序列,然后关闭naturals对应的channel,这将导致计算平方数的squarer对应的goroutine可以正常终止循环并关闭squares对应的channel。(在一个更复杂的程序中,可以通过defer语句关闭对应的channel。)最后,主goroutine也可以正常终止循环并退出程序。

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

	go func() {
		for i:=0;i<100;i++ {
			naturals <- i
		}
		close(naturals)
	}()

	go func() {
		for n:= range naturals{
			squares <- n * n
		}
		close(squares)
	}()

	for {
		for n:= range squares{
			fmt.Println(n)
		}
	}
}

带缓存的Channels

带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的。下面的语句创建了一个可以持有三个字符串元素的带缓存Channel。

ch = make(chan string, 3)

向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。

 

你可能感兴趣的:(go语言学习)