Go语言并发编程(千锋教育)

Go语言并发编程(千锋教育)

视频地址:https://www.bilibili.com/video/BV1t541147Bc?p=14

作者B站:https://space.bilibili.com/353694001

源代码:https://github.com/rubyhan1314/go_goroutine

1、基本概念

1.1、并发与并行

其实操作系统里对这些概念都有所说明和举例。

并发

  • 并发是指多个任务在同一时间段内交替执行,从外部看似乎是同时执行的。
  • 具体来说,当一个任务在等待I/O操作的结果时,CPU可以切换到另一个任务上去执行,这样就不需要等待I/O操作完成,从而提高了CPU的利用率。
  • 并发通常需要一个调度器来协调多个任务的执行。

并行

  • 并行是指多个任务同时执行,需要多个处理器或者多核CPU来支持。
  • 并行可以大大提高程序的执行效率,因为多个任务可以同时运行,而不是交替执行。
  • 并行通常需要特殊的硬件支持。

==并行性(Parallelism)不会总是导致更快的执行实际。因为并行运行的组件可能需要相互通信。==这种通信的开销在并发(Concurrent)系统中很低,但在并行系统中开销很高。

1.2、进程、线程、协程

进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)

进程 进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。 进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。

线程 线程是在进程之后发展出来的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。 线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。

协程 协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。 子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级",可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

Go语言对于并发的实现是靠协程,Goroutine。

2、初始Goroutine

2.1、什么是Goroutine

Goroutine是Go中特有的名词。区别于进程Process、线程Thread,协程Coroutine,因为Go语言的创造者觉得和他们是有区别的,所以专门创造了Goroutine。

Goroutine是与其他函数或方法同时运行的函数或方法。与线程相比,创建Goroutine的成本很小,它就是一段代码、一个函数入口,以及在堆上为其分配的一个堆栈。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。

Goroutine的优势主要体现在以下方面:

  • 栈内存小,约是Java线程栈的500~1000分之一
    Java线程栈普遍在M级别,也就是Java启动时-Xss设置的大小,可以通过java -XX:+PrintFlagsFinal -version|grep ThreadStackSize 查看,并且不支持动态扩展,满了会抛出栈溢出异常
    Goroutine的栈大小一般在2k,在内存不足时可进行动态扩展,可自动扩展至GB级别
  • 上下文切换快 ,是线程切换的5~8倍
    线程上下文切换由操作系统调度完成,相当于将一个线程从cpu核心上移动下来,把另一个线程移动上去,线程上下文切换耗时大约 1000-1500纳秒,约等于12k-18k条计算机指令
    Goruotine切换由Go运行时调度完成,相当于把一个Goroutine从线程上移动下来,把另一个Goroutine移动上去,协程上下文切换大约 200纳秒, 约2.4k条计算机指令
    如上所述:go运行时实现了类似操作系统调度线程的Goroutine调度器,线程的执行者是cpu核心,Goroutine执行者为线程

2.2、主goroutine

封装main函数的goroutine称为主goroutine。

image-20230803231623395

2.3、如何使用Goroutines

在函数或方法的前面加上关键字go,在调用时将会同时运行一个新的Goroutine。

实例代码:

package mainimport "fmt"func main() {// 1.先创建或启动子goroutine,执行printNum()go printNum()// 2.main中打印字母for i := 0; i < 1000; i++ {fmt.Println("主goroutine中打印字符:", 'A')}}func printNum() {for i := 0; i < 1000; i++ {fmt.Println("\t子goroutine中打印数字:", i)}
}

类似于多线程,线程之间的调度与执行是不确定的,但是当主goroutine运行结束时,子goroutine也会运行结束。

因此在编程时为了保证子goroutine执行完毕主goroutine才结束,需要用到通道来传递消息。

3、Goroutine并发模型

常见的线程模型
常见的有用户级线程模型、内核级线程模型、两级线程模型

常见的线程模型_为什么两层线程模型比内核级线程模型_Schuyler_yuan的博客-CSDN博客

线程并发常见模型

G-P-M 模型(Goroutine调度器模型)
在操作系统提供的内核线程之上,Go 搭建了一个特有的两级线程模型。goroutine 机制实现了 M : N 的线程模型,goroutine 机制是协程(goroutine)的一种实现,Golang 内置的调度器,可以让多核 CPU 中每个 CPU 执行一个协程。
GMP 线程调度模型_gmp调度模型_Schuyler_yuan的博客-CSDN博客

