Golang 入门系列(十五)如何理解go的并发?

前面已经讲过不少Golang系列知识,感兴趣的能够看看之前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.htmlcss

接下来要说的是golang的并发,其实以前简单介绍过协程(goroutine)和管道(channel) 等基础内容,只是比较简单,只讲了基本的语法。今天就详细说说golang的并发编程。html

 

1、并发和并行

Go是并发语言,而不是并行语言。因此咱们在讨论,咱们首先必须了解什么是并发,以及它与并行性有什么不一样。程序员

 

什么是并发

并发就是一段时间内处理许多事情。golang

好比,一我的在晨跑。在晨跑时,他的鞋带松了。如今这我的中止跑步,系鞋带,而后又开始跑步。这是一个典型的并发。这我的可以同时处理跑步和系鞋带,这是一我的可以同时处理不少事情。编程

 

什么是并行

并行就是同一时刻作不少事情。这听起来可能与并发相似,但其实是不一样的。缓存

再好比,这我的正在慢跑,而且使用他的手机听音乐。在这种状况下,一我的一边慢跑一边听音乐,那就是他同时在作不少事情。这就是所谓的并行。安全

 

并发不是并行。并发更关注的是程序的设计层面,并发的程序彻底是能够顺序执行的,只有在真正的多核CPU上才可能真正地同时运行。并行更关注的是程序的运行层面,并行通常是简单的大量重复,例如GPU中对图像处理都会有大量的并行运算。为更好的编写并发程序,从设计之初Go语言就注重如何在编程语言层级上设计一个简洁安全高效的抽象模型,让程序员专一于分解问题和组合方案,并且不用被线程管理和信号互斥这些繁琐的操做分散精力。
 
上图能清楚的说明了并发和并行的区别。
 

2、协程(Goroutines)

go中使用Goroutines来实现并发。Goroutines是与其余函数或方法同时运行的函数或方法。Goroutines能够被认为是轻量级的线程。与线程相比,建立Goroutine的成本很小。所以,Go应用程序能够并发运行数千个Goroutines。bash

Goroutines在线程上的优点。并发

  1. 与线程相比,Goroutines很是便宜。它们只是堆栈大小的几个kb,堆栈能够根据应用程序的须要增加和收缩,而在线程的状况下,堆栈大小必须指定而且是固定的编程语言

  2. Goroutines被多路复用到较少的OS线程。在一个程序中可能只有一个线程与数千个Goroutines。若是线程中的任何Goroutine都表示等待用户输入,则会建立另外一个OS线程,剩下的Goroutines被转移到新的OS线程。全部这些都由运行时进行处理,咱们做为程序员从这些复杂的细节中抽象出来,并获得了一个与并发工做相关的干净的API。

  3. 当使用Goroutines访问共享内存时,经过设计的通道能够防止竞态条件发生。通道能够被认为是Goroutines通讯的管道。

 

如何使用Goroutines

在函数或方法调用前面加上关键字go,您将会同时运行一个新的Goroutine。

实例代码:

package main import ( "fmt"
    "time" ) func hello() { fmt.Println("Hello world goroutine") } func main() { go hello() time.Sleep(1 * time.Second) fmt.Println("main function") }
运行结果: Hello world goroutine main function

 

如何启动多个Goroutines

示例代码:

package main import ( "fmt"
    "time" ) func numbers() { for i := 1; i <= 5; i++ { time.Sleep(250 * time.Millisecond) fmt.Printf("%d ", i) } } func alphabets() { for i := 'a'; i <= 'e'; i++ { time.Sleep(400 * time.Millisecond) fmt.Printf("%c ", i) } } func main() { go numbers() go alphabets() time.Sleep(3000 * time.Millisecond) fmt.Println("main terminated") } 运行结果: 1 a 2 3 b 4 c 5 d e main terminated

 

Goroutine切换

下面经过素数计算的例子来讲明goland是如何经过切换不一样的goroutine实现并发的。

package main

import (
"fmt"
"runtime"
"sync"
)

var wg sync.WaitGroup

func main() {

runtime.GOMAXPROCS(1)

wg.Add(2)
go printPrime("A")
go printPrime("B")

fmt.Println("Wait for finish")
wg.Wait()
fmt.Println("Program End")
}

func printPrime(prefix string) {
defer wg.Done()

  nextNum:
for i := 2; i < 6000; i++ {
for j := 2; j < i; j++ {
if i%j == 0 {
continue nextNum
}
}
fmt.Printf("%s:%d\n", prefix, i)
}
fmt.Printf("complete %s\n", prefix)
}

