一次只做一件事情并不是完成任务最快的方法.一些大的任务可以拆解成若干个小任务.goroutine可以让程序同时处理几个不同的任务.goroutine使用channel来协调它们的工作.channel允许goroutine互相发送数据并同步.这样一个goroutine就不会领先于另一个goroutine.它允许我们充分利用具有多处理器的计算机,让程序运行得尽可能的快.
我们有a.txt,b.txt,c.txt三个文件,我们需要读取它们的内容至内存,然后再计算它们的大小
package main
import (
"bufio"
"fmt"
"os"
"time"
)
func GetFileLen(name string) (length uint64, err error) {
var filelength uint64
filelength = 0
fmt.Println("Opening name", name)
file, err := os.OpenFile(name, os.O_RDONLY, os.FileMode(0600))
if err != nil {
return filelength, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
filelength += uint64(len(scanner.Text()))
}
fmt.Println(name, " length ", filelength)
return filelength, nil
}
func main() {
PrintTime()
l1, _ := GetFileLen("a.txt") // 1.6G
l2, _ := GetFileLen("b.txt") // 1.6G
l3, _ := GetFileLen("c.txt") // 1.6G
fmt.Println(l1, l2, l3)
PrintTime()
}
func PrintTime() {
t := time.Now().Unix()
fmt.Println(t)
}
结果如下
PS D:\goproj\routine> go run .\main.go
1648263533
Opening name a.txt
a.txt length 1861221360
Opening name b.txt
b.txt length 1861221360
Opening name c.txt
c.txt length 1861221360
1861221360 1861221360 1861221360
1648263542
如上所知,我们完成所有文件的读取需要9秒钟的时间
如图所示,现在的执行顺序是这样子的
如果读取文件的任务能同时运行,那么整个任务可以在更短的时间内完成.如下图所示
当我们调用GetFileLen
时,程序必须在那里等待文件打开并读取内容,这个时候其实程序本身是不作任何事情的.但是由于程序是阻塞的,所以它卡在原地了.
并发性允许程序暂停一个任务并处理其他的任务.等待用户输入的程序可能在后台执行其他处理.程序在读取文件同时更新进度条.
支持并行运行的程序能够同时运行多个任务.一台只有一个处理器的计算机同一时刻只能处理一个任务.但是现在大多数计算机都是多核的.计算机由操作系统负责在不同的处理器之间分配.
在Go中,并发任务被称为goroutine,其他编程语言中这个相同的概念叫线程.但是goroutine比线程占用更少的内存.启动和停止的时间更少,这也就是意味着在同样硬件的情况下,可以运行更多的goroutine.
启动另一个goroutine非常简单,它只要在函数前面加go关键字.
go GetFileLen("a.txt")
每个go的main函数也是一个goroutine,所以在main中启动的任何一个goroutine都是第2个.goroutine允许并发:暂停一个任务来处理其他的任务,也允许多个任务同时执行.
func main() {
PrintTime()
// l1, _ := GetFileLen("a.txt")
// l2, _ := GetFileLen("b.txt")
// l3, _ := GetFileLen("c.txt")
// fmt.Println(l1, l2, l3)
go GetFileLen("a.txt")
go GetFileLen("b.txt")
go GetFileLen("c.txt")
time.Sleep(5 * time.Second) // 一定要加上,因为不加程序将直接退出.
PrintTime()
}
输出结果如下
1648290898
Opening name c.txt
Opening name a.txt
Opening name b.txt
c.txt length 1861221360
a.txt length 1861221360
b.txt length 1861221360
1648290903
从上面的结果我们看出来.我们调用的顺序是a b c,但是出来的顺序却是c a b.这由于我们无法决定哪个goroutine先运行哪个后运行.完全取决于go自身的调度机制.
如下图所示
由于main goroutine的运行时间要远低于其他goroutine的运行时间,所以如果不加以控制,
main goroutine完成之后就会退出,而其它的goroutine输出的结果也就看不到了.这也是为什么要加Sleep的原因所在.
我们也可以把时间改成time.Sleep(4 * time.Second),就是4秒钟,如果改成2秒或者1秒,是看不到输出的.因为读取文件大概需要3秒钟.所以用休眠的方式并不好,因为不同计算硬件对读取大文件的操作时间是不确定的,说不定在一些老旧的电脑上读取这些文件需要30秒.因此我们需要用更精确的办法来控制.
goroutine是不允许有返回值的.这实际上是正确的.由于代码在goroutine中运行,所以你不能指望它以函数的形式马上给你返回值,因为什么时候返回到调用它的goroutine是不确定的,所以它不能保证返回值什么时候准备好.
goroutine之间的通讯方式被称为chnnel.它不仅允许你将一个值从一个goroutine发送给另一个goroutine并且保证了在接收该值的goroutine使用这个值之前,它一定是被发送过来的.
它实际上是解决了goroutine没有返回值的问题.channel声明如下
var myChannel chan uint64 // 声明channel变量
myChannel = make(chan uint64) // 创建channel变量
myChannel1 := make(chan uint64) // 短变量声明
myChannel <- 20 // 发送值给channel
var u uint64
u <- myChannel // 从channel中接收值
我们改造一下GetFileLen
使它接收channel,然后再在main goroutine中接收这个值
func GetFileLen(name string, c chan uint64) (length uint64, err error) {
var filelength uint64
filelength = 0
fmt.Println("Opening name", name)
file, err := os.OpenFile(name, os.O_RDONLY, os.FileMode(0600))
if err != nil {
return filelength, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
filelength += uint64(len(scanner.Text()))
}
fmt.Println(name, " length ", filelength)
c <- filelength // 发送值给channel
return filelength, nil
}
func main() {
PrintTime()
// 创建channel
c := make(chan uint64)
d := make(chan uint64)
e := make(chan uint64)
go GetFileLen("a.txt", c)
go GetFileLen("b.txt", d)
go GetFileLen("c.txt", e)
fmt.Println(<-c, <-d, <-e) // 接收channel的值
// time.Sleep(4 * time.Second)
PrintTime()
}
结果如下
1648297266
Opening name c.txt
Opening name b.txt
Opening name a.txt
b.txt length 1861221360
c.txt length 1861221360
a.txt length 1861221360
1861221360 1861221360 1861221360
1648297269
可以看到,这次用的时间是3秒.所有的值都求出来了,理论上来讲.即使再有多个GetFileLen也是3秒会完成.
channel在接收值的时候会阻塞当前的上下文,等待接收端goroutine的处理.利用这个特性,我们可以尝试修改一下程序,让它按照我们的意图在接收端输出受控制的顺序
func GetFileLen(name string, c chan string) (length uint64, err error) {
var filelength uint64
filelength = 0
fmt.Println("Opening name", name)
file, err := os.OpenFile(name, os.O_RDONLY, os.FileMode(0600))
if err != nil {
return filelength, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
filelength += uint64(len(scanner.Text()))
}
// fmt.Println(name, " length ", filelength)
// c <- filelength // 发送值给channel
// 发送值给channel
c <- fmt.Sprintf("%s length %s", name, strconv.FormatUint(filelength, 10))
// fmt.Println(S)
return filelength, nil
}
func main() {
PrintTime()
// l1, _ := GetFileLen("a.txt")
// l2, _ := GetFileLen("b.txt")
// l3, _ := GetFileLen("c.txt")
// fmt.Println(l1, l2, l3)
c := make(chan string)
d := make(chan string)
e := make(chan string)
go GetFileLen("a.txt", c)
go GetFileLen("b.txt", d)
go GetFileLen("c.txt", e)
fmt.Println(fmt.Sprint("receive : ", <-c))
fmt.Println(fmt.Sprint("receive : ", <-d))
fmt.Println(fmt.Sprint("receive : ", <-e))
// time.Sleep(4 * time.Second)
PrintTime()
}
运行的结果如下,现在无论运行多少次,receive 的顺序都不变了,但是Opening的顺序是会变的
1648299196
Opening name a.txt
Opening name c.txt
Opening name b.txt
receive : a.txt length 1861221360
receive : b.txt length 1861221360
receive : c.txt length 1861221360
1648299199
1648299504
Opening name c.txt
Opening name a.txt
Opening name b.txt
receive : a.txt length 1861221360
receive : b.txt length 1861221360
receive : c.txt length 1861221360
1648299508
大家可以调整一下c d e 的顺序,调整以后输出a b c的顺序也会跟着变化.这说明在goroutine的接收方顺序是可以同步的,但是调用goroutine的顺序是无法控制的
因篇幅问题不能全部显示,请点此查看更多更全内容