• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

Go语言学习之8goroutine详解、定时器与单元测试

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

主要内容:

1.Goroutine
2. Chanel
3. 单元测试

1. Goroutine

     Go 协程(Goroutine)(轻量级的线程,开线程没有数量限制)。
   (1)进程和线程
  A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
  B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
  C. 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

       例如:ngix是多进程的单线程程序

       内核线程、轻量级进程、用户线程三种线程概念详见:https://blog.csdn.net/gatieme/article/details/51481863 

    (2)并发和并行

        A. 并发是指立即处理多个任务的能力。多线程程序在一个核的cpu上运行(线程之间通过CPU轮询来执行),就是并发。go多线程的切换都是在用户态操作的,不像其他语言先切换到内核态,完成线程切换,然后返回用户态继续执行程序。
        B. 并行是指同时处理多个任务。多线程程序在多个核的cpu上运行,就是并行。
        例如:假如有一个 web 浏览器。这个 web 浏览器有各种组件。其中两个分别是 web 页面的渲染区和从网上下载文件的下载器。假设各个组件也都可以相互独立地运行。当浏览器在单核处理器中运行时,处理器会在浏览器的两个组件间进行上下文切换。它可能在一段时间内下载文件,转而又对用户请求的 web 页面进行渲染。这就是并发。并发的进程从不同的时间点开始,分别交替运行。在这里,就是在不同的时间点开始进行下载和渲染,并相互交替运行的。
       如果该浏览器在一个多核处理器上运行,此时下载文件的组件和渲染 HTML 的组件可能会在不同的核上同时运行。这称之为并行

       Go 编程语言原生支持并发。Go 使用 Go 协程(Goroutine) 和信道(Channel)来处理并发。

 

        注意:并行不一定会加快运行速度,因为并行运行的组件之间可能需要相互通信。在我们浏览器的例子里,当文件下载完成后,应当对用户进行提醒,比如弹出一个窗口。于是,在负责下载的组件和负责渲染用户界面的组件之间,就产生了通信。在并发系统上,这种通信开销很小。但在多核的并行系统上,组件间的通信开销就很高了。所以,并行不一定会加快运行速度!

       补充:用户线程指的是完全建立在用户空间的线程库,用户线程的建立,同步,销毁,调度完全在用户空间完成,不需要内核的帮助。因此这种线程的操作是极其快速的且低消耗的。

    (3)协程和线程
   协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
   线程:一个线程上可以跑多个协程,协程是轻量量级的线程。一个线程可以跑多个Goroutine。

        Go 协程相比于线程的优势:

  • 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。
  • Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。所有这一切都在运行时进行,作为程序员,我们没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。
  • Go 协程使用信道(Channel)来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是 Go 协程之间通信的管道.

      GO语言Goroutine与线程的区别:https://baijiahao.baidu.com/s?id=1620972759226100794&wfr=spider&for=pc

    (4)goroutine调度模型

        M 代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的。
        P 全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine。
        G 就是goroutine实现的核心结构了,G维护了goroutine需要的栈、程序计数器以及它所在的M等信息。
        Sched 结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。

       如果有IO操作时,会新起一个线程等待IO操作的Goroutine

 

       

       Go scheduler: https://www.jianshu.com/p/1911b1229a44
       解释Goroutine浅显易懂:https://www.jianshu.com/p/7ebf732b6e1f
       Go语言 Goroutine 浅析:http://baijiahao.baidu.com/s?id=1587634508058779877&wfr=spider&for=pc
       Goroutine并发调度模型深度解析之手撸一个协程池:
       https://www.jianshu.com/p/fa6d82934cb8?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

       启动一个go协程?

 1 package main 
 2 
 3 import "fmt"
 4 
 5 func test_go() {
 6     fmt.Println("hello world")
 7 }
 8 
 9 func main() {
10     go test_go()
11     fmt.Println("main func finished")
12 }
启动一个go协程

      执行结果:

      

     分析:发现起的go协程go test_go()并没有生效,只打印出 hello world,这是由于,启动一个新的协程时,协程的调用会立即返回。与函数不同,程序控制不会去等待 Go 协程执行完毕。下面使用sleep使主线程处于睡眠之中等待go协程执行结束(实际中该方法不靠谱)。后面会介绍靠谱的方法。

 1 package main 
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 func test_go() {
 8     fmt.Println("hello world")
 9 }
