Go语言之Goroutine与信道异常处理

目录
  • 一、Goroutine
    • 1、启动一个 Goroutine
    • 2、Go 语言的GMP模型
  • 二、信道
    • 1、死锁
    • 2、单向信道
    • 3、for 循环信道
    • 4、缓冲信道
    • 5、WaitGroup
    • 6、Select
    • 7、Mutex
  • 三、异常处理

一、Goroutine

Go 协程可以看做成一个轻量级的线程,Go 协程相比于线程的优势:

Goroutine 的成本更低大小只有 2 kb 左右,线程有几个兆。

Goroutine 会复用线程,比如说:我有 100 个协程,但是都是共用的的 3 个线程。

Goroutine 之间通信是通过 channel 通信的。(Go 推崇的是信道通信,而不推崇用共享变量通信)

1、启动一个 Goroutine

func test() {
   fmt.Println("go go go")
}

func main() {
   fmt.Println("主线程开始")

   // go 关键字开启 Goroutine,一个 Goroutine只占2kb左右
   go test() // 一个 go 就是一个协程,多个就是多个协程,也可以for循环起多个协程
   go test()

   time.Sleep(1*time.Second)  // Go 语言中主线程不会等待Goroutine执行完成,要等待它结束需要自己处理
   fmt.Println("主线程结束")
}

// 输出:
主线程开始
go go go
go go go
主线程结束

2、Go 语言的GMP模型

G:就是我们开起的 Goroutine

  • 它们会被相对平均的放到 P 的队列中

M:M 可以当成操作系统真正的线程,但是实际上是用户态的线程(用户线程)

  • 虽然 M 执行 G,但是实际上,M 只是映射到操作系统的线程上执行的
  • 然后操作系统的调度器把真正的操作系统的线程调度到CPU上执行

P:Processor 1.5版本以后默认情况是 CPU 核数(可以当做CPU核数)

  • P 会和 M 做交互,真正执行的是 M 在执行 G ,只不过 P 是在做调度
  • 一旦某个 G 阻塞了,那么 P 就会把它调度成下一个 G,放到 M 里面去执行

用户线程操作系统线程:

Python 中,用户线程跟操作系统线程是 1:1 的对应关系

Go 语言中,用户线程和操作系统线程是 m:n 的关系

二、信道

信道(Channel)也就是 Go 协程之间的通信管道,一端发送一端接收。

func main() {
   // 1、定义 channel
   var c chan int

   // 2、管道的零值
   //———>空值为 nil 说明是引用类型,当做参数传递时,不需要取地址,改的就是原来的,需要初始化在使用
   fmt.Println(c)     // 输出:<nil>

   // 3、管道初始化
   c = make(chan int)
   go test(c)        // c 是引用类型直接传

   // 4、从信道中取值,信道默认不管放值还是取值,都是阻塞的
   count := <-c   // 阻塞
   fmt.Println(count)

   /*
   当程序执行 go test(c) 时就开了一个 Goroutine
   然后继续执行到 count := <-c 从信道取值,这时就阻塞住了
   它会等待 Goroutine 往信道中放值后才会取出值,才会继续执行 fmt.Println(count)
   */
}

func test(c chan int) {
   fmt.Println("GO GO GO")
   time.Sleep(1 * time.Second)
   // 5、往信道中放一个值,信道默认不管放值还是取值,都是阻塞的
   c <- 1 // 阻塞
}

// 输出:
<nil>
GO GO GO
1

1、死锁

Goroutine 给一个信道放值的时候,按理会有其他 Goroutine 来接收数据,没有的话就会形成死锁。

func main() {
    c := make(chan int)
    c <- 1
}

// 报错:应为没有其他 Goroutine 从 c 中取值

2、单向信道

显而易见就是只能读或者只能写的信道

方式一:

func WriteOnly(c chan<- int) {
   c <- 1
}

func main() {
   write := make(chan<- int)  // 只写信道
   go WriteOnly(write)
   fmt.Println(<-write)      // 报错   ——>只写信道往外取就报错
}

方式二:

func WriteOnly(c chan<- int) {
   c <- 1
   // <-c       // 报错
}

func main() {
   write := make(chan int)    // 定义一个可读可写信道
   go WriteOnly(write)        // 传到函数中就成了只写信道,在Goroutine中只负责写,不能往外读
   fmt.Println(<-write)     // 主协程读
}

