并发目前来看比较主流的就三种:
在go里面,使用goroutine进行并发操作。
goroutine的本质是协程2,也可以认为是轻量级的线程,与创建线程相比,创建成本和开销都很小。同时,Golang 在 runtime、系统调用等多方面对 goroutine 调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前 goroutine 的CPU § 转让出去,让其他 goroutine 能被调度并执行。
Golang 从语言层面支持了协程,从语言层面支持了高并发。
(这儿不会对语法有过多的说明,大家请去官网查看,官网的教程非常详细,远比其他网页上的资料更全面)
当一个程序启动的时候,只有一个goroutine来调用main函数,称它为主goroutine。新的goroutine通过在函数或者方法前面加上关键字go进行创建。
package main
import (
"fmt"
"time"
)
func Hello() {
fmt.Println("Hello")
}
func main() {
go Hello() // 开启一个新的goroutine
time.Sleep(1*time.Second) // 大家可以把这个去掉看看会出现什么结果
fmt.Println("World")
}
/*
Hello
World
*/
Go语言中没有任何显示方法可以从外部终止一个 goroutine 的执行。这儿说明一下Goroutine的终止情况。
之所以这么设计有很多原因。我目前理解地也不是很深刻,简单说一些我认为的原因:
对于杀死的协程占有的资源,需要进行释放与其他管理;
对于杀死的协程占有的锁,需要进行拆除;
禁止之后需要程序员更加注意考虑协程的开始与结束。
线程的栈空间是固定分配的,虽然区别于不同的系统会有不同的大小,但是基本都是2MB。这个栈用于保存局部变量,用于在函数切换时使用。
对于goroutine这种轻量级的协程来说,一个大小固定的栈可能会导致资源浪费:比如一个协程里面只print了一个语句,那么栈基本没怎么用。当然,也有可能嵌套调用很深,那么可能也不够用。
所以go采用了动态扩张收缩的策略:初始化为2KB,最大可扩张到1GB。
每个线程都有一个id,这个在线程创建时就会返回,所以可以很方便的通过id操作某个线程。
但是在goroutine内没有这个概念,这个是go语言设计之初考虑的,防止被滥用,所以你不能在一个协程中杀死另外一个协程,编码时需要考虑到协程什么时候创建,什么时候释放。
GOMAXPROCS用于设置上下文个数,这个上下文用于协程间的切换,默认值是CPU的个数,也就是说这个个数是指定同时执行协程的内核线程数,即,用户线程(协程)与内核线程的数量对应关系是1:GOMAXPROCS的5(这儿说的1是一个虚指,若有多个协程同时开启,则对应是M:N,如下例子)。
for {
go fmt.Print(0)
fmt.Print(1)
}
/*
$ GOMAXPROCS=1 go run example.go
11111111111111111100000000000000000000111111111...
$ GOMAXPROCS=2 go run example.go
01010101010010101001110010101010010101010101010...
*/
第一次执行语句指定只启动一个上下文,那么由于是2个协程映射到1个内核线程,那么1次只能跑一个协程,所以会跑一段时间再进行切换(由调度器进行判断什么时候切换,而不是内核)。第二次启动二个上下文,2个协程映射到2个内核线程,那么同一时间有2个干活的内核线程,所以能看到0和1交替打印,也就是说,此时真正实现了并发。
如果说goroutine是Go并发的执行体,那么 Channel 就是他们之间的连接。
Channel是可以让一个goroutine发送特定的值到另外一个goroutine的通信机制。
var ch chan int // 声明一个传递int类型的channel
ch := make(chan int) // 使用内置函数make()定义一个channel
//=========
ch <- value // 将一个数据value写入至channel,这会导致阻塞
// 直到有其他goroutine从这个channel中读取数据
value := <-ch // 从channel中读取数据,如果channel之前没有写入数据
// 也会导致阻塞,直到channel中被写入数据为止
//=========
close(ch) // 关闭channel
默认的channel是阻塞的。
无缓冲通道上的发送操作将会被阻塞,直到另一个goroutine在对应的通道上执行接收操作,此时值才传送完成,两个goroutine都继续执行。
package main
import (
"fmt"
"time"
)
var done chan string
func Hello() {
fmt.Println("Hello")
time.Sleep(1*time.Second)
done <- "World"
}
func main() {
done = make(chan string) // 创建一个channel
go Hello()
fmt.Println(<-done)
}
/*
Hello
World
*/
可以参考上文不用channel的代码,我们在main中使用了sleep来阻止main goroutine过早结束。而这儿就算main goroutine执行到了最后一行,因为信道阻塞的缘故,只有在Hello goroutine运行到往信道里输入一个东西之后,它才会继续工作。
通道可以用来连接goroutine,这样一个的输出是另一个输入。这就叫做管道。
package main
import (
"fmt"
"time"
)
var echo chan string
var receive chan string
// 定义goroutine 1
func Echo() {
time.Sleep(1*time.Second)
echo <- "Hello World"
}
// 定义goroutine 2
func Receive() {
temp := <- echo // 阻塞等待echo的通道的返回
receive <- temp
}
func main() {
echo = make(chan string)
receive = make(chan string)
go Echo()
go Receive()
getStr := <-receive // 接收goroutine 2的返回
fmt.Println(getStr)
}
在这里不一定要去关闭channel,因为底层的垃圾回收机制会根据它是否可以访问来决定是否自动回收它。(这里不是根据channel是否关闭来决定的)
当程序则够复杂的时候,为了代码可读性更高,拆分成一个一个的小函数是需要的。
此时go提供了单向通道的类型,来实现函数之间channel的传递。
package main
import (
"fmt"
"time"
)
// 定义goroutine 1
func Echo(out chan<- string) { // 定义输出通道类型
time.Sleep(1*time.Second)
out <- "Hello World"
close(out)
}
// 定义goroutine 2
func Receive(out chan<- string, in <-chan string) { // 定义输出通道类型和输入类型
temp := <-in // 阻塞等待echo的通道的返回
out <- temp
close(out)
}
func main() {
echo := make(chan string)
receive := make(chan string)
go Echo(echo)
go Receive(receive, echo)
getStr := <-receive // 接收goroutine 2的返回
fmt.Println(getStr)
}
goroutine的通道默认是阻塞的,加一个缓冲区就可以缓解阻塞问题
ch := make(chan string, 3) // 创建了缓冲区为3的通道
//=========
len(ch) // 长度计算
cap(ch) // 容量计算
在golang里头select的功能与epoll(nginx)/poll/select的功能类似,都是监听IO操作,当IO操作发生的时候,触发相应的动作。通过这类方式能够实现异步IO。
package main
import "fmt"
func main() {
ch := make (chan int, 1)
ch<-1
select {
case <-ch:
fmt.Println("1")
case <-ch:
fmt.Println("2")
}
}
/*
输出是随机2选1
*/
package main
import "fmt"
func main() {
ch := make (chan int, 1)
ch<-1
select {
case <-ch:
fmt.Println("1")
case 2:
fmt.Println("2")
}
}
/*
编译错误:
2 evaluated but not used
select case must be receive, send or assign recv
*/
package main
import "fmt"
func main() {
ch := make (chan int, 1)
select {
case <-ch:
fmt.Println("1")
default:
fmt.Println("2")
}
}
/*
2
*/
package main
import "fmt"
func main() {
ch := make (chan int, 1)
// ch<-1 <= 注意这里备注了。
select {
case <-ch:
fmt.Println("咖啡色的羊驼")
}
}
/*
fatal error: all goroutines are asleep - deadlock!
*/
package main
import (
"fmt"
"time"
)
func main() {
ch := make (chan int)
select {
case <-ch:
case <-time.After(time.Second * 1): // 利用time来实现,After代表多少时间后执行输出东西
fmt.Println("超时啦!")
}
}
package main
import (
"fmt"
)
func main() {
ch := make (chan int, 1) // 注意这里给的容量是1
ch <- 1
select {
case ch <- 2:
default:
fmt.Println("通道channel已经满啦,塞不下东西了!")
}
}
package main
import (
"fmt"
"time"
)
func main() {
i := 0
ch := make(chan string, 0)
defer func() {
close(ch)
}()
go func() {
DONE:
for {
time.Sleep(1*time.Second)
fmt.Println(time.Now().Unix())
i++
select {
case m := <-ch:
println(m)
break DONE // 跳出 select 和 for 循环
default:
}
}
}()
time.Sleep(time.Second * 4)
ch<-"stop"
}
/*
1532390471
1532390472
1532390473
stop
1532390474
*/
Linux中的异步I/O模型 ↩︎
进程和线程、协程的区别 ↩︎
A Tour of Go: Goroutines ↩︎
深入golang之—goroutine并发控制与通信 ↩︎
操作系统概念(第四章) 线程 ↩︎