4、runtime包

runtime 调度器是个非常有用的东西,关于 runtime 包几个方法:

  1. NumCPU:返回当前系统的 CPU 核数量

  2. GOMAXPROCS:设置最大的可同时使用的 CPU 核数

    通过runtime.GOMAXPROCS函数,应用程序何以在运行期间设置运行时系统中得P最大数量。但这会引起“Stop the World”。所以,应在应用程序最早的调用。并且最好是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS,而不是在程序中调用runtime.GOMAXPROCS函数。

    无论我们传递给函数的整数值是什么值,运行时系统的P最大值总会在1~256之间。

go1.8后,默认让程序运行在多个核上,可以不用设置了 go1.8前,还是要设置一下,可以更高效的利益cpu

  • Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行

    这个函数的作用是让当前 goroutine 让出 CPU,当一个 goroutine 发生阻塞,Go 会自动地把与该 goroutine 处于同一系统线程的其他 goroutine 转移到另一个系统线程上去,以使这些 goroutine 不阻塞。

  • Goexit:退出当前 goroutine(但是defer语句会照常执行)

  • NumGoroutine:返回正在执行和排队的任务总数

    runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的Goroutine的数量。这里的特指是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。

    注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。

  • GOOS:目标操作系统

  • runtime.GC:会让运行时系统进行一次强制性的垃圾收集

    1. 强制的垃圾回收:不管怎样,都要进行的垃圾回收。
    2. 非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。
  • GOROOT :获取goroot目录

5、安全问题

什么是临界资源

临界资源是一次仅允许一个进程使用的共享资源。*各进程采取互斥的方式,实现共享的资源称作临界资源。*属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。

临界资源安全问题

并发本身并不复杂,但是因为有了资源竞争的问题,程序就变得复杂起来。