3、for 循环信道

for 循环循环信道,如果不关闭,会报死锁,如果关闭了,放不进去,循环结束。

func producer(chnl chan int) {
   for i := 0; i < 10; i++ {
      chnl <- i   // i 放入信道
   }
   close(chnl)      // 关闭信道
}

func main() {
   ch := make(chan int)
   go producer(ch)
   // 循环获取信道内容
   for value := range ch {
      fmt.Println(value)
   }
}

/*
当 for 循环 range ch 的时候信道没有值,会阻塞等待 go producer(ch) 开起的 Goroutine 中放入值
当 Goroutine 中放入一个值,就会阻塞,那么 range ch 就会取出一个值,然后再次阻塞等待
直到 Goroutine 放入值完毕关闭信道,for 循环 range ch 也就结束循环了
*/

4、缓冲信道

在默认情况下信道是阻塞的,缓冲信道也就是说我信道里面可以缓冲一些东西,可以不阻塞了。

只有在缓冲已满的情况,才会阻塞信道

只有在缓冲为空的时候,才会阻塞主缓冲信道接收数据

func main() {
   // 指定的数字就是缓冲大小
   var c chan int = make(chan int, 3) // 无缓冲信道数字是0
   c <- 1
   c <- 2
   c <- 3
   c <- 4     // 缓冲满了,死锁

   <-c
   <-c
   <-c
   <-c          // 取空了,死锁

   fmt.Println(len(c))       // 长度:目前放了多少
   fmt.Println(cap(c))       // 容量:可以最多放多少
}

5、WaitGroup

等待所有 Goroutine 执行完成

func process1(i int, wg *sync.WaitGroup) {
   fmt.Println("started Goroutine ", i)
   time.Sleep(2 * time.Second)
   fmt.Printf("Goroutine %d ended\n", i)
   // 3、一旦有一个完成,减一
   wg.Done()
}

func main() {
   var wg sync.WaitGroup    // 没有初始化,值类型,当做参数传递,需要取地址

   for i := 0; i < 10; i++ {
      wg.Add(1)      // 1、启动一个 Goroutine,add 加 1
      go process1(i, &wg)     // 2、把wg传过去,因为要改它并且它是值类型需要取地址传过去
   }

   wg.Wait()  // 4、一直阻塞在这,直到调用了10个 Done,计数器减到零
}

6、Select

Select 语句用于在多个发送 / 接收信道操作中进行选择。

例如:我要去爬百度,我发送了三个请求去,可能有一些网络原因,或者其他原因,不一定谁先回来,Select 选择就是谁先回来我先用谁。

场景一:对性能极致的要求,我就可以选择一个最快的线路执行我最快的功能,就可以用Select来做

场景二:我去拿这个数据的时候,不是一直等在这里,而是我可以干一点别的事情,使用死循环 Select 的时候加上 default 去做其他事情。

// 模拟去服务器去取值
func server(ch chan string) {
   time.Sleep(3 * time.Second)
   ch <- "from server"
}

func main() {
   output1 := make(chan string)
   output2 := make(chan string)

   // 开起两个协程执行 server
   go server(output1)
   go server(output2)

   select {
   case s1 := <-output1:  // 阻塞,谁先回来就执行谁
      fmt.Println(s1, "output1")
   case s2 := <-output2:  // 阻塞,谁先回来就执行谁
      fmt.Println(s2, "output2")
   }
}

7、Mutex

使用锁的场景:多个 Goroutine 通过共享内存来实现数据通信,就会出现并发安全的问题,并发安全的问题就需要加锁。

临界区:当程序并发运行时修改共享资源的代码,也就同一块内存的变量的时候,这些修改的资源的代码就称为临界区。

如果在任意时刻只允许一个 Goroutine 访问临界区,那么就可以避免竞争条件,而使用 Mutex(锁) 可以实现

不加用锁的情况下:

var x = 0   //全局,各个 Goroutine 都可以拿到并且操作

func increment(wg *sync.WaitGroup) {
   x = x + 1
   wg.Done()
}

func main() {
   var w sync.WaitGroup
   for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w)
   }
   w.Wait()

   fmt.Println("最终的值:", x)
}

// 输出:理想情况下是1000,因为并发有安全的问题,所以数据乱了
最终的值: 978

