• 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
    ",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包中描述。

  • 相关阅读:
    线程池小结(一)
    [转]ViewPager学习笔记(一)——懒加载
    [转]Private Libraries、Referenced Libraries、Dependency Libraries的区别
    关于context你必须知道的一切
    【转】在mac上配置安卓SDK
    【转】HTTP中的长连接和短连接分析
    中间件解析FDMEMTABLE.delta生成SQL的方法
    delphi 中配置文件的使用(*.ini)和TIniFile 用法
    Delphi 字符串加密与解密函数
    Delphi编写的等长加密与解密
  • 原文地址:https://www.cnblogs.com/flash55/p/12378704.html
Copyright © 2020-2023  润新知