运行结果:
Wait for finish B:2 B:3 B:5 B:7 B:11 ... B:457 B:461 B:463 B:467 A:2 A:3 A:5 A:7 ... A:5981 A:5987 complete A B:5939 B:5953 B:5981 B:5987 complete B Program End

经过以上的输出结果,能够看出两个Goroutine是在一个处理器上经过切换goroutine实现并发执行。

 

3、通道(channels)

通道能够被认为是Goroutines通讯的管道。相似于管道中的水从一端到另外一端的流动,数据能够从一端发送到另外一端,经过通道接收。

 

声明通道

每一个通道都有与其相关的类型。该类型是通道容许传输的数据类型。(通道的零值为nil。nil通道没有任何用处,所以通道必须使用相似于地图和切片的方法来定义。)

示例代码:

package main import "fmt" func main() { var a chan int
    if a == nil { fmt.Println("channel a is nil, going to define it") a = make(chan int) fmt.Printf("Type of a is %T", a) } } 运行结果: channel a is nil, going to define it Type of a is chan int

也能够简短的声明:

a := make(chan int)

发送和接收

发送和接收的语法:

data := <- a   // read from channel a
a <- data      // write to channel a

在通道上箭头的方向指定数据是发送仍是接收。

 

一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另外一个Goroutine从该通道读取数据。相似地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。

这些通道的特性是帮助Goroutines有效地进行通讯,而无需像使用其余编程语言中很是常见的显式锁或条件变量。

示例代码:

package main import ( "fmt"
    "time" ) func hello(done chan bool) { fmt.Println("hello go routine is going to sleep") time.Sleep(4 * time.Second) fmt.Println("hello go routine awake and going to write to done") done <- true } func main() { done := make(chan bool) fmt.Println("Main going to call hello go goroutine") go hello(done) <-done fmt.Println("Main received data") }

运行结果:

 Main going to call hello go goroutine
 hello go routine is going to sleep
 hello go routine awake and going to write to done
 Main received data

 

定向通道

以前咱们学习的通道都是双向通道,咱们能够经过这些通道接收或者发送数据。咱们也能够建立单向通道,这些通道只能发送或者接收数据。

建立仅能发送数据的通道,示例代码:

package main import "fmt" func sendData(sendch chan<- int) { sendch <- 10 } func main() { sendch := make(chan<- int) go sendData(sendch) fmt.Println(<-sendch) }

报错:

 # command-line-arguments
 .\main.go:12:14: invalid operation: <-sendch (receive from send-only type chan<- int)

 

示例代码:

package main import "fmt" func sendData(sendch chan<- int) { sendch <- 10 } func main() { chnl := make(chan int) go sendData(chnl) fmt.Println(<-chnl) }

运行结果:
10

 

死锁

为何会死锁?非缓冲信道上若是发生了流入无流出,或者流出无流入,也就致使了死锁。或者这样理解 Go启动的全部goroutine里的非缓冲信道必定要一个线里存数据,一个线里取数据,要成对才行 。

示例代码:

package main

func main() {
c, quit := make(chan int), make(chan int)

go func() {
c <- 1 // c通道的数据没有被其余goroutine读取走,堵塞当前goroutine
quit <- 0 // quit始终没有办法写入数据
}()

<-quit // quit 等待数据的写
}
 报错: fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]: main.main() /tmp/sandbox249677995/main.go:11 +0x80

 

关闭通道

关闭通道只是关闭了向通道写入数据,但能够从通道读取。

package main import ( "fmt" ) var ch chan int = make(chan int, 3) func main() { ch <- 1 ch <- 2 ch <- 3 close(ch) for v := range ch { fmt.Println(v) } }

 

4、缓冲通道

以前学习的全部通道基本上都没有缓冲。发送和接收到一个未缓冲的通道是阻塞的。

能够用缓冲区建立一个通道。发送到一个缓冲通道只有在缓冲区满时才被阻塞。相似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。

能够经过将额外的容量参数传递给make函数来建立缓冲通道,该函数指定缓冲区的大小。

语法:

ch := make(chan type, capacity) 

上述语法的容量应该大于0,以便通道具备缓冲区。默认状况下,无缓冲通道的容量为0,所以在以前建立通道时省略了容量参数。

示例代码:

func main() { done := make(chan int, 1) // 带缓存的管道 go func(){ fmt.Println("你好, 世界") done <- 1 }() <-done }

 

5、最后

以上,就把golang并发编程相关的内容介绍完了,但愿能对你们有所帮助。

 

原文出处:https://www.cnblogs.com/zhangweizhong/p/11447334.html

相关文章
相关标签/搜索