golang 之并发

在了解之前,要注意golang是并发语言而不是并行语言

并发和并行

  • 并发是一次性做大量事情的能力(两个或多个事件在同一时间间隔发生)
  • 并行同一时间执行多个任务的能力(两个或者多个事件在同一时刻发生)

举例说明:

  每天早上10分钟我洗脸,刷牙,吃早饭等等很多事情,这就是并发。  我一边刷牙的同时在烧水做饭这就是并行。

技术层面来说:假如一个web网页中有视频播放和文件下载两个动作,当浏览器在单核的处理器下运行时, CPU核心会在这两个事件中来回切换,(同时)播放视频和下载,这就称为并发。并发进程在不同的时间点开始并有着重叠的执行周期。假如你的CPU是多核处理器,那么下载和播放会在不同的CPU核心同时执行,这就是并行。

goroutine

   在go中,每一个并发执行的操作都称为goroutine,当一个程序启动时,只有一个goroutine来调用main函数,称它为主goroutine。新的goroutine通过go语法来创建。

f()  // 调用f(); 等待它返回
go f()  //新建一个调用f()的goroutine,不用等待。

 调度模型

groutine能拥有强大的并发实现是通过GPM调度模型实现:

  • G: 代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
  • M: 代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息
  • P: 全程processor,处理器,它的主要作用来执行goroutine,所以它维护了一个goroutine队列。里面存储了所有需要它来执行的goroutine。
  • Sched: 代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。

调度实现

  • 有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。
  • P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),
  • Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。

如果一个线程阻塞会发生什么情况呢?如下图

 

  从上图中可以看出,一个线程放弃了它的上下文让其他的线程可以运行它。M1可能仅仅为了让它处理图中系统调用而被创建出来,或者它可能来自一个线程池。这个处于系统调用中的线程将会保持在这个导致系统调用的goroutine上,因为从技术上来说,它仍然在执行,虽然阻塞在OS里了。

  另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,

 

 使用goroutine

package main

import (
    "fmt"
    "time"
)

func cal(a int , b int )  {
    c := a+b
    fmt.Printf("%d + %d = %d\n",a,b,c)
}

func main() {
  
    for i :=0 ; i<10 ;i++{
        go cal(i,i+1)  //启动10个goroutine 来计算
    }
    time.Sleep(time.Second * 2) // sleep作用是为了等待所有任务完成
} 

GOMAXPROCS

设置goroutine运行的CPU数量,最新版本的go已经默认已经设置了。

num := runtime.NumCPU()    //获取主机的逻辑CPU个数
runtime.GOMAXPROCS(num)    //设置可同时执行的最大CPU数

 也可以根据个人手动设置,例如

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)

 上面GOMAXPROCS设置为1,当遇到两个go调度时,就会发生等待。如果设置为2就会并行执行(前提是你的cpu数量>=2),如下例子

func a() {
	for i := 1; i < 10; i++ {
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(2)
	go a()
	go b()
	time.Sleep(time.Second)
}

 在执行上面代码时细心的同学会发现,每一步最后都会睡眠一秒,才能打印结果,这是因为并发执行,goroutine还没来得及返回结果,主线程已经执行完了。

  那么如果不会面有没有其他的方法?当然有。第一种便是采用sync.WaitGroup来实现

var wg sync.WaitGroup

func hello(i int) {
	defer wg.Done() // goroutine结束就登记-1
	fmt.Println("Hello Goroutine!", i)
}
func main() {

	for i := 0; i < 10; i++ {
		wg.Add(1) // 启动一个goroutine就登记+1
		go hello(i)
	}
	wg.Wait() // 等待所有登记的goroutine都结束
}

 详细用法详见sync包。另外一种便是channel

channel

channel是用来传递数据的一个数据结构,同map一样使用内置的make来创建。如

ch := make(chan int)  // 无缓冲通道
ch1 := make(chan int, 10) //缓冲为10的通道

channel类型

定义格式 

ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .

它表示三种类型的定义,可选的<-表示channel的方向。如果没有指定,即表示双向通道,既可以接收,也可以发送。

chan T          // 可以接收和发送类型为 T 的数据
chan<- float64  // 只可以用来发送 float64 类型的数据
<-chan int      // 只可以用来接收 int 类型的数据

 <-总是最优先与最左边类型结合。如

chan<- chan int    // 等价 chan<- (chan int)
chan<- <-chan int  // 等价 chan<- (<-chan int)
<-chan <-chan int  // 等价 <-chan (<-chan int)
chan (<-chan int)

channel操作

常见三种操作,接收,发送和关闭

ch := make(chan int)
  • 发送:ch <- 1 //将1发送到ch通道中
  • 接收:x := <-ch // 从ch接收值并赋给x。也可以直接抛弃:<-ch
  • 关闭:close(ch)

  close时可以通过i, ok := <-c可以查看Channel的状态,判断值是零值还是正常读取的值。

c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

无缓冲通道

需要注意的是,无缓冲通道上的发送操作将会阻塞,值到另一个goroutine在对立的通道上执行接收操作。这时值才算完成。相反,如果接收操作先执行,接收方goroutine将阻塞,直到另一个goroutine在同一个通道发送一个值。

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)
}

 打印结果