10 
11 func main() {
12     go test_go()
13     time.Sleep(time.Second)
14     fmt.Println("main func finished")
15 }
sleep阻塞主线程等待go协程执行结束

      执行结果:

      

      启动多个go协程?

 1 package main
 2 
 3 import (  
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func numbers() {  
 9     for i := 1; i <= 5; i++ {
10         time.Sleep(250 * time.Millisecond)
11         fmt.Printf("%d ", i)
12     }
13 }
14 func alphabets() {  
15     for i := 'a'; i <= 'e'; i++ {
16         time.Sleep(400 * time.Millisecond)
17         fmt.Printf("%c ", i)
18     }
19 }
20 func main() {  
21     go numbers()
22     go alphabets()
23     time.Sleep(3000 * time.Millisecond)
24     fmt.Printf("\nmain terminated")
25 }
启动多个go协程

      读者可以自行分析该程序的时间片打印输出。

    (5)如何设置golang运行的cpu核数
         1.5之前go需要手动设置程序执行的内核数,1.5之后go自动设置

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "runtime"
 6 )
 7 
 8 func main() {
 9     num := runtime.NumCPU()  //查看有几个内核
10     fmt.Printf("cpu num:%d\n", num) 
11     runtime.GOMAXPROCS(1)    //设置有程序用几个内核执行
12 }
获取CPU核数并设置执行程序的核数

    (6)不同goroutine之间进行通讯

        A:全局变量和锁同步

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6     "time"
 7 )
 8 
 9 var (
10     m    = make(map[int]uint64)
11     lock sync.Mutex
12 )
13 
14 type task struct {
15     n int
16 }
17 
18 func calc(t *task) {
19     var sum uint64
20     sum = 1
21     for i := 1; i < t.n; i++ {
22         sum *= uint64(i)
23     }
24 
25     fmt.Println(t.n, sum)
26     lock.Lock()  //加锁,不然多个协程修改全局变量会存在竞争
27     m[t.n] = sum
28     lock.Unlock()
29 }
30 
31 func main() {
32     for i := 0; i < 16; i++ {
33         t := &task{n: i}
34         go calc(t)
35     }
36 
37     time.Sleep(10 * time.Second)
38     lock.Lock()
39     for k, v := range m {
40         fmt.Printf("%d! = %v\n", k, v)
41     }
42     lock.Unlock()
43 }
全局变量和锁同步

       B:Channel

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func write(ch chan int) {
 9     for i := 0; i < 100; i++ {
10         ch <- i
11         fmt.Println("put data:", i)
12     }
13 }
14 
15 func read(ch chan int) {
16     for {
17         var b int
18         b = <-ch
19         fmt.Println(b)
20         time.Sleep(time.Second)
21     }
22 }
23 
24 func main() {
25     intChan := make(chan int, 10)
26     go write(intChan)
27     go read(intChan)
28 
29     time.Sleep(10 * time.Second)
30 }
channel write and read

    (7)goroutine中使用recover

        如果某个goroutine出现panic,为了不使程序崩溃挂掉,可以在该goroutine中使用recover(类似于python中的try……except)捕获该panic。

 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func test() {
 9     defer func() { //defer必须放置在最前面,才能捕获后面所有的panic,程序退出时执行defer
10         err := recover() //捕获goroutine错误
11         if err != nil {
12             fmt.Println(err)
13         }
14     }()
15 
16     var p *int
17     *p = 20 //panic
18 }
19 
20 func main() {
21     go test()
22     time.Sleep(time.Second)
23     fmt.Println("main progress exit")
24 }
example
 1 package main
 2 
 3 import (
 4     "fmt"
 5     "runtime"
 6     "time"
 7 )
 8 
 9 func test() {
10 
11     defer func() {
12         if err := recover(); err != nil {  //处理panic,calc依然可以正常执行
13             fmt.Println("panic:", err)
14         }
15     }()
16 
17     var m map[string]int  //panic: assignment to entry in nil map
18     m["stu"] = 100
19 }
20 
21 func calc() {
22     for {
23         fmt.Println("i'm calc")
24         time.Sleep(time.Second)
25     }
26 }
27 
28 func main() {
29     num := runtime.NumCPU()
30     runtime.GOMAXPROCS(num - 1)
31     go test()
32     for i := 0; i < 2; i++ {
33         go calc()
34     }
35 
36     time.Sleep(time.Second * 10000)
37 }
recover示例2