// 火车站售票 4个窗口卖10张票
package mainimport ("fmt""strconv""time"
)var ticket = 10 // 10张票func main() {for i := 1; i <= 4; i++ {go saleTicket(string("第" + strconv.Itoa(i) + "个窗口"))}time.Sleep(3 * time.Second)							// 等待子goroutine执行完毕
}func saleTicket(name string) {for ticket > 0 {time.Sleep(1 * time.Millisecond)				// 模拟时延fmt.Println(name, "售出了第", ticket, "张票")ticket--}
}/*第1个窗口 售出了第 10 张票第2个窗口 售出了第 10 张票第3个窗口 售出了第 10 张票第4个窗口 售出了第 10 张票第2个窗口 售出了第 6 张票第4个窗口 售出了第 6 张票第1个窗口 售出了第 6 张票第3个窗口 售出了第 6 张票第3个窗口 售出了第 2 张票第4个窗口 售出了第 2 张票第2个窗口 售出了第 2 张票第1个窗口 售出了第 2 张票第3个窗口 售出了第 -2 张票
*/

解决

要想解决这样的问题,很多编程语言的解决方案都是同步。

通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个数据,解锁后才允许其它goroutine来访问。

示例代码:

package mainimport ("fmt""strconv""sync""time"
)var ticket = 10 // 10张票
var mutex sync.RWMutexfunc main() {for i := 1; i <= 4; i++ {go saleTicket(string("第" + strconv.Itoa(i) + "个窗口"))}time.Sleep(3 * time.Second)
}func saleTicket(name string) {for {time.Sleep(1 * time.Millisecond)mutex.Lock()if ticket > 0 {fmt.Println(name, "售出了第", ticket, "张票")ticket--}mutex.Unlock()}
}

写在最后

在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信,而要以通信的方式去共享内存。

在Go语言中不鼓励使用锁来保护共享状态的方式在不同的Goroutine中分享信息,而是鼓励使用channel将共享状态或共享状态的变化在各个Goroutine之间传递。

6、sync包

6.1、waitGroup

对于一个可寻址的 sync.WaitGroup 值 wg:

  • 我们可以使用方法调用 wg.Add(delta) 来改变值 wg 维护的计数。
  • 方法调用 wg.Done() 和 wg.Add(-1) 是完全等价的。
  • 如果一个 wg.Add(delta) 或者 wg.Done() 调用将 wg 维护的计数更改成一个负数,一个恐慌将产生。
  • 当一个协程调用了 wg.Wait() 时,
    • 如果此时 wg 维护的计数为零,则此 wg.Wait() 此操作为一个空操作(noop);
    • 否则(计数为一个正整数),此协程将进入阻塞状态。当以后其它某个协程将此计数更改至 0 时(一般通过调用 wg.Done()),此协程将重新进入运行状态(即 wg.Wait() 将返回)。

示例代码:

package mainimport ("fmt""net/http""sync"
)func main() {// 声明一个等待组var wg sync.WaitGroup// 准备一系列的网站地址var urls = []string{"http://www.github.com/","https://www.qiniu.com/","https://www.golangtc.com/",}// 遍历这些地址for _, url := range urls {// 每一个任务开始时, 将等待组增加1wg.Add(1)// 开启一个并发go func(url string) {// 使用defer, 表示函数完成时将等待组值减1defer wg.Done()// 使用http访问提供的地址_, err := http.Get(url)// 访问完成后, 打印地址和可能发生的错误fmt.Println(url, err)// 通过参数传递url地址}(url)}// 等待所有的任务完成wg.Wait()fmt.Println("over")
}

6.2、互斥锁

互斥锁解决卖票问题:

package mainimport ("fmt""strconv""sync""time"
)var ticket = 10 // 10张票
var mutex sync.Mutexfunc main() {for i := 1; i <= 4; i++ {go saleTicket(string("第" + strconv.Itoa(i) + "个窗口"))}time.Sleep(3 * time.Second)
}func saleTicket(name string) {for {time.Sleep(1 * time.Millisecond)mutex.Lock() // 上锁if ticket > 0 {fmt.Println(name, "售出了第", ticket, "张票")ticket--}mutex.Unlock() //解锁}
}

6.3、读写锁

Go语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。

RWMutex是读/写互斥锁。锁可以由任意数量的读取器或单个编写器持有。RWMutex的零值是未锁定的mutex。

如果一个goroutine持有一个RWMutex进行读取,而另一个goroutine可能调用lock,那么在释放初始读取锁之前,任何goroutine都不应该期望能够获取读取锁。特别是,这禁止递归读取锁定。这是为了确保锁最终可用;被阻止的锁调用会将新的读卡器排除在获取锁之外。

我们怎么理解读写锁呢?当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。我们可以将其总结为如下三条:

  1. 同时只能有一个 goroutine 能够获得写锁定。
  2. 同时可以有任意多个 gorouinte 获得读锁定。
  3. 同时只能存在写锁定或读锁定(读和写互斥)。

所以,RWMutex这个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景

基本遵循两大原则:

1、可以随便读,多个goroutine同时读。2、写的时候,啥也不能干。不能读也不能写。

读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是在同一时刻,它只允许有一个写操作在进行。

并且在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。

示例代码:

package mainimport ("fmt""sync""time"
)var rwMutex *sync.RWMutex
var wg *sync.WaitGroupfunc main() {rwMutex = new(sync.RWMutex)wg = new(sync.WaitGroup)//wg.Add(2)//多个同时读取//go readData(1)//go readData(2)wg.Add(3)go writeData(1)go readData(2)go writeData(3)wg.Wait()fmt.Println("main..over...")
}func writeData(i int) {defer wg.Done()fmt.Println(i, "开始写:write start。。")rwMutex.Lock() //写操作上锁fmt.Println(i, "正在写:writing。。。。")time.Sleep(3 * time.Second)rwMutex.Unlock()fmt.Println(i, "写结束:write over。。")
}func readData(i int) {defer wg.Done()fmt.Println(i, "开始读:read start。。")rwMutex.RLock() //读操作上锁fmt.Println(i, "正在读取数据:reading。。。")time.Sleep(3 * time.Second)rwMutex.RUnlock() //读操作解锁fmt.Println(i, "读结束:read over。。。")
}

7、channel

7.1、什么是通道

通道可以被认为是Goroutines通信的管道。类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收。当多个Goroutine想实现共享数据的时候,虽然也提供了传统的同步机制,但是Go语言强烈建议的是使用Channel通道来实现Goroutines之间的通信。

“不要通过共享内存来通信,而应该通过通信来共享内存” 这是一句风靡golang社区的经典语

Go语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。Go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以Go的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。

7.2、通道的声明

// 声明通道
var 通道名 chan 数据类型
// 创建通道:如果通道为nil(就是不存在),就需要先创建通道
通道名 = make(chan 数据类型)// 也可以使用短声明
通道名 := make(chan 数据类型)

示例代码:

package mainimport "fmt"func main() {var a chan intif a == nil {fmt.Println("通道是 nil 的,不能使用,需要先创建通道。。")a = make(chan int)fmt.Printf("通道的类型为:%T\n", a)}
}

7.3、通道的注意点

  • channel是引用类型的数据,在作为参数传递的时候,传递的是内存地址。
  • 用于goroutine传递消息的。
  • 通道,每个都有相关联的数据类型, nil chan,不能使用
  • 阻塞: 发送数据:chan <- data,阻塞的,直到另一条goroutine,读取数据来解除阻塞 读取数据:data <- chan,也是阻塞的。直到另一条goroutine,写出数据解除阻塞。
  • 本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作。

最后:通道是goroutine之间的连接,所以通道的发送和接收必须处在不同的goroutine中。

死锁

使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。

类似地,如果Goroutine正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。

7.4、通道的使用

发送和接收数据

data := <- a 	// read from channel a  
a <- data 		// write to channel a

在通道上箭头的方向指定数据是发送还是接收。

另外:

v, ok := <- a 	// 从通道a中读取

示例代码1:

package mainimport "fmt"func main() {var ch1 chan boolch1 = make(chan bool)go func() {for i := 0; i < 10; i++ {fmt.Println("子Goroutine中,i:", i)}// 循环结束后,向通道中写数据,表示执行完成ch1 <- truefmt.Println("结束。。。")}()data := <-ch1fmt.Println("main...data---->", data)fmt.Println("main...over...")}

示例代码2:

package mainimport ("fmt"
)func main() {ch1 := make(chan int)go func() {fmt.Println("子goroutine开始执行。。")data := <-ch1fmt.Println(data)}()ch1 <- 10fmt.Println("main..over..")}

一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个goroutine从通道中读取数据。相对的,当从通道中读取数据时,读取被阻塞,直到一个goroutine将数据写入该通道。

这些通道的特性帮助goroutines有效地进行通信,而无需像其它编程语言中非常常见的显示锁或条件变量。

7.5、通道的关闭和范围循环

通道关闭

发送者可以通过关闭信道,来通知接收方不会有更多的数据被发送到channel上。

close(ch)

接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭。

语法结构:

v, ok := <- ch  

在上面的语句中,如果ok的值是true,表示成功的从通道中读取了一个数据value。如果ok是false,这意味着我们正在从一个封闭的通道读取数据。从闭通道读取的值将是通道类型的零值。

实例代码:

package mainimport ("fmt""time"
)func main()  {ch1 := make(chan int)go sendData(ch1)/*子goroutine,写出数据10个每写一个,阻塞一次,主程序读取一次,解除阻塞主goroutine:循环读每次读取一个,阻塞一次,子程序,写出一个,解除阻塞发送发,关闭通道的--->接收方,接收到的数据是该类型的零值,以及false*///主程序中获取通道的数据for{time.Sleep(1*time.Millisecond)v, ok := <- ch1 		//其他goroutine,显示的调用close方法关闭通道。if !ok{fmt.Println("已经读取了所有的数据,", ok)break}fmt.Println("取出数据:",v, ok)}fmt.Println("main...over....")
}func sendData(ch1 chan int)  {// 发送方:10条数据for i:=0;i<10 ;i++  {ch1 <- i		//将i写入通道中}close(ch1) 			//将ch1通道关闭了。
}

范围循环 for-range

	for v := range ch {}

实例代码:

package mainimport "fmt"func main() {ch := make(chan int)go sendData(ch)for v := range ch {fmt.Println("读取数据:", v)}fmt.Println("main。。over。。")}func sendData(ch chan int) {for i := 0; i < 10; i++ {ch <- i}close(ch)
}

注意,一定要手动关闭通道!

7.6、缓冲通道

之前学习的所有通道基本上都没有缓冲。发送和接收到一个未缓冲的通道是阻塞的。

一次发送操作对应一次接收操作,对于一个goroutine来讲,它的一次发送,在另一个goroutine接收之前都是阻塞的。同样的,对于接收来讲,在另一个goroutine发送之前,它也是阻塞的。

缓冲通道就是指一个通道,带有一个缓冲区。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。

可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小。

语法:

ch := make(chan type, capacity)  

上述语法的容量应该大于0,以便通道具有缓冲区。默认情况下,无缓冲通道的容量为0,因此在之前创建通道时省略了容量参数。

示例代码:

package mainimport ("fmt""strconv""time"
)func main() {/*非缓存通道:make(chan T)缓存通道:make(chan T ,size)缓存通道,理解为是队列:非缓存,发送还是接受,都是阻塞的缓存通道,缓存区的数据满了,才会阻塞状态。。*/ch1 := make(chan int)           //非缓存的通道fmt.Println(len(ch1), cap(ch1)) //0 0//ch1 <- 100	//阻塞的,需要其他的goroutine解除阻塞,否则deadlockch2 := make(chan int, 5)        //缓存的通道,缓存区大小是5fmt.Println(len(ch2), cap(ch2)) //0 5ch2 <- 100                      //fmt.Println(len(ch2), cap(ch2)) //1 5//ch2 <- 200//ch2 <- 300//ch2 <- 400//ch2 <- 500//ch2 <- 600fmt.Println("--------------")ch3 := make(chan string, 4)go sendData3(ch3)for {time.Sleep(1*time.Second)v, ok := <-ch3if !ok {fmt.Println("读完了,,", ok)break}fmt.Println("\t读取的数据是:", v)}fmt.Println("main...over...")
}func sendData3(ch3 chan string) {for i := 0; i < 10; i++ {ch3 <- "数据" + strconv.Itoa(i)fmt.Println("子goroutine,写出第", i, "个数据")}close(ch3)
}

7.7、定向通道

双向通道

通道,channel,是用于实现goroutine之间的通信的。一个goroutine可以向通道中发送数据,另一条goroutine可以从该通道中获取数据。前面所学习的通道,都是既可以发送数据,也可以读取数据,我们又把这种通道叫做双向通道。

单向通道

/*双向:chan T -->chan <- data,写出数据,写data <- chan,获取数据,读单向:定向chan <- T,只支持写,<- chan T,只读
*/

示例代码:

package mainimport "fmt"func main() {ch1 := make(chan int) //双向,读,写//ch2 := make(chan <- int) // 单向,只写,不能读//ch3 := make(<- chan int) //单向,只读,不能写//ch1 <- 100//data :=<-ch1//ch2 <- 1000//data := <- ch2//fmt.Println(data)//	<-ch2 //invalid operation: <-ch2 (receive from send-only type chan<- int)//ch3 <- 100//	<-ch3//	ch3 <- 100 //invalid operation: ch3 <- 100 (send to receive-only type <-chan int)//go fun1(ch2)go fun1(ch1)data := <-ch1fmt.Println("fun1中写出的数据是:", data)//fun2(ch3)go fun2(ch1)ch1 <- 200fmt.Println("main。。over。。")
}// 该函数接收,只写的通道
func fun1(ch chan<- int) {// 函数内部,对于ch只能写数据,不能读数据ch <- 100fmt.Println("fun1函数结束。。")
}func fun2(ch <-chan int) {//函数内部,对于ch只能读数据,不能写数据data := <-chfmt.Println("fun2函数,从ch中读取的数据是:", data)
}

定向通道在实际使用时一般作为参数限制函数内部的操作,实际上传递进来的通道一般还是双向通道。

7.8、time包中的通道

主要就是定时器,标准库中的Timer让用户可以定义自己的超时逻辑,尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。

Timer是一次性的时间触发事件,这点与Ticker不同,Ticker是按一定时间间隔持续触发时间事件。

Timer常见的创建方式:

t:= time.NewTimer(d)
t:= time.AfterFunc(d, f)
c:= time.After(d)

虽然说创建方式不同,但是原理是相同的。

Timer有3个要素:

定时时间:就是那个d
触发动作:就是那个f
时间channel: 也就是t.C

7.8.1、time.NewTimer()

NewTimer()创建一个新的计时器,该计时器将在其通道上至少持续d之后发送当前时间。

它的返回值是一个Timer。

源代码:

// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
func NewTimer(d Duration) *Timer {c := make(chan Time, 1)t := &Timer{C: c,r: runtimeTimer{when: when(d),f:    sendTime,arg:  c,},}startTimer(&t.r)return t
}

通过源代码我们可以看出,首先创建一个channel,关联的类型为Time,然后创建了一个Timer并返回。

  • 用于在指定的Duration类型时间后调用函数或计算表达式。
  • 如果只是想指定时间之后执行,使用time.Sleep()
  • 使用NewTimer(),可以返回的Timer类型在计时器到期之前,取消该计时器
  • 直到使用<-timer.C发送一个值,该计时器才会过期

示例代码:

package mainimport ("time""fmt"
)func main() {/*1.func NewTimer(d Duration) *Timer创建一个计时器:d时间以后触发,go触发计时器的方法比较特别,就是在计时器的channel中发送值*///新建一个计时器:timertimer := time.NewTimer(3 * time.Second)fmt.Printf("%T\n", timer) //*time.Timerfmt.Println(time.Now())   //2019-08-15 10:41:21.800768 +0800 CST m=+0.000461190//此处在等待channel中的信号,执行此段代码时会阻塞3秒ch2 := timer.C     //<-chan time.Timefmt.Println(<-ch2) //2019-08-15 10:41:24.803471 +0800 CST m=+3.003225965}

7.8.2、timer.Stop

示例代码:

package mainimport ("fmt""time"
)func main() {/*1.func NewTimer(d Duration) *Timer创建一个计时器:d时间以后触发,go触发计时器的方法比较特别,就是在计时器的channel中发送值*///新建一个计时器:timertimer := time.NewTimer(3 * time.Second)fmt.Printf("%T\n", timer) //*time.Timerfmt.Println(time.Now())   //2023-08-03 14:03:16.7591597 +0800 CST m=+0.002060301//此处在等待channel中的信号,执行此段代码时会阻塞3秒ch2 := timer.C     //<-chan time.Timefmt.Println(<-ch2) //2023-08-03 14:03:19.7699419 +0800 CST m=+3.012825001fmt.Println("-------------------------------")//新建计时器,一秒后触发timer2 := time.NewTimer(5 * time.Second)//新开启一个协程来处理触发后的事件go func() {//等触发时的信号<-timer2.Cfmt.Println("Timer 2 结束。。")}()//由于上面的等待信号是在新线程中,所以代码会继续往下执行,停掉计时器time.Sleep(3 * time.Second)stop := timer2.Stop()if stop {fmt.Println("Timer 2 停止。。")}}

7.8.3、time.After()

在等待持续时间之后,然后在返回的通道上发送当前时间。它相当于NewTimer(d).C。在计时器触发之前,垃圾收集器不会恢复底层计时器。如果效率有问题,使用NewTimer代替,并调用Timer。如果不再需要计时器,请停止。

源码:

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {return NewTimer(d).C
}

示例代码:

package mainimport ("fmt""time"
)func main() {/*func After(d Duration) <-chan Time返回一个通道:chan,存储的是d时间间隔后的当前时间。*/ch1 := time.After(3 * time.Second) //3s后fmt.Printf("%T\n", ch1)            // <-chan time.Timefmt.Println(time.Now())            //2023-08-03 14:09:21.065501 +0800 CST m=+0.003107401time2 := <-ch1fmt.Println(time2)			 	   //2023-08-03 14:09:24.0662873 +0800 CST m=+3.003884401}

7.9、select语句

select 是 Go 中的一个控制结构。select 语句类似于 switch 语句,但是select会**随机执行**一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

select语句的语法结构和switch语句很相似,也有case语句和default语句:

select {case communication clause  :statement(s);      case communication clause  :statement(s); /* 你可以定义任意数量的 case */default : /* 可选 */statement(s);
}

说明:

  • 每个case都必须是一个通信

  • 所有channel表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。

  • 否则:

    如果有default子句,则执行该语句。

    如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

示例代码:

package mainimport ("fmt""time"
)func main() {ch1 := make(chan int)ch2 := make(chan int)go func() {time.Sleep(3 * time.Second)ch1 <- 100}()go func() {time.Sleep(2 * time.Second)ch2 <- 100}()select {case num1 := <-ch1:fmt.Println("从通道1中获取的数据。。", num1)case num2, ok := <-ch2:if ok {fmt.Println("从通道2中获取的数据。。", num2)} else {fmt.Println("通道已经关闭")}default:fmt.Println("没有获取到数据")}fmt.Println("main...over...")}

结合timer可以用来监听通道上的数据流动:

package mainimport ("fmt""time"
)func main() {ch1 := make(chan int)ch2 := make(chan int)go func() {time.Sleep(3 * time.Second)ch1 <- 100}()select {case <-ch1:fmt.Println("case1可以执行")case <-ch2:fmt.Println("case2可以执行")case <-time.After(3 * time.Second):fmt.Println("case3执行。。timeout。。")//default://	fmt.Println("default执行。。")}}
  • 否则:

    如果有default子句,则执行该语句。

    如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

示例代码:

package mainimport ("fmt""time"
)func main() {ch1 := make(chan int)ch2 := make(chan int)go func() {time.Sleep(3 * time.Second)ch1 <- 100}()go func() {time.Sleep(2 * time.Second)ch2 <- 100}()select {case num1 := <-ch1:fmt.Println("从通道1中获取的数据。。", num1)case num2, ok := <-ch2:if ok {fmt.Println("从通道2中获取的数据。。", num2)} else {fmt.Println("通道已经关闭")}default:fmt.Println("没有获取到数据")}fmt.Println("main...over...")}

结合timer可以用来监听通道上的数据流动:

package mainimport ("fmt""time"
)func main() {ch1 := make(chan int)ch2 := make(chan int)go func() {time.Sleep(3 * time.Second)ch1 <- 100}()select {case <-ch1:fmt.Println("case1可以执行")case <-ch2:fmt.Println("case2可以执行")case <-time.After(3 * time.Second):fmt.Println("case3执行。。timeout。。")//default://	fmt.Println("default执行。。")}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/75117.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【1++的C++进阶】之多态

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的C进阶】 文章目录 一&#xff0c;什么是多态&#xff1f;二&#xff0c;剖析多态的调用原理三&#xff0c;抽象类四&#xff0c;多继承中的虚函数表 一&#xff0c;什么是多态&#xff1f; …

谈谈对Android音视频开发的探究

在日常生活中&#xff0c;视频类应用占据了我们越来越多的时间&#xff0c;各大公司也纷纷杀入这个战场&#xff0c;不管是抖音、快手等短视频类型&#xff0c;虎牙、斗鱼等直播类型&#xff0c;腾讯视频、爱奇艺、优酷等长视频类型&#xff0c;还是Vue、美拍等视频编辑美颜类型…

list模拟

之前模拟了string,vector&#xff0c;再到现在的list&#xff0c;list的迭代器封装最让我影响深刻。本次模拟的list是双向带头节点的循环链表&#xff0c;该结构虽然看起来比较复杂&#xff0c;但是却非常有利于我们做删除节点的操作&#xff0c;结构图如下。 由于其节点结构特…

【etcd】docker 启动单点 etcd

etcd: v3.5.9 etcd-browser: rustyx/etcdv3-browser:latest 本文档主要描述用 docker 部署单点的 etcd&#xff0c; 用 etcd-browser 来查看注册到 etcd 的 key 默认配置启动 docker run -d --name ai-etcd --networkhost --restart always \-v $PWD/etcd.conf.yml:/opt/bitn…

从gRPC入门到放弃

文章目录 gRPCgRPC是什么为什么要用gRPC安装gRPC安装gRPC安装Protocol Buffers v3安装插件检查 gRPC的开发方式编写.proto文件定义服务生成指定语言的代码编写业务逻辑代码 gRPC入门示例编写proto代码编写Server端Go代码编写Client端Go代码gRPC跨语言调用生成Python代码编写Pyt…

K8s安全配置:CIS基准与kube-bench工具

01、概述 K8s集群往往会因为配置不当导致存在入侵风险&#xff0c;如K8S组件的未授权访问、容器逃逸和横向攻击等。为了保护K8s集群的安全&#xff0c;我们必须仔细检查安全配置。 CIS Kubernetes基准提供了集群安全配置的最佳实践&#xff0c;主要聚焦在两个方面&#xff1a;主…

基于双层优化的微电网系统规划设计方法(Matlab代码实现)

目录 &#x1f4a5;1 概述 1.1 微电网系统结构 1.2 微电网系统双层规划设计结构 1.3 双层优化模型 1.4 上层容量优化模型 1.5 下层调度优化模型 &#x1f4da;2 运行结果 &#x1f389;3 文献来源 &#x1f308;4 Matlab代码、数据、文章讲解 &#x1f4a5;1 概述 文献来源&…

牛客网Verilog刷题——VL51

牛客网Verilog刷题——VL51 题目答案 题目 请编写一个十六进制计数器模块&#xff0c;计数器输出信号递增每次到达0&#xff0c;给出指示信号zero&#xff0c;当置位信号set 有效时&#xff0c;将当前输出置为输入的数值set_num。模块的接口信号图如下&#xff1a; 模块的时序图…

作者推荐 | 【底层服务/编程功底系列】「底层技术原理」史上最清晰的采用程序员的视角方式进行深入探索Linux零拷贝技术原理及实现

采用程序员的视角方式进行深入探索Linux零拷贝技术原理及实现 背景介绍什么是零拷贝第一步&#xff1a;用户空间数据复制到内核空间第二步&#xff1a;用户空间数据复制到内核空间第三步&#xff1a;用户空间数据再次复制到内核空间第四步&#xff1a;内核态数据buffer写回到So…

html5播放器视频切换和连续播放的实例

当前播放器实例可以使用changeVid接口切换正在播放的视频。当有多个视频&#xff0c;在上一个视频播放完毕时&#xff0c;自动播放下一个视频时也可采用该处理方式。 const option {vid: 88083abbf5bcf1356e05d39666be527a_8,//autoplay: true,//playsafe: , //PC端播放加密视…

超详细|ChatGPT论文润色教程

本文讲述使用中科大开源ChatGPT论文辅助工具&#xff0c;对论文进行润色 祝看到本教程的小伙伴们都完成论文&#xff0c;顺利毕业。 可以加QQ群交流&#xff0c;一群&#xff1a; 123589938 第一章 介绍 今天给大家分享一款非常不错的ChatGPT论文辅助工具&#xff0c;使用了专…

电脑更新win10黑屏解决方法

电脑更新win10黑屏解决方法 电脑黑屏出现原因解决步骤 彻底解决 电脑黑屏 出现原因 系统未更新成功就关机&#xff0c;导致系统出故障无法关机 解决步骤 首先长安电源键10s关机 按电源键开机&#xff0c;出现logo时按F8进入安全模式。 进入自动修复环境后&#xff0c;单击…

ElasticSearch 7.x

前言 elastic表示可伸缩&#xff0c;search表示查询。所以es的核心即为查询。通常情况下&#xff0c;我们的数据可以分为三类&#xff1a;结构化数据、非结构化数据、半结构化数据。 结构化数据&#xff1a;一般会用特定的结构来组织和管理数据&#xff0c;表现为二维表结构。…

Spring Bean的生命周期

文章目录 Spring Bean的生命周期加载Bean对象创建Bean对象构造对象填充属性初始化实例注册销毁 销毁 Spring Bean的生命周期 Spring Bean的生命周期就是指Bean对象从创建到销毁的过程&#xff0c;大体可以分为&#xff1a;实例化、属性赋值、初始化、使用、销毁。 加载Bean对象…

【数据分析】numpy (二)

numpy作为数据分析&#xff0c;深度学习常用的库&#xff0c;本篇博客我们来介绍numpy的一些进阶用法&#xff1a; 一&#xff0c;numpy的常用简单内置函数&#xff1a; 1.1求和&#xff1a; a np.array([[1, 2],[3, 4]]) np.sum(a)10 1.2求平均值&#xff1a; np.mean(a…

向 Maven 中央仓库上传一个修改过的基于jeecg的autoPOI的 jar包记录

1、注册https://issues.sonatype.org/账号 下面就代表注册好了&#xff0c;同时提交的工单也通过了 2、这里主要是goupId 需要进行认证&#xff0c;需要到域名注册商近一个txt的解析&#xff0c;以便确保这个是你的 通过下面来验证你的域名信息&#xff0c;这里主要是上面的工…

学生信息管理系统springboot学校学籍专业数据java jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 学生信息管理系统springboot 系统3权限&#xff1a;超…

如何使用fiddler进行抓包

首先需要下载fiddler&#xff0c;推荐使用bing搜索引擎搜索&#xff08;百度搜狗一般搜这种工具展示的前几个全都是广告&#xff09;&#xff0c;直接搜索fiddler&#xff0c;搜出来第一个fiddler官网 然后直接点击download下载 进入下载页面后&#xff0c;正确填写一个邮箱&a…

unity TextMeshPro 富文本

<b>粗体标签</b> <i>斜体标签</i> <u>下划线标签</u> <s>删除线标签</s> <sup>上标标签</sup>前面后边上标签 5<sup>。</sup>C <sub>下标标签&#xff0c;如&#xff1a;</sub>H<sub&…

【雕爷学编程】MicroPython动手做(33)——物联网之天气预报2

天气&#xff08;自然现象&#xff09; 是指某一个地区距离地表较近的大气层在短时间内的具体状态。而天气现象则是指发生在大气中的各种自然现象&#xff0c;即某瞬时内大气中各种气象要素&#xff08;如气温、气压、湿度、风、云、雾、雨、闪、雪、霜、雷、雹、霾等&#xff…