-5 17 12

单向通道

  当程序演进时,将大的函数拆分为多个更小的是很自然的,在当一个通道用作函数的行参时,它几乎总是被有意地限制不能发送或接收。为了将这种意图可以比避免误用,在go的类型系统提供了单向通道。仅仅导出发送或者接收操作。如类型chan <- int是一个只能发送的通道。反之 <- chan int是一个只能接收int类型通道。

func sum(out chan<- int) {
	for i := 0; i < 100; i++ {
		out <- i
	}
	close(out)
}

func squarer(out chan<- int, in <-chan int) {
	for i := range in {
		out <- i * i
	}
	close(out)
}
func printer(in <-chan int) {
	for i := range in {
		fmt.Println(i)
	}
}

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go sum(ch1)
	go squarer(ch2, ch1)
}

有缓冲通道  

  缓冲通道有一个元素队列,队列的最大长度在创建时通过make的容量参数来设置。

ch := make(chan string, 3)

  

 

  一个空的缓冲通道

缓冲通道上的发送操作在队列的尾部插入一个元素,接收操作从队列的头部移除一个元素。如果通道满了,发送操作将会阻塞所在的goroutine直到另一个goroutine对它进行移除操作留出可用的空间。反过来,如果通道空了。执行接收操作的goroutine阻塞,直到另一个goroutine在通道上发送数据。

func main() {
	ch := make(chan string, 3)
	ch <- "a"
        ch <- "b"
        ch <- "c"
	fmt.Println("发送成功")
        x := <-ch // 打印a
}

range 

func main() {
    go func() {
        time.Sleep(1 * time.Hour)
    }()
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i
        }
        close(c)
    }()
    for i := range c {
        fmt.Println(i)
    }
    fmt.Println("Finished")
}

 如上面的例子,range c 产生的迭代值为channel中发送的值,它会一直迭代直到channel关闭。如果此时close(c)关掉。程序会一直阻塞在for....range c 这一行。

select

  select语句选择一组可能的send操作和receive操作去处理。它类似switch,但是只是用来处理通讯(communication)操作。它的case可以是send语句,也可以是receive语句,亦或者defaultreceive语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。最多允许有一个default case,它可以放在case列表的任何位置,尽管我们大部分会将它放在最后。

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}
func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

  如果有同时多个case去处理,比如同时有多个channel可以接收数据,那么Go会伪随机的选择一个case处理(pseudo-random)。如果没有case需要处理,则会选择default去处理,如果default case存在的情况下。如果没有default case,则select语句会阻塞,直到某个case需要处理。

  特别注意的是nil channel会一直被阻塞。如果没有default: nil chanel会一直阻塞。

最后列出channel的几种常用关系

小结:

  在处理并发时会发生数据错乱的情况,这时候就会用到锁机制,如上面一开始介绍sync包。锁将会在sync包中描述。

 

posted @ 2020-03-01 14:31  Dwyane.wang  阅读(532)  评论(0)    收藏  举报