热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Go语言中Goroutine与通道机制及其异常处理深入解析

在Go语言中,Goroutine可视为一种轻量级的并发执行单元,其资源消耗远低于传统线程,初始栈大小仅为2KB,而普通线程则通常需要几MB。此外,Goroutine的调度由Go运行时自动管理,能够高效地支持成千上万个并发任务。本文深入探讨了Goroutine的工作原理及其与通道(channel)的配合使用,特别是在异常处理方面的最佳实践,为开发者提供了一套完整的解决方案,以确保程序的稳定性和可靠性。

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

一、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)     // 输出:

   // 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 // 阻塞
}

// 输出:

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
", 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,无论是否出错,都会执行的代码
    
}()
可能会错误的代码

学习之旅

推荐阅读
  • IIS 7及7.5版本中应用程序池的最佳配置策略与实践
    在IIS 7及7.5版本中,优化应用程序池的配置是提升Web站点性能的关键步骤。具体操作包括:首先定位到目标Web站点的应用程序池,然后通过“应用程序池”菜单找到对应的池,右键选择“高级设置”。在一般优化方案中,建议调整以下几个关键参数:1. **基本设置**: - **队列长度**:默认值为1000,可根据实际需求调整队列长度,以提高处理请求的能力。此外,还可以进一步优化其他参数,如处理器使用限制、回收策略等,以确保应用程序池的高效运行。这些优化措施有助于提升系统的稳定性和响应速度。 ... [详细]
  • 探讨 `org.openide.windows.TopComponent.componentOpened()` 方法的应用及其代码实例分析 ... [详细]
  • Go语言实现Redis客户端与服务器的交互机制深入解析
    在前文对Godis v1.0版本的基础功能进行了详细介绍后,本文将重点探讨如何实现客户端与服务器之间的交互机制。通过具体代码实现,使客户端与服务器能够顺利通信,赋予项目实际运行的能力。本文将详细解析Go语言在实现这一过程中的关键技术和实现细节,帮助读者深入了解Redis客户端与服务器的交互原理。 ... [详细]
  • MySQL:不仅仅是数据库那么简单
    MySQL不仅是一款高效、可靠的数据库管理系统,它还具备丰富的功能和扩展性,支持多种存储引擎,适用于各种应用场景。从简单的网站开发到复杂的企业级应用,MySQL都能提供强大的数据管理和优化能力,满足不同用户的需求。其开源特性也促进了社区的活跃发展,为技术进步提供了持续动力。 ... [详细]
  • RancherOS 是由 Rancher Labs 开发的一款专为 Docker 设计的轻量级 Linux 发行版,提供了一个全面的 Docker 运行环境。其引导镜像仅 20MB,非常适合在资源受限的环境中部署。本文将详细介绍如何在 ESXi 虚拟化平台上安装和配置 RancherOS,帮助用户快速搭建高效、稳定的容器化应用环境。 ... [详细]
  • 在Windows命令行中,通过Conda工具可以高效地管理和操作虚拟环境。具体步骤包括:1. 列出现有虚拟环境:`conda env list`;2. 创建新虚拟环境:`conda create --name 环境名`;3. 删除虚拟环境:`conda env remove --name 环境名`。这些命令不仅简化了环境管理流程,还提高了开发效率。此外,Conda还支持环境文件导出和导入,方便在不同机器间迁移配置。 ... [详细]
  • Envoy 流量分配策略优化
    在本研究中,我们对Envoy的流量分配策略进行了优化,旨在提高系统的稳定性和性能。实验环境包括一个前端代理服务(Envoy,IP地址为172.31.57.10)和五个后端服务。通过调整Envoy的配置,实现了更高效的流量分发和负载均衡,显著提升了整体系统的响应速度和可靠性。 ... [详细]
  • 如何在Java中高效构建WebService
    本文介绍了如何利用XFire框架在Java中高效构建WebService。XFire是一个轻量级、高性能的Java SOAP框架,能够简化WebService的开发流程。通过结合MyEclipse集成开发环境,开发者可以更便捷地进行项目配置和代码编写,从而提高开发效率。此外,文章还详细探讨了XFire的关键特性和最佳实践,为读者提供了实用的参考。 ... [详细]
  • 尽管存在唯一列,仍显示“当前选择不包含唯一列。网格编辑、复选框、编辑、复制和删除功能不可用”的消息。 ... [详细]
  • 在软件开发领域,“池”技术被广泛应用,如数据库连接池、线程池等。本文重点探讨Java中的线程池ThreadPoolExecutor,通过详细解析其内部机制,帮助开发者理解如何高效利用线程池管理任务执行。线程池不仅能够显著减少系统资源的消耗,提高响应速度,还能通过合理的配置,如饱和策略,确保在高负载情况下系统的稳定性和可靠性。文章还将结合实际案例,展示线程池在不同应用场景下的具体实现与优化技巧。 ... [详细]
  • MySQL 数据操作:增、删、查、改全面解析
    MySQL 数据操作:增、删、查、改全面解析 ... [详细]
  • Windows环境下详细教程:如何搭建Git服务
    Windows环境下详细教程:如何搭建Git服务 ... [详细]
  • 深入RTOS实践,面对原子操作提问竟感困惑
    在实时操作系统(RTOS)的实践中,尽管已经积累了丰富的经验,但在面对原子操作的具体问题时,仍感到困惑。本文将深入探讨RTOS中的原子操作机制,分析其在多任务环境下的重要性和实现方式,并结合实际案例解析常见的问题及解决方案,帮助读者更好地理解和应用这一关键技术。 ... [详细]
  • MySQL日志分析在应急响应中的应用与优化策略
    在应急响应中,MySQL日志分析对于检测和应对数据库攻击具有重要意义。常见的攻击手段包括弱口令、SQL注入、权限提升和备份数据窃取。通过对MySQL日志的深入分析,不仅可以及时发现潜在的攻击行为,还能详细还原攻击过程并追踪攻击源头。此外,优化日志记录和分析策略,能够提高安全响应效率,增强系统的整体安全性。 ... [详细]
  • 在Matlab中,我尝试构建了一个神经网络模型,用于预测函数 y = x^2。为此,我设计并实现了一个拟合神经网络,并对其进行了详细的仿真和验证。通过调整网络结构和参数,成功实现了对目标函数的准确估计。此外,还对模型的性能进行了全面评估,确保其在不同输入条件下的稳定性和可靠性。 ... [详细]
author-avatar
13888102467波光_1984
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有