2. 信道(Channel

     Channel 可以想像成 Go 协程之间通信的管道。如同管道中的水会从一端流到另一端,通过使用信道,数据也可以从一端发送,在另一端接收。

    (1)channel概念

  • 类似unix中管道(pipe)
  • 先进先出
  • 线程安全,多个goroutine同时访问,不需要加锁
  • channel是有类型的, 一个整数的channel只能存放整数

    (2) channel声明 

var 变量名 chan 类型,例如:
var test chan int 
var test chan string 
var test chan map[string]string 
var test chan stu   //stu是一个结构体

     注意:所有信道都关联了一个类型。信道只能运输这种类型的数据,而运输其他类型的数据都是非法的。

 1 package main 
 2 
 3 import "fmt"
 4 
 5 func main() {
 6     var ch chan int
 7     // ch = make(chan int, 1)
 8     ch<-1
 9     var num int
10     num = <-ch
11     fmt.Println(num)
12 }
声明未初始化信道

     运行结果:出现死锁

     

    (3)channel初始化

使用make进行初始化,例如: 
var test chan int
test = make(chan int, 10) 
var test chan string
test = make(chan string, 10)

     上面的程序声明了信道但是未初始化,去掉上面程序的注释初始化信道,执行结果:输出1
     注意:chan T 表示 T 类型的信道。
                信道的零值为 nil。信道的零值没有什么用,应该像对 map 和切片所做的那样,用 make 来定义信道。
     例如:

 1 package main
 2 
 3 import "fmt"
 4 
 5 func main() {  
 6     var ch chan int
 7     if ch == nil {
 8         fmt.Println("channel a is nil, going to define it")
 9         ch = make(chan int)
10         fmt.Printf("Type of a is %T", ch)
11     }
12 }
信道的声明

     快速声明一个信道:

ch := make(chan int)

    (4)channel基本操作

       信道旁的箭头方向指定了是发送数据还是接收数据

  • 从channel读取数据:

          var testChan chan int
          testChan = make(chan int, 10)
          var a int
          a = <- testChan  //箭头对于 testChan 来说是向外指的,因此我们读取了信道 testChan 的值,并把该值存储到变量 a 中。

  • 从channel写 入数据:

          var testChan chan int
          testChan = make(chan int, 10)
          var a int = 10
          testChan <- a   //箭头指向了 testChan,因此我们在把数据写入信道 testChan。
    (5)带缓冲区的channel

        对于无缓冲信道的发送和接收过程是阻塞的。而对于有缓冲信道,只在缓冲已满的情况,才会阻塞向缓冲信道(Buffered Channel)发送数据。同样,只有在缓冲为空的时候,才会阻塞从缓冲信道接收数据。

ch := make(chan type, capacity)
有缓冲信道:capacity 应该大于 0
无缓冲信道:capacity为0,或者不设置capacity则容量默认也为 0
  • testChan只能放 一个元素:

   var testChan chan int
   testChan = make(chan int)
   var a int
   a = <- testChan

  • testChan是带缓冲区的chan, 一次可以放10个元素:

     var testChan chan int
     testChan = make(chan int, 10)
     var a int = 10
     testChan <- a

 1 package main
 2 
 3 import (  
 4     "fmt"
 5 )
 6 
 7 func main() {  
 8     chStr := make(chan string, 2)
 9     chStr <- "zhangsan"
10     chStr <- "lisi"
11     fmt.Println(<-chStr)
12     fmt.Println(<-chStr)
13 
14     chInt := make(chan int, 2)
15     chInt <- 10
16     chInt <- 20
17     fmt.Println(<-chInt)
18     fmt.Println(<-chInt)
19 }
带缓冲区的channel
 1 package main
 2 
 3 import (  
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func write(ch chan int) {  
 9     for i := 0; i < 5; i++ {
10         ch <- i //当存入第三个数时会阻塞住,直到信道ch里面有数据被取走
11         fmt.Printf("Write %d to ch\n", i)
12     }
13     close(ch) //关闭信道ch
14 }
15 func main() {  
16     ch := make(chan int, 2) //信道一次最多存入两个数
17     go write(ch)  
18     time.Sleep(2 * time.Second) //等待
19     for v := range ch {
20         fmt.Printf("read value %d from ch\n", v)
21         time.Sleep(time.Second)
22 
23     }
24 }
25 
26 // 执行结果:
27 // Write 0 to ch
28 // Write 1 to ch  //先往信道里面写入两个数,阻塞
29 // read value 0 from ch //取走一个
30 // Write 2 to ch  //立即往信道写入一个数
31 // read value 1 from ch
32 // Write 3 to ch
33 // read value 2 from ch
34 // Write 4 to ch
35 // read value 3 from ch
36 // read value 4 from ch
带缓冲区的chennel2
 1 package main
 2 
 3 import "fmt"
 4 
 5 type student struct {
 6     name string
 7 }
 8 
 9 func main() {
10 
11     var stuChan chan interface{}
12     stuChan = make(chan interface{}, 10)
13 
14     stu := student{name: "stu01"}
15 
16     stuChan <- &stu
17 
18     var stu01 interface{}
19     stu01 = <-stuChan
20 
21     var stu02 *student
22     stu02, ok := stu01.(*student) //stu01转为*student类型
23     if !ok {
24         fmt.Println("can not convert")
25         return
26     }
27 
28     fmt.Println(stu02)
29 }
example
 1 package main
 2 
 3 import (
 4     "fmt"
 5     "sync"
 6     "time"
 7 )
 8 
 9 var wg sync.WaitGroup
10 
11 func consumer(goods chan string) {
12     for i := 0; i < 10; i++ {
13         g, ok := <-goods
14         if !ok {
15             fmt.Println("produce done ", g)
16         }
17         fmt.Println("consumer ", g)
18         time.Sleep(20*time.Millisecond)
19     }
20 
21     wg.Done()
22 }
23 
24 func produce(goods chan string) {
25     for i := 0; i < 10; i++ {
26         g := fmt.Sprintf("baozi%d", i)
27         goods <- g
28         fmt.Println("produce ", g)
29         time.Sleep(10*time.Millisecond)
30     }
31     close(goods) //生产完毕
32 
33     wg.Done()
34 }
35 
36 func main() {
37     var goods chan string
38     goods = make(chan string, 10)
39     
40     wg.Add(2)
41     go produce(goods)
42     go consumer(goods)
43 
44     wg.Wait()
45 }
生产者消费者模型
 1 package main
 2 
 3 import (
 4     "fmt"
 5     "time"
 6 )
 7 
 8 func write(ch chan int) {
 9     for i := 0; i < 100; i++ {
10         ch <- i
11         fmt.Println("put data:", i)
12     }
13 }
14 
15 func read(ch chan int) {

                      

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
go操作json,注意json的嵌套、数组发布时间:2022-07-10
下一篇:
Windows Go 开发环境下载、安装并配置发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap