首页 > 其他分享 >【学习笔记】go协程和通道

【学习笔记】go协程和通道

时间:2023-04-09 22:06:00浏览次数:40  
标签:接收 协程 函数 goroutine 笔记 Go go main 通道


虽然,线程池为逻辑编写者提供了线程分配的抽象机制。但是,如果面对随时随地可能发生的并发和线程处理需求,线程池就不是非常直观和方便了。能否有一种机制:使用者分配足够多的任务,系统能自动帮助使用者把任务分配到CPU上,让这些任务尽量并发运作。这种机制在Go语言中被称为goroutine。goroutine的概念类似于线程,但goroutine由Go程序运行时的调度和管理。Go程序会智能地将goroutine中的任务合理地分配给每个CPU。 Go程序从main包的main()函数开始,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。

Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。

Go 协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

Go程序中使用go关键字为一个函数创建一个goroutine。一个函数可以被创建多个goroutine,一个goroutine必定对应一个函数。

使用普通函数创建goroutine

为一个普通函数创建goroutine的写法如下:
go 函数名( 参数列表 )

使用go关键字创建goroutine时,被调用函数的返回值会被忽略。
例子 使用go关键字,将running()函数并发执行,每隔一秒打印一次计数器,而main的goroutine则等待用户输入,两个行为可以同时进行。请参考下面代码:
例一

package main
        
        import (
                "fmt"
                "time"
        )
        
        func running() {
                
                var times int
                // 构建一个无限循环
                for {
                        times++
                        fmt.Println("tick", times)
        
                        // 延时1秒
                        time.Sleep(time.Second)
                }
        
        }
        
        func main() {
        
                // 并发执行程序
               go running()    
                // 接受命令行输入,不做任何事情
                var input string
                fmt.Scanln(&input)
        }

这个例子中,Go程序在启动时,运行时(runtime)会默认为main()函数创建一个goroutine。在main()函数的goroutine中执行到go running语句时,归属于running()函数的goroutine被创建,running()函数开始在自己的goroutine中执行。此时,main()继续执行,两个goroutine通过Go程序的调度机制同时运作。

例二:

【学习笔记】go协程和通道_学习


结果:

【学习笔记】go协程和通道_Go_02

输出的效果说明, main 这个主线程和test 协程同时执行.主线程和协程执行流程:

【学习笔记】go协程和通道_Go_03

总结:

  1. 主线程是一个物理线程,直接作用在cpu 上的。是重量级的,非常耗费cpu 资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang 在并发上的优势了

设置运行的cpu的数目:

【学习笔记】go协程和通道_数据_04

使用匿名函数创建goroutine

1.使用匿名函数创建goroutine的格式
使用匿名函数或闭包创建goroutine时,除了将函数定义部分写在go的后面之外,还需要加上匿名函数的调用参数,格式如下:
go func( 参数列表 ){
函数体
}( 调用参数列表 )
·参数列表:函数体内的参数变量列表。
·函数体:匿名函数的代码。
·调用参数列表:启动goroutine时,需要向匿名函数传递的调用参数。
2.使用匿名函数创建goroutine的例子
在main()函数中创建一个匿名函数并为匿名函数启动goroutine。匿名函数没有参数。代码将并行执行定时打印计数的效果。参见下面的代码:

01        package main
02        
03        import (
04                "fmt"
05                "time"
06        )
07        
08        func main() {
09        
10                go func() {
11        
12                        var times int
13        
14                        for {
15     times++
16      fmt.Println("tick", times)
17        
18         time.Sleep(time.Second)
19                        }
20        
21                }()
22        
23                var input string
24                fmt.Scanln(&input)
25        }

goroutine虽然类似于线程概念,但是从调度性能上没有线程细致,而细致程度取决于Go程序的goroutine调度器的实现和运行环境。 终止goroutine的最好方法就是自然返回goroutine对应的函数。虽然可以用golang.org/x/net/context包进行goroutine生命期深度控制,但这种方法仍然处于内部试验阶段,并不是官方推荐的特性。

Go语言的协作程序(goroutine)和普通的协作程序(coroutine)

C#、Lua、Python语言都支持coroutine特性。coroutine与goroutine在名字上类似,都可以将函数或者语句在独立的环境中运行,但是它们之间有两点不同:
·goroutine可能发生并行执行;但coroutine始终顺序执行。 狭义地说,goroutine可能发生在多线程环境下,goroutine无法控制自己获取高优先度支持;coroutine始终发生在单线程,coroutine程序需要主动交出控制权,宿主才能获得控制权并将控制权交给其他coroutine。

·goroutine间使用channel通信;coroutine使用yield和resume操作。
goroutine和coroutine的概念和运行机制都是脱胎于早期的操作系统。 coroutine的运行机制属于协作式任务处理,早期的操作系统要求每一个应用必须遵守操作系统的任务处理规则,应用程序在不需要使用CPU时,会主动交出CPU使用权。如果开发者无意间或者故意让应用程序长时间占用CPU,操作系统也无能为力,表现出来的效果就是计算机很容易失去响应或者死机。 goroutine属于抢占式任务处理,已经和现有的多线程和多进程任务处理非常类似。应用程序对CPU的控制最终还需要由操作系统来管理,操作系统如果发现一个应用程序长时间大量地占用CPU,那么用户有权终止这个任务。

通道

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。 Go语言提倡使用通信的方法代替共享内存,这里通信的方法就是使用通道(channel)

【学习笔记】go协程和通道_数据_05

Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个goroutine访问通道进行发送和获取数据。goroutine间通过通道就可以通信。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

声明通道类型

通道本身需要一个类型进行修饰,就像切片类型需要标识元素类型。通道的元素类型就是在其内部传输的数据类型,声明如下:

var 通道变量 chan 通道类型

·通道类型:通道内的数据类型。
·通道变量:保存通道的变量。 chan类型的空值是nil,声明后需要配合make后才能使用。
创建通道
通道是引用类型,需要使用make进行创建,格式如下:
通道实例 := make(chan 数据类型)

ch1 := make(chan int)  // 创建一个整型类型的通道
ch2 := make(chan interface{}) // 创建一个空接口类型的通道,可以存放任意格式

type Equip struct{ /* 一些字段 */ }
ch2 := make(chan *Equip)  // 创建Equip指针类型的通道,可以存放*Equip
//1. 创建一个可以存放3 个int 类型的管道
var intChan chan int
intChan = make(chan int, 3)

通道的发送使用特殊的操作符“<-”,将数据通过通道发送的格式为:
通道变量 <- 值
·通道变量:通过make创建好的通道实例。
·值:可以是变量、常量、表达式或者函数返回值等。值的类型必须与ch通道的元素类型一致。

通过通道发送数据的例子

使用make创建一个通道后,就可以使用“<-”向通道发送数据,代码如下:

// 创建一个空接口通道
ch := make(chan interface{})
// 将0放入通道中
ch <- 0
// 将hello字符串放入通道中
ch <- "hello"

发送将持续阻塞直到数据被接收

把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go程序运行时能智能地发现一些永远无法发送成功的语句并做出提示,代码如下:

func main() {
        // 创建一个整型通道
        ch := make(chan int)

    // 尝试将0通过通道发送
        ch <- 0
}

运行代码,报错: fatal error: all goroutines are asleep - deadlock!
报错的意思是:运行时发现所有的goroutine(包括main)都处于等待goroutine。也就是说所有goroutine中的channel并没有形成发送和接收对应的代码。

使用通道接收数据

通道接收同样使用“<-”操作符,通道接收有如下特性
·通道的收发操作在不同的两个goroutine间进行。 由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个goroutine中进行。
·接收将持续阻塞直到发送方发送数据。 如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
·每次接收一个元素。 通道一次只能接收一个数据元素。 通道的数据接收一共有以下4种写法。
1.阻塞接收数据 阻塞模式接收数据时,将接收变量作为“<-”操作符的左值,格式如下:
data := <-ch
执行该语句时将会阻塞,直到接收到数据并赋值给data变量。
2.非阻塞接收数据
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:
data, ok := <-ch
·data:表示接收到的数据。未接收到数据时,data为通道类型的零值。
·ok:表示是否接收到数据。 非阻塞的通道接收方法可能造成高的CPU占用,因此使用非常少。如果需要实现接收超时检测,可以配合select和计时器channel进行,可以参见后面的内容。
3.接收任意数据,忽略接收的数据

阻塞接收数据后,忽略从通道返回的数据,格式如下:
<-ch
执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在goroutine间阻塞收发实现并发同步。

01        package main
02        
03        import (
04                "fmt"
05        )
06        
07        func main() {
08        
09                // 构建一个通道
10                ch := make(chan int)
11        
12                // 开启一个并发匿名函数
13                go func() {
14        
15                        fmt.Println("start goroutine")
16        
17                        // 通过通道通知main的goroutine
18                        ch <- 0
19        
20                        fmt.Println("exit goroutine")
21        
22                }()
23        
24                fmt.Println("wait goroutine")
25        
26                // 等待匿名goroutine
27                <-ch
28        
29                fmt.Println("all done")
30        
31        }

代码说明如下:
·第10行,构建一个同步用的通道。
·第13行,开启一个匿名函数的并发。
·第18行,匿名goroutine即将结束时,通过通道通知main的goroutine,这一句会一直阻塞直到main的goroutine接收为止。
·第27行,开启goroutine后,马上通过通道等待匿名goroutine结束。 执行代码,输出如下:
wait goroutine
start goroutine
exit goroutine
all done
4.循环接收
通道的数据接收可以借用for range语句进行多个元素的接收操作,格式如下:
for data := range ch {

}
通道ch是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过for遍历获得的变量只有一个,即上面例子中的data。

代码:

