跳转至

18.goroutine与channel

goroutine

go语言中,有一个称为goroutine的概念,这个概念和其他编程语言中的协程、进程和线程又相似之处,但又有所不同。

goroutine主要用于并发操作。

go语言中并发操作很简单,在要执行的函数或代码前加go关键词就行,其他部分无需修改,这也是go语言的一大优势。例如

go f(<argument>)

要注意,如果main函数结束了,无论goroutine是否结束,都会立即停止程序。

和其他语言一样,go的并发不能保证其执行先后顺序,即使按照特定顺序先后启动goroutine。

Warning

只要goroutine没有返回,其就会一直占用内存等资源。

channel

既然goroutine不能保证这个并发操作在main函数退出时继续,go语言设计了另一个工具,chanel。

channel的概念来源于老式气动管道传输系统。在go语言中,channel用于多个并发操作之间的数据传输。

创建通道

channel := make(chan <dataType>) // datatype为要传输的数据类型

如果不使用make创建通道,通道变量的默认值就是nil

通道中数据的发送与接受

c <- 99 // 向通道发送值
r := <- c // 从通道接受值

当一个goroutine执行了发送操作,其会阻塞,直到这个值被接受。相同,执行接受操作的goroutine也会被阻塞,直到接受到数据。

数据之间的传输与接受是通过同一个channel对象完成的,即这个对象必须传给goroutine,goroutine向其发送或接收,main或者其他goroutine接受或发送。可以看出,channel其实是一个指针变量。

等待接受,直到数秒后

对于网络程序,我们在等待数秒后没有接收到任何响应,就认为其链接超时并主动断开连接。

go语言的标准库提供了处理上述问题的代码。标准库time中的time.After(<time>)会返回一个channel,并在指定时间后,go语言的运行时会向这个channel发送数据。其中传入变量为time为time包内置相关时间变量,如time.Second

光有上述工具还不够,go语言提供了一个名为select的语句。这个语句的写法类似switch,不过每个case上都是一个通道,如果任意一个通道接受到数据,则执行相应case。下面是select语句的示例:

c := make(chan int)
go routineFunction(c)
timeout := time.After(2* time.Second)
select {
    case res := <- c:
        <statement>
    case <- timeout:
        <statement>
}

Warning

select语句如果不包含任何case,其将永远等待下去。

关闭通道

chan对象,go提供了一个函数close(<chanVariable>)。对通道对象执行该函数后,会将通道关闭。对关闭的通道写入数据会引发panic,读取则会获得对应数据类型的零值。

那么如果检查通道是否关闭呢?可以使用如下代码:

res, ok := <- c

okFalse则代表通道关闭了。

range遍历通道变量

通道变量还可以使用range变量,其结果是直到通道被关闭前的所有输入值。

互斥锁

为了保证各goroutine不会同时访问(主要是写入)某一个共享值而引发错误,go语言中有一个称为互斥锁的概念。

互斥锁的实现在sync包中,通过类型sync.Mutex实现。

上锁与解锁

var mu sync.Mute
func main()
{
    mu.Lock()
    defer mu.Unlock()
}

解锁一般使用defer来实现。互斥锁通常在同一包中声明并使用。

当一个goroutine尝试对互斥锁对象执行上锁操作时,如果这个变量已经被上锁,则当前goroutine会被阻塞,直到其解锁。依据这个原理,从而实现了共享资源的安全访问。

通常,将一个互斥锁对象放在结构体中,组成一个共享资源对象。