加锁的情况:

var x = 0   //全局,各个 Goroutine 都可以拿到并且操作

func increment(wg *sync.WaitGroup, m *sync.Mutex) {
   m.Lock()    // 加锁
   x = x + 1 // 同一时间只能有一个 Goroutine 执行
   m.Unlock()  // 解锁
   wg.Done()
}
func main() {
   var w sync.WaitGroup
   var m sync.Mutex    // 因为是个值类型,函数传递需要传地址
   fmt.Println(m)     // 输出:{0 0} ——>值类型

   for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, &m)
   }
   w.Wait()
   fmt.Println("最终的值:", x)
}

// 输出:
最终的值: 1000

使用信道来实现:

var x = 0

func increment(wg *sync.WaitGroup, ch chan bool) {
   ch <- true   // 缓冲信道放满了,就会阻塞。
   x = x + 1
   <-ch         // 执行完了就取出
   wg.Done()
}
func main() {
   var w sync.WaitGroup
   ch := make(chan bool, 1)   // 定义了一个有缓存大小为1的信道
   for i := 0; i < 1000; i++ {
      w.Add(1)
      go increment(&w, ch)
   }

   w.Wait()
   fmt.Println("最终的值:", x)
}

// 输出:
最终的值:1000

总结:不同 Goroutine 之间传递数据的方式:共享变量、信道。

如果是修改变量,倾向于用 Mutex

如果是 Goroutine 之间通信,倾向于用信道

三、异常处理

defer:延时执行,并且即便程序出现严重错误,也会执行

func main() {
 defer fmt.Println("我最后执行")
 defer fmt.Println("我倒数第三执行")
 fmt.Println("我先执行")

 var a []int
 fmt.Println(a[10])   // 报错

 fmt.Println("后执行") // 不会执行了
}

// 输出:
我先执行
我倒数第三执行
我最后执行
panic: runtime error: index out of range [10] with length 0
panic:主动抛出异常

recover:恢复程序,继续执行

func f1() {
   fmt.Println("f1 f1")
}

func f2() {
   defer func() {        // 这个匿名函数永远会执行

      //如果没有错误,执行 recover 会返回 nil,如果有错误执行 recover 会返回错误信息
      if error := recover(); error != nil {
         // 表示出错了,打印一下错误信息,程序恢复了,继续执行
         fmt.Println(error)
      }
      fmt.Println("我永远会执行,不管是否出错")
   }()

   fmt.Println("f2 f2")
   panic("主动抛出错误")
}

func f3() {
   fmt.Println("f3 f3")
}

func main() {
   //捕获异常,处理异常,让程序继续运行
   f1()
   f2()
   f3()
}

Go 语言异常捕获与 Python 异常捕获对比

Python:

try:
 可能会错误的代码
except Exception as e:
 print(e)
finally:
 无论是否出错,都会执行的代码

Go :

defer func() {
 if error:=recover();error!=nil{
  // 错误信息 error
  fmt.Println(error)
 }

 相当于finally,无论是否出错,都会执行的代码

}()

可能会错误的代码

