Golang并发编程

Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。 goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

并行和并发

  • 并行:在同一时刻,有多条指令在多个CPU处理器上同时执行
  • 2个队伍,2个窗口,要求硬件支持
  • 并发:在同一时刻,只能有一条指令执行,但多个进程指令被快速地轮换执行
  • 2个队伍,1个窗口,要求提升软件能力

Go语言并发优势

  • go从语言层面就支持了并发
  • 简化了并发程序的编写

 Goroutine

  • Goroutine 是go并发设计的核心
  • Goroutine 就是协程,它比线程更小,十几个Goroutine 在底层可能就是五六个线程
  • go语言内部实现了Goroutine e的内存共享,执行Goroutine 只需极少的栈内存(大概是4~5KB)

使用Goroutine

1
go 函数名( 参数列表 )

使用如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main  
  
import (  
   "fmt"  
   "time")  
  
func running() {  
  
   var times int  
   // 构建一个无限循环  
   for {  
      times++  
      fmt.Println("tick", times)  
  
      // 延时1秒  
      time.Sleep(time.Second)  
   }  
  
}  
  
func main() {  
  
   // 并发执行程序  
   go running()  
  
   // 接受命令行输入, 不做任何事情  
   var input string  
   fmt.Scanln(&input)  
}

输出结果:

1
2
3
4
5
6
7
8
tick 1
tick 2
tick 3
123tick 4
123tick 5
132tick 6
113tick 7
1313tick 8

代码执行后,命令行会不断地输出 tick,同时可以使用 fmt.Scanln() 接受用户输入。两个环节可以同时进行。

https://xenolies-blog-images.oss-cn-hangzhou.aliyuncs.com/Pics/Golang并发.jpg

Go 程序在启动时,运行时(runtime)会默认为 main() 函数创建一个 goroutine。

在 main() 函数的 goroutine 中执行到 go running 语句时,归属于 running() 函数的 goroutine 被创建,running() 函数开始在自己的 goroutine 中执行。

此时,main() 继续执行,两个 goroutine 通过 Go 程序的调度机制同时运作。

Goroutine 管理

sync.WaitGroup

如果几个 Goroutine 需要执行,且每个 Goroutine 没有先后执行限制,可以引用 sync.WaitGroup 来处理这样的情况.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main  
  
import (  
   "fmt"  
   "sync")  
  
func main() {  
   var wg sync.WaitGroup  
  
   wg.Add(3)  
  
   go funA(&wg)  
   go funB(&wg)  
   go funC(&wg)  
  
   wg.Wait()  
  
   fmt.Println("Finish")  
}  
  
func funA(wg *sync.WaitGroup) {  
   defer wg.Done()  
   fmt.Println("FunA")  
}  
  
func funB(wg *sync.WaitGroup) {  
   defer wg.Done()  
   fmt.Println("FunB")  
}  
  
func funC(wg *sync.WaitGroup) {  
   defer wg.Done()  
   fmt.Println("FunC")  
}

输出结果:

1
2
3
4
FunC
FunB
FunA
Finish

需要注意的是 sync.WaitGroup 的操作的是sync.WaitGroup中的地址,需要传入 &wg

通道 Channel

而如果 Goroutine 之间需要数据沟通,就需要使用 通道 (Channel) 来实现 Goroutine 之间的数据通信 . 你可以把它看成一个管道.通过它并发核心单元就可以发送或者接收数据进行通讯(Communication)

Channel 是 Golang 在语言级别提供的 Goroutine 之间的通信方式,可以使用 Channel 在两个或多个 Goroutine 之间传递消息。

声明通道

通道类型和普通变量类型的声明区别,仅仅是加了个 chan 关键字 .

1
var ch chan int  //声明一个名为 ch ,类型为int 的channel

在 Golang 中使用 Make关键字来创建 Channel实例

1
ch := make(chan int)

给Channel赋值也相当简单.它的操作符是箭头 <- ,箭头的指向就是数据的流向.

1
2
ch <- v // 发送值v到Channel ch中
v := <- ch // 从Channel ch中接收数据,并将数据赋值给v

