并发与并行


并发:在某个时间只能执行一个任务,但系统在执行过程中会进行多个任务间的快速切换,所以宏观上来看,就像同一个时刻执行多个任务一样。

并行:在某个时间执行多个任务,并行要求有多核,每个核执行不同的任务,多个核都在运转。

示例图说明:

并发是单核在处理,例如ABCD四个任务,单核心会不断在这四个任务之间切换,肉眼看四个任务都在执行,但实际只有一个任务在执行,切换都是微秒,所以宏观上是都在执行,微观就只有一个任务在执行。

并行是多核在处理,例如ABCD四个任务,每个任务都有一个单独的核在处理,不用来回切换,都在自己干自己的,一块向前行,就是并行。


图片


goroutine


go天生支持并发,像java等语言,并发时都是交给操作系统去分配,而在go中,go内置实现了任务的调度和管理。


go中的并发由goroutine实现,类似于线程。创建goroutine只需要在函数前加一个go关键字,当我们需要并发时,只需要把任务写到函数中,然后使用go关键字开启一个goroutine去执行即可。


像之前的例子,main函数就会启动一个goroutine来执行go程序,例如以下示例:


func hello() {
fmt.Println("hello goroutine")
}
func main() {
hello() // hello goroutine
fmt.Println("main goroutine") // main goroutine

}面这个例子属于串行方式,也就是程序是按顺序执行,上面程序只有执行完毕,下面的程序才能执行。


这里给hello函数添加go关键字,给它分配一个单独的goroutine来执行,这时这个程序就会有两个goroutine,一个hello函数的,一个main函数的。


func hello() {
fmt.Println("hello goroutine")
}
func main() {
go hello()
fmt.Println("main goroutine") // main goroutine
}

上面函数的打印结果只有一行main goroutine,并没有hello goroutine,原因在于程序没有串行执行,hello函数和main函数是不同goroutine(线程)执行的,当hello还没有执行完时,main就执行结束了,导致了结果只输出了一行。


可以在main函数最后添加一个time.Sleep,来等待一秒,这时hello函数就执行完了,然后一秒后main函数结束,结果是都有输出,并且可以看到程序打印后有明显的停顿一秒现象。


func main() {
	go hello() // hello goroutine  fmt.Println("main goroutine") // main goroutine  time.Sleep(time.Second)
}


多个goroutine


上面例子是单个的goroutine,我们停顿了一秒,如果goroutine过多,且我们无法估测多久会执行完,例如以下代码:


func hello(i int) {
fmt.Println("hello goroutine", i)
}
func main() {
for i := 0; i < 10; i++ {
go hello(i)
}
fmt.Println("main goroutine") // main goroutine time.Sleep(time.Second * 2)
}


这里可以使用sync.WaitGroup来解决,它实现了goroutine的同步,示例代码如下。


var wg sync.WaitGroup // 创建一个sync.WaitGroup
func hello(i int) {
	defer wg.Done() // 每执行一次hello函数,wg就减一  fmt.Println("hello goroutine", i)
}
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1) // 每使用goroutine调用一次hello,wg就加一    go hello(i)  
	}
	fmt.Println("main goroutine") // main goroutine  wg.Wait()
}


上面代码执行发现,每次结果输出完毕后,没有停顿,特别流畅的就结束了,而不是之前使用time.Sleep,总是停顿一会。


上面代码执行多次发现,hello输出时顺序并不是按照0、1、2、3...顺序来的,而是无序的,这是因为它们每个都有自己的goroutine,是并发执行,而不是串行。


goroutine初始化栈大小是2kb,不固定,可增大可缩小,最大可达1gb,因为初始化小,所以goroutine可一次创建十万个也没问题。


gomaxprocs


gomaxprocs用来确定使用多少个os线程来执行go代码,默认是机器上所有的cpu核心数。


在go中,可通过runtime.GOMAXPROCS()来设置当前程序并发时占用的cpu核心数。


示例:例如下面示例,将程序的核心数设置为1,然后通过两个goroutine执行a和b函数,打印结果是先执行完一个函数再执行另一个。




将上面runtime.GOMAXPROCS改为2,即使用并行来执行程序,两个核处理两个goroutine,同时进行。


使用runtime.NumCPU可查看电脑上的核心数。


func a() {
	for i := 0; i < 10; i++ {
		fmt.Println("A", i)
	}
}
func b() {
	for i := 0; i < 10; i++ {
		fmt.Println("B", i)
	}
}
func main () {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)
}


总结


go的并发属于该语言的一大优势,在其它语言中os线程都是内核来调用的,goroutine则是由go的runtime自己调度的。

写程序的时候也很简单,直接go关键字即可,不像其它语言,要关注的东西太多。

点赞(466)

评论列表共有 0 条评论

立即
投稿
返回
顶部