go中的goroutine和channel

golang在语言层面上就提供了对并发的支持,通过goroutine和实现goroutine间通信的channel

进程,线程,协程与并行,并发 这篇文章对几个概念讲述得很好。

goroutine本质上就是协程,但它又不太一样:

1.go的运行时自带一个goroutine调度器来调度已创建的goroutine协程。而一般意义上的协程,对共同依赖的线程的控制权是在各自的代码中由开发者自己定义的,比如python和ES6,可以通过yield关键字,在执行某个异步任务后,把控制权交回给执行栈。等异步的结果返回,又把控制权交给异步函数处理返回,任务是在单个线程上调度的。

2.协程同一时刻只能有一个在跑,但是goroutine可以实现并行,就是说多个goroutine可以同时在多个进程跑。

3.goroutine之间的通信通过channel实现。

由于goroutine运行在同一个地址空间,访问共享内存时要遵循同步原则。

一个官方示例:

package main
import (
	"fmt"
	"time"
)
func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}
func main() {
//go关键字用于创建一个新的goroutine
//say("hello")在当前主goroutine中执行
//say("world")在另一个goroutine中执行
	go say("world")  
	say("hello")
}

这个例子中time.Sleep用于挂起当前goroutine,time.Sleep是交出控制权的一种方式。。

如果把say("hello")注释掉,程序不会打印字符串,因为线程一直在当前主goroutine中,执行完main函数后就退出了,其他协程还没来得及执行。golang之并发编程这篇文章对此做了讨论。

如果把say("hello")换成time.Sleep(300 * time.Millisecond),程序会打印三次"world",因为主goroutine歇了300ms,这段时间交给了执行say("world")的goroutine。

channel

go提供了channel这个概念,顾名思义,作为信道,来处理goroutine之间的通信。在实现上,就是声明一个chan type,来存放要接收或发送的数据。

ch := make(chan int)
//箭头方向表示数据传输方向
ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and
           // assign value to v.

引用A Tour of Go中示例:

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	//fmt.Println(sum)
	c <- sum // send sum to c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c
	fmt.Println(x, y, x+y)
}

在这个最基本的例子中,我们可看到:

1.channel实例c被传给sum。sum内部通过c<-value文法将数据发送给c。

2.在主goroutine中,通过value := <-c 文法,接收其他协程送来的数据。

多次执行代码,会输出 '17 -5 12' 或  '-5 17 12'。可见goroutine在执行时,顺序方面是相当随机的。

接下来我们可以做一些试验来验证一些东西:

试验一:将主函数中获取channel数据的那行移个位置:

    go sum(s[:len(s)/2], c)
    x, y := <-c, <-c // receive from c
    go sum(s[len(s)/2:], c)

改成这样之后,再运行代码,报错:

fatal error: all goroutines are asleep - deadlock!

这是因为执行到x, y := <-c, <-c这行代码时,程序要从channel c中读取两个数据x,y。可是执行完第一个go sum()之后channel c中只写入了一个数据x,而此时并没有其他goroutine在活动。也就是说,channel c中 第二个位置的数据永远不会到来,程序不可能一直这样等下去,go直接弹出错误,视之为死锁了。

不过如果我们在sum函数中添加一行,给channel c写入一个数值:

func sum(s []int, c chan int) {
    ...
    c <- sum // send sum to c
    c <- 1
}

再运行,程序正常执行,并打印出:17 1 18

因为channel c 中第一和第二个位置已经有数据,不会“等一个永远不会来的数据”,自然不会死锁。

试验二:在sum函数中添加一行time.Sleep(1000 * time.Millisecond)

func sum(s []int, c chan int) {
    ...
    fmt.Println(sum)
    time.Sleep(1000 * time.Millisecond)
    fmt.Println(sum)
    c <- sum // send sum to c
}

程序不会报错,只是main函数的fmt.Println延时了约1秒才打印预期的结果。也就是说,channel遵循同步的思想,只要程序中还有goroutine为结束,它就会一直等到预期的数据被写入,才执行并赋值给x,y。然后继续执行后边的代码。

buffered channel

上面的例子我们看到,channel是会阻塞的。对于开发者而言,channel就像一个管道、或者传送带,发送者在另一段塞入东西,接收者在这一端拿出来。如果接收者这边需要一次性拿到3个数据,只能等发送者那边塞完三个数据,再一次性拿出来。

我做了一翻试验,琢磨了好阵子,发现通过make(chan int)声明channel时,默认地,当发送者塞入一个数据后,goroutine会自动交出执行权,等其他goroutine拿出这个数据。

比如发送者goroutine是A,接收者goroutine是B。

在"A一次只塞入一个数据,B一次只拿出一个数据"的设定下,A和B轮番切换执行。

在"A一次只塞入一个数据,B一次拿出三个数据"的设定下,A第一次塞入数据后,切换到B,发现B要一次性拿三个数据,就切换到A,直到A塞满3个数据,再回到B拿出数据。

也就是说,goroutine会因为channel而阻塞。如果channel中没有位置放数据了,发送者会交出控制权,直到channel中有位置放数据才继续执行。如果channel中没有数据,接收者会交出执行权,直到channel中有东西可以拿,才继续往下执行。

我们可以在make一个channel的时候,传入第二个参数:channel可以额外缓存多少个数据:

chanMessage := make(chan string,2)

这时候,发送者在可以额外塞入2个字符串,才交出执行权。

遍历channel中的数据:

for i := range ch   //i是channel ch中的每个数据

select语句:

在并发编程时,往往要顾及到多个协程的通信,go可以通过select语句管理多个channel的通信:

//主函数调用,给其他协程传送数据
func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:  //如果channel c的数据被其他协程取走,继续放下一个数字
            x, y = y, x+y
        case <-quit:  //如果其他协程返回一个quit值,打印"quit"
            fmt.Println("quit")
            return
        default:      //所有case都不满足,执行default
            ...
        }
    }
} 

func main() {
//声明两个channel 实例:c用来传送数据,quit用来传输"结束通信"的标记
	c := make(chan int)
	quit := make(chan int)
//创建一个匿名函数goroutine,打印主函数传过来的数据
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
//接收完10个数据后,回传一个quit值给主函数
		quit <- 0
	}()
	fibonacci(c, quit)
}

mutex:

由于协程间共享内存资源,对资源的访问应该是互斥的,以避免冲突,go标准库sync包提供了一种数据结构--mutex。

sync.mutex提供了两个方法:Lock()和unlock()

我们可以在一个代码块前后分别执行这两个方法,让这段代码中的变量在执行期间遵循互斥原则:

import (
    "fmt"
    "sync"
    ...
)
type SafeCounter struct {
	v   map[string]int //存放计数值
	mux sync.Mutex     //mux是个固定的symc.Mutex
}
//该函数被调用时被锁定,一个时间点内只有一个goroutine可以访问c.v
func (c *SafeCounter) Inc(key string) {
	c.mux.Lock()
	c.v[key]++
	c.mux.Unlock()
}

关闭信道

发送者可以执行下边语句关闭信道,表示不会再发送数据。

close(ch) //c是channel实例

接收者可以通过下面语句判断信道是否被关闭:

v, ok := <-ch

ok为false表示信道已被关闭。

>>>>>>>>>

初学这方面时看到一些不错的分享和讨论:

Golang 的 goroutine 是如何实现的?

Golang 之协程详解

GoLang学习 -- goroutine使用指南

原理解Go的Goroutine和channel

协程的好处有哪些?

理解Goroutine

猜你喜欢

转载自blog.csdn.net/weixin_36094484/article/details/82289525