[Go]goroutine、go的并发与并行、channels

goroutine

A goroutine is a lightweight thread of execution. 

goroutine是一个轻量执行线程。

使用go语句 start a new goroutine:

go 函数名( 参数列表 )


// 创建一个匿名函数的goroutine
go func (msg string) {
    fmt.Println(msg)
} ("goroutine")


// 创建一个新goroutine调用某已定义函数
func f(from string) {
    for i := 0; i < 3; i++ {
        fmt.Println(from, ":", i)
    }
}
go f("goroutine")

Golang创造出goroutine是为了很好地实现并发功能,因此了解goroutine的概念后要明白Go是如何用它实现并发并行的才能更好地使用它。


Go的并发

创建一个goroutine,存放在全局运行队列中。Go运行时的调度器将这些goroutine分配给一个逻辑处理器,并存放到其对应的本地运行队列中。逻辑处理器负责执行。这套管理、调度、执行goroutine的方式称为Go的并发

咱们写一段并发代码表示得更清楚一些:

package main
import (
	"fmt"
	"sync"
	"time"
)
var wg sync.WaitGroup //计数信号量
func say(s string) {
	defer wg.Done()
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond) //为了看到并发效果
		fmt.Println(s)
	}
}
func main() {
        runtime.GOMAXPROCS(1)  //只创建一个逻辑管理器
	wg.Add(2)
	go say("world")  //start a goroutine
	go say("hello")  //start a goroutine
	wg.Wait()
}

输出:

分析下这段代码:

  • world与hello交叉输出,说明两个goroutine是并发执行的。具体某个时刻执行哪个由 Go runtime 进行管理。 
  • 采用sleep是为了拉长执行时间,从而看到并发效果,否则看上去会跟顺序执行的一样。
  • 写go并发时一般都会用到信号量。进程启动时会创建一个主线程,主线程结束的时候程序进程就终止了。因此使用goroutine时要让主线程等待,否则可能看不到goroutine的输出。

[Go]goroutine、go的并发与并行、channels_第1张图片

  • 逻辑管理器是单个or多个分别涉及的是并发、并行,具体见后文

写完代码大概知道go并发、goroutine怎么用了。至于其更深层次的原理(涉及go runtime等等),另外挖个坑慢慢填~


Go的并行

调度器同时分配全局运行队列中的goroutine到多个逻辑处理器上,称作Go的并行

默认情况下,Go会给每个可用的物理处理器都分配一个逻辑处理器。比如电脑是4核的,则会默认创建4个逻辑处理器。这种情况下,若去掉上述代码中的 “runtime.GOMAXPROCS(1)”,则代码执行过程中其实是并发+并行的。


Channels

Channels are the pipes that connect concurrent goroutines.

通道是并发goroutine之间联系的管道,用来传递数据。

基本操作

一个栗子:通过两个 goroutine 来计算数字之和

// make(chan val-type)
// Channels are typed by the values they convey.

package main

import "fmt"

func sum(s []int, c chan int) {
        sum := 0
        for _, v := range s {
                sum += v
        }
        c <- sum // 把 sum 发送到通道 c
}

func main() {
        s := []int{7, 2, 8, -9, 4, 0}

        c := make(chan int)
        go sum(s[:len(s)/2], c)
        go sum(s[len(s)/2:], c)
        x, y := <-c, <-c // 从通道 c 中接收

        fmt.Println(x, y, x+y)
}

Channels provide a powerful way to reason about the flow of data from one goroutine to another without the use of locks or critical sections.

默认情况下,发送和接收会一直阻塞直到所有sender&receiver都准备好了。因此不需要另外用信号量控制,程序会自动等待消息收到。

缓冲区

默认通道是不带缓冲区的,此时发送方会阻塞直到接收方从通道中接收了值;

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小。此时发送方会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

func main() {
        ch := make(chan int, 2)  //设置缓冲区,大小为2

        // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据,而不用立刻需要去同步读取数据
        ch <- 1
        ch <- 2

        fmt.Println(<-ch)
        fmt.Println(<-ch)
}

遍历通道、关闭通道

通道关闭后,不能再向通道发送信息,但是还可以接收。(A closed channel never blocks)

func main() {
        ch := make(chan bool, 2)
        ch <- true
        ch <- true
        close(ch)
 
        for i := 0; i < cap(ch) +1 ; i++ {
                v, ok := <- ch  // ok返回量反映channel是否被关闭
                fmt.Println(v, ok)
        }
}
true true
true true
false false

使用range遍历通道:

func main() {
        ch := make(chan bool, 2)
        ch <- true
        ch <- true
        close(ch)
 
        for v := range ch {
                fmt.Println(v) // 被调用两次
        }
}

与select结合使用

select 语句使一个 goroutine 可以等待多个 channel 通信操作。

select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
                // 起初case1可执行
		case c <- x:
			x, y = y, x+y
                // 阻塞直到quit中有数据,取出,结束
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
                // 先从c中取十次数据
                // 阻塞直到c中有数据
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
                // 再向quit中发送数据
		quit <- 0
	}()
	fibonacci(c, quit)
}

为了在尝试发送或者接收时不发生阻塞,可使用 default 分支。当 select 中的其它分支都没有准备好时,default 分支就会执行。

再与WaitGroup结合:

const n = 100
finish := make(chan bool)
var done sync.WaitGroup
for i := 0; i < n; i++ { 
    done.Add(1)
    go func() {
        select {
            case <-time.After(1 * time.Hour):
            case <-finish:
        }
        done.Done()
    }()
}
t0 := time.Now()
close(finish)    // closing finish makes it ready to receive
done.Wait()      // wait for all goroutines to stop
fmt.Printf("Waited %v for %d goroutines to stop\n", time.Since(t0), n)

struct{}类型的channel

将 channel 定义为 type chan struct{} ,表示 channel 没有任何数据;只对其关闭的特性感兴趣。在这种情况下使用:never send a value on the channel, and the receiver always discards any value received. 

finish := make(chan struct{})

下一篇用go实现经典的并发模型:生产者消费者~

 

参考资料

https://www.flysnow.org/2017/04/11/go-in-action-go-goroutine.html

https://www.runoob.com/go/go-concurrent.html

https://www.jianshu.com/p/7f45d7989f3a

https://dave.cheney.net/2013/04/30/curious-channel

你可能感兴趣的:(go)