到此这篇关于Go语言之Goroutine与信道异常处理的文章就介绍到这了,更多相关Go语言Goroutine与信道异常处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Go语言之Goroutine与信道异常处理

    目录 一.Goroutine 1.启动一个 Goroutine 2.Go 语言的GMP模型 二.信道 1.死锁 2.单向信道 3.for 循环信道 4.缓冲信道 5.WaitGroup 6.Select 7.Mutex 三.异常处理 一.Goroutine Go 协程可以看做成一个轻量级的线程,Go 协程相比于线程的优势: Goroutine 的成本更低大小只有 2 kb 左右,线程有几个兆. Goroutine 会复用线程,比如说:我有 100 个协程,但是都是共用的的 3 个线程. Goro

  • go语言编程之select信道处理示例详解

    目录 select信道处理 fibonacci数列监听 select监听协程 select信道处理 注意:有default就不会阻塞 package main func main() { var chan1 = make(chan int) var chan2 = make(chan int) select { case <-chan1: // 如果chan1成功读到数据,则进行该case处理语句 case chan2: // 如果chan2成功读到数据,则进行该case处理语句 default

  • Go语言使用goroutine及通道实现并发详解

    目录 使用通道接收数据 阻塞接收数据 非阻塞接收数据 接收任意数据,忽略掉接收的数据 循环接收数据 使用通道接收数据 在上一篇文章中介绍了通道以及使用通道发送数据,本篇接着了解通道的基本内容,如何使用通道接收数据: 通道的接收同样使用"<-"操作符: 使用通道接收数据的特性如下: 通道的发送和接收操作在不同的两个goroutine间进行,由于通道中的数据在没有接收方接收时会持续阻塞,所以通道的接收必定在另外一个goroutine中进行: 如果在接收方接收时,通道中没有发送方发送数

  • C语言异常处理机制案例讲解

    异常处理机制:setjmp()函数与longjmp()函数 C标准库提供两个特殊的函数:setjmp() 及 longjmp(),这两个函数是结构化异常的基础,正是利用这两个函数的特性来实现异常. 所以,异常的处理过程可以描述为这样: 首先设置一个跳转点(setjmp() 函数可以实现这一功能),然后在其后的代码中任意地方调用 longjmp() 跳转回这个跳转点上,以此来实现当发生异常时,转到处理异常的程序上,在其后的介绍中将介绍如何实现. setjmp() 为跳转返回保存现场并为异常提供处理

  • Go语言数据类型详细介绍

    目录 一.Go 语言两大数据类型 二.基础类型 三.复合类型 指针 四.数组(array) 五.切片(slice) 六.字典/映射(map) 七.通道(channel) 八.结构体(struct) 九.接口(interface) 十.错误(error) 一.Go 语言两大数据类型 Go 语言数据类型包含基础类型和复合类型两大类. 基础类型包括: 布尔类型 bool. 数值类型 int,int8,int16,int32,int64,float32,float64. uint8,uint16,uin

  • Go语言中并发的工作原理

    一.Go语言中Goroutine的基本原理 Go语言里的并发指的是能让某个函数独立于其他函数运行的能力. Go语言的goroutine是一个独立的工作单元, Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP)的范型(paradigm). CSP 是一种消息传递模型,通过在goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问.消息的传递通过Go语言中的Channel(通道)来实现. 进程(pro

  • Java基础概述面试题复习

    目录 Java概述 何为编程 什么是Java jdk1.5之后的三大版本 JVM.JRE和JDK的关系 JVM JRE JDK 什么是跨平台性?原理是什么 Java语言有哪些特点 什么是字节码?采用字节码的最大好处是什么 什么是Java程序的主类?应用程序和小程序的主类有何不同? Java应用程序与小程序之间有那些差别? Java和C++的区别 Oracle JDK 和 OpenJDK 的对比 总结 Java概述 何为编程 编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终

  • 一文教你如何封装安全的go

    目录 前言 封装 SafeGo SafeGoAndWait 实现说明 首先是接口设计方面 其次是日志兼容hade 最后是打印panic的trace记录 总结 前言 在业务代码开发过程中,我们会有很大概率使用go语言的goroutine来开启一个新的goroutine执行另外一段业务,或者开启多个goroutine来并行执行多个业务逻辑.所以我为hade框架增加了两个方法goroutine.SafeGo 和 goroutine.SafeGoAndWait. 封装 SafeGo SafeGo 这个函

  • Go 并发编程协程及调度机制详情

    目录 协程的概念 goroutine 的诞生 使用 goroutine 加快速度 goroutine 的机制原理 前言: 协程(coroutine)是 Go 语言最大的特色之一,goroutine 的实现其实是通过协程. 协程的概念 协程一词最早出现在 1963 年发表的论文中,该论文的作者为美国计算机科学家 Melvin E.Conway.著名的康威定律:“设计系统的架构受制于产生这些设计的组织的沟通结构.” 也是这个作者. 协程是一种用户态的轻量级线程,可以想成一个线程里面可以有多个协程,而

  • GO语言异常处理机制panic和recover分析

    本文实例分析了GO语言异常处理机制panic和recover.分享给大家供大家参考.具体如下: Golang 有2个内置的函数 panic() 和 recover(),用以报告和捕获运行时发生的程序错误,与 error 不同,panic-recover 一般用在函数内部.一定要注意不要滥用 panic-recover,可能会导致性能问题,我一般只在未知输入和不可靠请求时使用. golang 的错误处理流程:当一个函数在执行过程中出现了异常或遇到 panic(),正常语句就会立即终止,然后执行 d

随机推荐