import (
	"fmt"
	"time"
)
func main() {
	// 构建一个通道
	ch := make(chan int)
	// 开启一个并发匿名函数
	go func() {
		// 从3循环到0
		for i := 3; i >= 0; i-- {
			// 发送3到0的数值
			ch <- i
			// 每次发送完时等待
			time.Sleep(time.Second)
		}
	}()
	// 遍历接收通道数据
	for data := range ch {
		// 打印通道数据
		fmt.Println(data)
		// 当遇到数据0时,退出接收循环
		if data == 0 {
			break
		}
	}

}

channel 使用的注意事项

  1. channel 中只能存放指定的数据类型
  2. channle 的数据放满后,就不能再放入了
  3. 如果从channel 取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel 数据取完了,再取,就会报dead lock


标签:接收,协程,函数,goroutine,笔记,Go,go,main,通道
From: https://blog.51cto.com/u_15980129/6179215

相关文章

  • 【学习笔记】rabbitmq设置队列ttl和使用延迟插件的代码示例
    文章目录设置队列ttl配置文件生产者消费者设置消息ttl延迟插件的使用修改配置文件修改生产者修改消费者设置队列ttl代码架构:创建两个队列QA和QB,两者队列TTL分别设置为10S和40S,然后在创建一个交换机X和死信交换机Y,它们的类型都是direct,创建一个死信队列QD配置文件spring.rabbitmq.h......
  • 【spring学习笔记】(二)Spring MVC注解配置 参数转换注解@RequestMapping@RequestParam
    @TOC介绍在SpringMVC项目中,<\context:component-scan>配置标签还会开启@Request-Mapping、@GetMapping等映射注解功能(也就是会注册RequestMappingHandler-Mapping和RequestMappingHandlerAdapter等请求映射和处理等组件),但是<context:component-scan>不支持数据转换或验证等注解功......
  • 【学习笔记】mybatis中的缓存介绍和使用
    文章目录介绍一级缓存和二级缓存让一级缓存失效的方法二级缓存的使用清空或者跳过二级缓存的3种方式介绍什么是缓存?缓存就是存储数据的一个地方(称作:Cache),当程序要读取数据时,会首先从缓存中获取,有则直接返回,否则从其他存储设备中获取,缓存最重要的一点就是从其内部获取数据的速度是......
  • 【学习笔记】在windows下进行基于TCP的本地客户端和服务端socket通信
    文章目录socket介绍java中使用socket基于tcp的socket通信使用ServerSocket类创建一个web服务器:(java)windows下的基于tcp的socket编程(c++写)InetAddress类的方法附录1TCPUDP附录2websocketsocket介绍Socket的英文原义是“孔”或“插座”。在编程中,Socket被称做套接字,是网络通......
  • 文件包含,文件上传笔记
    get用来获取数据,post用来发送数据,get请求参数会放到url中,但是隐私性和安全性较差,且请求的数据长度有限,post请求没有长度限制,存放在body中。文件包含漏洞定义:在通过PHP的函数引入文件时,由于传入的文件名没有经过合理的校验,从而操作了预想之外的文件,导致意外的文件泄露甚至恶意的代码......
  • django中migrate后重建库生成表失败?!
    如题,吓死我了虚拟环境里开了两个项目,终端migrate的时候弄错了,结果一直在生成另一个没有变化的库...我还以为是出了什么问题,删了原先的要建表库重新建了一遍。结果再自动生成的时候重新建的表一直没反应,给我吓坏了。还以为是我的sql出问题了,跑去C盘没找到mysql目录吓得我满头大......
  • 解决Godot使用VsCode编写C#代码,智能提示不见了[一问随笔]
    问题:我的项目采用了godot+visualstudiocode+C#,有天突然换引擎,从Godot4.0.0升级到Godot4.0.2,visualstudiocode突然不给代码提示了,甚是奇怪。查看报错发现这样一句话找不到指定的sdk“godot.net.sdk/4.0.2”糟了!不会这个版本不支持用vscode写代码吧!解决方式:......
  • 协同文档:OT与CRDT实现协同编辑笔记
    讲协同编辑,先回顾下从BBS、邮件,到IM信息的异步传播信息的生产和消费异步发生。典型的场景如论坛,博客,文档库,邮件。我在写这篇文档的时候,你们看不到。你们看的时候,我早已写完。异步场景下,信息的生产者会谨慎的推敲措辞,以确保自己的意思被准确的传达。表达方式的丰富性很重要,除了......
  • .NET Core 离线 生成 Tron 波场私钥和地址笔记
    NuGet引入依赖库PM>Install-PackageTron.Wallet.Net随机生成私钥和对应的地址usingTron.Wallet.Net;namespaceConsoleApp1{internalclassProgram{staticasyncTaskMain(string[]args){vartronECKey=TronECKey.GenerateKey(TronN......
  • AMBA总线(3)—— AHB学习笔记
    前面学习APB总线时,由于内容不多就直接将APB4手册翻译了下。到了AHB总线再这样学习就不好了,一是逐句翻译太累人,二是原文翻译过来划不清重点。因此APB总线以学习笔记的形式记录下来,但其实大多数也就是手册的翻译和理解。1AHB特点AHB协议相比APB协议更加复杂,性能更加优越,手册上也......