无缓冲 Channel

无缓冲的 Channel(unbuffered channel) 是指在接收前没有能力保存任何值的 channel。

这种类型的 Channel 要求发送 Goroutine 和接收 Goroutine 同时准备好,才能完成发送和接收操作。

如果两个 Goroutine 没有同时准备好,Channel 会导致先执行发送或接收操作的 Goroutine 阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。

无缓冲 Channel 传递情况如图: https://xenolies-blog-images.oss-cn-hangzhou.aliyuncs.com/Pics/unbuffered%20channel.png

实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main  
  
import (  
   "fmt"  
   "time")  
  
func main() {  
   ch := make(chan int) //这里就是创建了一个无缓冲Channel  
  
   go write(ch)  //写入通道的Goroutine 
   go read(ch)   //读出通道的Goroutine 
  
   time.Sleep(1)

	close(ch) //使用后关闭
  
}  
  
func write(ch chan int) {  
   for i := 0; i < 6; i++ {  
      ch <- i //循环写入管道  
      fmt.Println("写入", i)  
   }  
}  
  
func read(ch chan int) {  
   for i := 0; i < 6; i++ { //主go程  
      num := <-ch //循环读出管道  
      fmt.Println("读出", num)  
   }  
}

输出结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
读出 0
写入 0
写入 1
读出 1
读出 2
写入 2
写入 3
读出 3
读出 4
写入 4
写入 5
读出 5

也由此可见,无缓冲的Channel是即写即读,能保证并发的数据统一.

但是问题在于只能存在一个值,如果大量的值要写入到通道,就需要带缓冲的Channel来解决这个问题了.

带缓冲 Channel

带缓冲的 Channel(buffered channel) 是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 Goroutine 之间必须同时完成发送和接收。

通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:

无缓冲的通道保证进行发送和接收的 Goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

缓冲区效果如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main  
  
import "fmt"  
  
func main() {  
  
   ch := make(chan int, 2) //创建有两个缓冲区的Channel  
   ch <- 11                //给Channel传入值  
   ch <- 12  
  
   close(ch) //关闭Channel,此时Channel值存在11和12两个值  
  
   for x := range ch { //遍历Channel  
      fmt.Println("x: ", x) //读出Channel的值  
   }  
  
   x, b := <-ch //再次读出Channel中的值,此时Channel中的值全部被读出,Channel为0,且会返回一个False说明Channel没有值了  
   fmt.Println(x, b)  
  
}

输出结果:

1
2
3
x:  11
x:  12
0 false

带缓冲 Channel 传递情况如图: https://xenolies-blog-images.oss-cn-hangzhou.aliyuncs.com/Pics/buffered%20channel.png

实例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main  
  
import (  
   "fmt"  
   "time")  
  
func main() {  
   ch := make(chan int, 10) //创建有10个缓冲区的Channel  
  
   go write(ch)  
   time.Sleep(100) //休眠  
   go read(ch)  
  
   time.Sleep(1000)  
  
   close(ch)  
  
}  
  
func write(ch chan int) {  
   for i := 0; i < 6; i++ {  
      ch <- i //循环写入管道  
      fmt.Println("写入", i)  
   }  
  
}  
  
func read(ch chan int) {  
   for i := 0; i < 6; i++ { //主go程  
      num := <-ch //循环读出管道  
      fmt.Println("读出", num)  
   }  
}

输出结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
写入 0
写入 1
写入 2
写入 3
写入 4
写入 5
读出 0
读出 1
读出 2
读出 3
读出 4
读出 5

这里我们发现,数据不是即写即读了,说明数据都存在缓冲区了.

其他

如何理解线程, 进程以及协程 一文读懂什么是进程、线程、协程 - 腾讯云开发者社区-腾讯云 (tencent.com)

本文简言之就是:

进程

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。

线程

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

协程

  1. 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
  2. 线程的默认Stack大小是1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
  3. 由于在同一个线程上,因此可以避免竞争关系而使用锁。
  4. 适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。
0%