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。
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 包几个方法:
-
NumCPU:返回当前系统的
CPU
核数量 -
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:会让运行时系统进行一次强制性的垃圾收集
- 强制的垃圾回收:不管怎样,都要进行的垃圾回收。
- 非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。
-
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)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。我们可以将其总结为如下三条:
- 同时只能有一个 goroutine 能够获得写锁定。
- 同时可以有任意多个 gorouinte 获得读锁定。
- 同时只能存在写锁定或读锁定(读和写互斥)。
所以,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执行。。")}}
相关文章:
![](https://img-blog.csdnimg.cn/img_convert/6fa3b1c16069580daa7cbd54750d1e0f.png)
Go语言并发编程(千锋教育)
Go语言并发编程(千锋教育) 视频地址:https://www.bilibili.com/video/BV1t541147Bc?p14 作者B站:https://space.bilibili.com/353694001 源代码:https://github.com/rubyhan1314/go_goroutine 1、基本概念 1.1、…...
![](https://www.ngui.cc/images/no-images.jpg)
CSS革命:用Sass/SCSS引领前端创新
目录 前言SCSSSassSass 和 SCSS 的区别 前言 在现代的前端开发中,CSS已成为呈现网页和应用程序样式的核心。然而,原生的CSS语法在大型项目中可能变得混乱、冗长且难以维护。 为了解决这些问题,SCSS(Sass CSS)和Sass&am…...
![](https://img-blog.csdnimg.cn/e729f2ec665746a8aeb671cc2b92b720.png)
MAPPO 算法的深度解析与应用和实现
【论文研读】 The Surprising Effectiveness of PPO in Cooperative Multi-Agent Games 说明: 来源:36th Conference on Neural Information Processing Systems (NeurIPS 2022) Track on Datasets and Benchmarks. 是NIPS文章,质量有保障&…...
![](https://www.ngui.cc/images/no-images.jpg)
API接口的涉及思路以及部分代码
在现代软件开发中,API(Application Programming Interface)接口扮演了一个至关重要的角色。通过API接口,不同的应用程序、系统或服务之间可以进行数据交换和相互调用,实现功能的扩展和集成。本文将探讨API接口的设计思…...
![](https://img-blog.csdnimg.cn/927dc02dd3ea4223952a63311a581a00.png)
Stable Diffusion无需代码连接QQ邮箱的方法
Stable Diffusion用户使用场景: 电商商家在产品测试阶段,通过微信社群日常收集用户对产品设计的反馈,包括对产品的修改建议或外观设计等,并将这些反馈上传至集简云小程序。然后,他们使用Stable Diffusion AI工具生成图…...
![](https://img-blog.csdnimg.cn/49e800303d9748458b6bc8884b297156.png)
Excel表格(一)
1.单一栏的宽度和高度设置 2.大标题的跨栏居中 3.让单元格内的文字------自动适应 4.序号递增 5.货币符号 6.日期格式的选择 选到单元格,选中对应的日期格式 7.自动求和的计算 然后在按住回车键即可求出当前行的金额 点击自动求和 8.冻结表格栏 9.排序 1.单栏排序 …...
![](https://img-blog.csdnimg.cn/763e8d1b4a754f838186a737d7b75719.jpeg#pic_center)
详细介绍渗透测试与漏洞扫描
一、概念 渗透测试: 渗透测试并没有一个标准的定义,国外一些安全组织达成共识的通用说法;通过模拟恶意黑客的攻击方法,来评估计算机网络系统安全的一种评估方法。这个过程包括对系统的任何弱点、技术缺陷或漏洞的主动的主动分析…...
![](https://img-blog.csdnimg.cn/86aca1ba473d4cd9837ad13f8a9815e0.png)
Scikit-learn聚类方法代码批注及相关练习
一、代码批注 代码来自:https://scikit-learn.org/stable/auto_examples/cluster/plot_dbscan.html#sphx-glr-auto-examples-cluster-plot-dbscan-py import numpy as np from sklearn.cluster import DBSCAN from sklearn import metrics from sklearn.datasets …...
![](https://img-blog.csdnimg.cn/img_convert/879000d95cbc4bc6b8ed25d5b1cada5e.png)
C#程序的启动显示方案(无窗口进程发送消息) - 开源研究系列文章
今天继续研究C#的WinForm的实例显示效果。 我们上次介绍了Winform窗体的唯一实例运行代码(见博文:基于C#的应用程序单例唯一运行的完美解决方案 - 开源研究系列文章 )。这就有一个问题,程序已经打开了,这时候再次运行该应用程序,…...
![](https://img-blog.csdnimg.cn/20200418214051828.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjkwMjEyNg==,size_16,color_FFFFFF,t_70)
java泛型和通配符的使用
泛型机制 本质是参数化类型(与方法的形式参数比较,方法是参数化对象)。 优势:将类型检查由运行期提前到编译期。减少了很多错误。 泛型是jdk5.0的新特性。 集合中使用泛型 总结: ① 集合接口或集合类在jdk5.0时都修改为带泛型的结构② 在实例化集合类时…...
![](https://img-blog.csdnimg.cn/img_convert/aca74b1a68825e1b727150994851e2d0.png)
【网络】自定义协议 | 序列化和反序列化 | 以tcpServer为例
本文首发于 慕雪的寒舍 以tcpServer的计算器服务为例,实现一个自定义协议 阅读本文之前,请先阅读 tcpServer 本文完整代码详见 Gitee 1.重谈tcp 注意,当下所对tcp的描述都是以简单、方便理解起见,后续会对tcp协议进行深入解读 …...
![](https://img-blog.csdnimg.cn/03f69a0b4cd846669e27bff38384c1b3.png)
06-3_Qt 5.9 C++开发指南_多窗体应用程序的设计(主要的窗体类及其用途;窗体类重要特性设置;多窗口应用程序设计)
文章目录 1. 主要的窗体类及其用途2. 窗体类重要特性的设置2.1 setAttribute()函数2.2 setWindowFlags()函数2.3 setWindowState()函数2.4 setWindowModality()函数2.5 setWindowOpacity()函数 3. 多窗口应用程序设计3.1 主窗口设计3.2 QFormDoc类的设计3.3 QFormDoc类的使用3.…...
![](https://img-blog.csdnimg.cn/bdbfafa32e744dd1919b9ad0c9ac39b8.png)
(力扣)用两个栈实现队列
这里是栈的源代码:栈和队列的实现 当然,自己也可以写一个栈来用,对题目来说不影响,只要符合栈的特点就行。 题目: 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、pe…...
![](https://img-blog.csdnimg.cn/7022f26c803f4238bb33509b7ff9d56e.png)
【自动化测试框架】关于unitttest你需要知道的事
一、UnitTest单元测试框架提供了那些功能 1.提供用例组织和执行 如何定义一条“测试用例”? 如何灵活地控制这些“测试用例”的执行? 2.提供丰定的断言方法 当测试用例的执行结果与预期结果不一致时,判定测试用例失败。在自动化测试中,通过“断言”…...
![](https://img-blog.csdnimg.cn/img_convert/7ecd21965cb9d96ccf3636a35f6958d0.jpeg)
手机便签中可以打勾的圆圈或小方块怎么弄?
在日常的生活和工作中,很多网友除了使用手机便签来记录灵感想法、读书笔记、各种琐事、工作事项外,还会用它来记录一些清单,例如待办事项清单、读书清单、购物清单、旅行必备物品清单等。 在按照记录的清单内容来执行的时候,为了…...
![](https://img-blog.csdnimg.cn/38515ce88e2a4828b0cf91d5993ccc4b.png)
【Linux】gdb 的使用
目录 1. 使用 gdb 的前置工作 2. 如何使用 gdb 进行调试 1、如何看到我的代码 2、如何打断点 3、怎么运行程序 4、如何进行逐过程调试 5、如何进行逐语句调试 6、如何监视变量值 7、如何跳到指定位置 8、运行完一个函数 9、怎么跳到下一个断点 10、如何禁用/开启…...
![](https://img-blog.csdnimg.cn/img_convert/0e95e18e9a451afdca2caf77e94ee36d.png)
C++11之右值引用
C11之右值引用 传统的C语法中就有引用的语法,而C11中新增了的 右值引用(rvalue reference)语法特性,所以从现在开始我们之前学习的引用就叫做左值引用(lvalue reference)。无论左值引用还是右值引用&#…...
![](https://www.ngui.cc/images/no-images.jpg)
【PHP的设计模式】
PHP的设计模式 一、策略模式二、工厂模式三、单例模式四、注册模式五、适配器模式六、观察者模式 一、策略模式 策略模式是对象的行为模式,用意是对一组算法的封装。动态的选择需要的算法并使用。 策略模式指的是程序中涉及决策控制的一种模式。策略模式功能非常强…...
![](https://img-blog.csdnimg.cn/370211db388947aca5d548e2216bfc54.png)
React 之 Redux - 状态管理
一、前言 1. 纯函数 函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念 确定的输入,一定会产生确定的输出 函数在执行过程中,不能产生副作用 2. 副作用 表示在执行一个函数时&a…...
![](https://www.ngui.cc/images/no-images.jpg)
集合转数组
首先,我们在看到集合转数组的时候可能第一个想到的就是toArray(),但是我们在调用 toArray()的时候,可能会遇到异常 java.lang.ClassCastException;这是因为 toArray()方法返回的类型是 Obejct[],如果我们将其转换成其他类型&#…...
![](https://img-blog.csdnimg.cn/39f54fc3bcdf48f1a8dc353a65f81005.png)
使用Python将Word文档转换为PDF的方法
摘要: 文介绍了如何使用Python编程语言将Word文档转换为PDF格式的方法。我们将使用python-docx和pywin32库来实现这个功能,这些库提供了与Microsoft Word应用程序的交互能力。 正文: 在现实生活和工作中,我们可能会遇到将Word文…...
![](https://www.ngui.cc/images/no-images.jpg)
Java 判断一个字符串在另一个字符串中出现的次数
1.split实现 package com.jiayou.peis.official.account.biz.utils;public class Test {public static void main(String[] args) {String k"0110110100100010101111100101011001101110111111000101101001100010101" "011101100101011010100011111010111001001…...
![](https://www.ngui.cc/images/no-images.jpg)
设计模式十三:代理(Proxy Pattern)
代理模式是一种结构型设计模式,它允许通过在对象和其真实服务之间添加一个代理对象来控制对该对象的访问。代理对象充当了客户端和真实服务对象之间的中介,并提供了额外的功能,如远程访问、延迟加载、访问控制等。 代理模式的使用场景包括&a…...
![](https://img-blog.csdnimg.cn/5b13eedbaf6f411cb1a5e91710690144.png)
Redis基础 (三十八)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 前言 一、概述 1.1 NoSQL 1.2 Redis 二、安装 2.1 安装方式 : 三、目录结构 3.1 rpm -ql redis 3.2 /etc/redis.conf 主配置文件 3.3 /var/lib/redis …...
![](https://www.ngui.cc/images/no-images.jpg)
maven中的scope
1、compile:默认值,可省略不写。此值表示该依赖需要参与到项目的编译、测试以及运行周期中,打包时也要包含进去。 2、test:该依赖仅仅参与测试相关的工作,包括测试代码的编译和执行,不会被打包,…...
![](https://img-blog.csdnimg.cn/e1a600d2d03f475f815ae785d1f3c5bc.png)
【网络基础实战之路】实现RIP协议与OSPF协议间路由交流的实战详解
系列文章传送门: 【网络基础实战之路】设计网络划分的实战详解 【网络基础实战之路】一文弄懂TCP的三次握手与四次断开 【网络基础实战之路】基于MGRE多点协议的实战详解 【网络基础实战之路】基于OSPF协议建立两个MGRE网络的实验详解 PS:本要求基于…...
![](https://img-blog.csdnimg.cn/1155249777944b20af420272a301cb1d.png)
CNN(四):ResNet与DenseNet结合--DPN
🍨 本文为🔗365天深度学习训练营中的学习记录博客🍖 原作者:K同学啊|接辅导、项目定制 前面实现了ResNet和DenseNet的算法,了解了它们有各自的特点: ResNet:通过建立前面层与后面层之间的“短路…...
![](https://img-blog.csdnimg.cn/img_convert/7046751de11a706ac05bf59a967f870e.png)
汽车EBSE测试流程分析(四):反思证据及当前问题解决
EBSE专题连载共分为“五个”篇章。此文为该连载系列的“第四”篇章,在之前的“篇章(三)”中已经结合具体研究实践阐述了“步骤二,通过系统调研确定改进方案”等内容。那么,在本篇章(四)中&#…...
![](https://img-blog.csdnimg.cn/2021060109523638.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDc5NzUzOQ==,size_16,color_FFFFFF,t_70)
如何在Spring MVC中使用@ControllerAdvice创建全局异常处理器
文章目录 前言一、认识注解:RestControllerAdvice和ExceptionHandler二、使用步骤1、封装统一返回结果类2、自定义异常类封装3、定义全局异常处理类4、测试 总结 前言 全局异常处理器是一种 🌟✨机制,用于处理应用程序中发生的异常ÿ…...
![](https://img-blog.csdnimg.cn/3b0a5d733f3e4c64b0feb4ce4617dbb7.png)
2023/08/05【网络课程总结】
1. 查看git拉取记录 git reflog --dateiso|grep pull2. TCP/IP和OSI七层参考模型 3. DNS域名解析 4. 预检请求OPTIONS 5. 渲染进程的回流(reflow)和重绘(repaint) 6. V8解析JavaScript 7. CDN负载均衡的简单理解 8. 重学Ajax 重学Ajax满神 9. 对于XML的理解 大白话叙述XML是…...
![](/images/no-images.jpg)
有趣的个人网站/外贸推广建站
NAT原理与NAT穿透 原创大鞭炮好大 发布于2019-02-26 14:22:56 阅读数 92 收藏 展开 分享一下我老师大神的人工智能教程。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.…...
![](https://img-blog.csdnimg.cn/img_convert/3b1557a443cdc4a17708f4090d3f7e9e.png)
客户资源网/seo刷关键词排名工具
这个话题一般情况下,我不可能明说出来,甚至更不可能发出来。因为有以下的一些原因: 1. 如何包装,只代表个人观点,毕竟一个面试官一个心态和标准。没人敢打包票说这是包装圣经。 2.分层不同,负责筛选的hr&…...
![](https://img2018.cnblogs.com/blog/1535493/201907/1535493-20190722195443573-1761409074.jpg)
wordpress2.9.2漏洞/seo排名工具外包
OpenStack-配仪表盘 【基于此文章的环境】点我快速打开文章 计算节点安装(compute1) 1、安装 yum install openstack-dashboard -y &>/dev/nullecho $? 2、配置 1. 官方配置 【官方地址】点我快速打开文章 2. 导入配置 lsrzcat local_settings &…...
![](/images/no-images.jpg)
wordpress mip主题/软件测试培训机构哪家好
用到的方法是Animation translateAnimation new TranslateAnimation(float begin_X, float end_X, float begin_Y,float end_Y);参数分别代表开始时X的坐标,结束时X的坐标,开始时Y的坐标,结束时Y的坐标。 我们可以通过DisplayMetrics metric…...
![](/images/no-images.jpg)
自己做网站要花钱吗/云南网络推广公司排名
4.6. 定义函数 我们可以创建一个用来生成指定边界的斐波那契数列的函数: >>>def fib(n): # write Fibonacci series up to n ... """Print a Fibonacci series up to n.""" ... a, b 0, 1 ... while a < n: ... print(a, end ) ...…...
![](https://img-blog.csdnimg.cn/20190121114237851.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0x1b21pbmdrdWkxMTA5,size_16,color_FFFFFF,t_70)
手游代理/seo排名优化网站
1.可以与Kylin结合使用的可视化工具很多,例如: • ODBC:与Tableau、Excel、PowerBI等工具集成 • JDBC:与Saiku、BIRT等Java工具集成 • RestAPI:与JavaScript、Web网页集成 • Kylin开发团队还贡献了Zepplin的插件&am…...