内容简介:•并发(concurrency)是指一次处理大量事情的能力。并发的关键是你有处理多个任务的能力,不一定要同时。•并行(parallelism)指的是同时处理多个事情。并行的关键是你有同时处理多个任务的能力。简单的理解一下,并发就是你在跑步的时候鞋带开了,你停下来系鞋带。而并行则是,你一边听歌一边跑步。
Go是并发语言,而不是并行语言。
一、并发和并行的区别
•并发(concurrency)是指一次处理大量事情的能力。并发的关键是你有处理多个任务的能力,不一定要同时。
•并行(parallelism)指的是同时处理多个事情。并行的关键是你有同时处理多个任务的能力。
简单的理解一下,并发就是你在跑步的时候鞋带开了,你停下来系鞋带。而并行则是,你一边听歌一边跑步。
并行并不代表比并发快,举一个例子,当文件下载完成时,应该使用弹出窗口来通知用户。而这种通信发生在负责下载的组件和负责渲染用户界面的组件之间。在并发系统中,这种通信的开销很低。而如果这两个组件并行地运行在 CPU 的不同核上,这种通信的开销却很大。因此并行程序并不一定会执行得更快。
Go 原生支持并发。在 Go 中,使用 Go 协程(Goroutine)和信道(channel)来处理并发。
二、Go协程(Goroutine)
只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员⽆需了解任何执⾏细节,调度器会⾃动将其安排到合适的系统线程上执⾏。协程是⼀种⾮常轻量级的实现,可在单个进程⾥执⾏成千上万的并发任务。
•调度器不能保证多个 goroutine 执⾏次序,且进程退出时不会等待它们结束。
•Go 协程之间通过信道(channel)进行通信。
•协程里可以创建协程
(1)协程的创建
package main import ( "fmt" "time" ) func hello() { fmt.Println("Hello world goroutine") } func main() { //开启了一个新的协程。hello() 函数将和 main() 函数一起运行。 go hello() time.Sleep(1 * time.Second) //延时结束主程序,不然不能保证主程序会等协程程序 fmt.Println("main function") }
(2)创建多个协程
package main import ( "fmt" "time" ) func numbers() { for i := 1; i <= 5; i++ { time.Sleep(250 * time.Millisecond) fmt.Printf("%d ", i) } } func alphabets() { for i := 'a'; i <= 'e'; i++ { time.Sleep(400 * time.Millisecond) fmt.Printf("%c ", i) } } func main() { //numbers()和alphabets()并发执行 go numbers() go alphabets() time.Sleep(3000 * time.Millisecond) fmt.Println("Main Over") }
输出:
1 a 2 3 b 4 c 5 d e Main Over
(3)调⽤ runtime.Goexit()将⽴即终⽌当前 goroutine 执⾏。但所有已注册 defer延迟调⽤会被被执⾏。
修改一下上面的代码
func alphabets() { defer fmt.Println("结束") //defer会被调用 for i := 'a'; i <= 'e'; i++ { time.Sleep(400 * time.Millisecond) fmt.Printf("%c ", i) runtime.Goexit() //立即结束该协程 } }
程序输出则会变为
1 a 结束
2 3 4 5 Main Over
(4)调⽤ runtime.Gosched()将当前 goroutine 暂停,放回队列等待下次被调度执⾏。
package main import ( "fmt" "runtime" ) func main() { go func() { //子协程 //没来的及执行主进程结束 for i := 0; i < 5; i++ { fmt.Println(i) } }() for i := 0; i < 2; i++ { //默认先执行主进程主进程执行完毕 //让出时间片,先让别的协议执行,它执行完,再回来执行此协程 runtime.Gosched() fmt.Println("执行") } }
三、信道(Channel)
信道(Channel)可以被认为是协程之间通信的管道。数据可以从信道的一端发送并在另一端接收。
•默认为同步模式,需要发送和接收配对。否则会被阻塞,直到另⼀⽅准备好后被唤醒。
•信道支持单向信道
信道声明
var ch chan T
我们声明了一个T类型的名称叫做ch的信道
信道的 0 值为 nil。我们需要通过内置函数 make 来创建一个信道,就像创建 map 和 slice 一样。
ch := make(chan T)
(1)信道的创建
//内置类型channel var a chan int if a == nil { a = make(chan int) fmt.Printf("%T\n", a) //chan int } //自定义类型channel var p chan person if p == nil { p = make(chan person) //chan main.person fmt.Printf("%T\n", p) }
(2)通过信道发送和接收数据
data := <- a // 从信道 a 中读取数据并将读取的值赋值给变量 data 。 a <- data // 向信道 a 中写入数据。
(3)发送和接收默认是阻塞的
package main import ( "fmt" "time" ) func hello(done chan bool) { fmt.Println("hello go routine is going to sleep") time.Sleep(4 * time.Second) //只有写数据后才能继续执行 done <- true fmt.Println("hello go routine awake and going to write to done") } func main() { done := make(chan bool) go hello(done) <-done fmt.Println("Main received data") }
(4)死锁
使用信道是要考虑的一个重要因素是死锁(Deadlock)只读未写与只写未读都会触发死锁,并触发 panic 。
channel 上如果发生了流入和流出不配对,就可能会发生死锁。
package main func main() { ch := make(chan int) ch <- 5 //只写未读触发死锁 }
(6)单向信道与关闭信道close()
发送者可以关闭信道以通知接收者将不会再发送数据给信道。
v, ok := <- ch
判断信道是否已关闭
package main import ( "fmt" ) //只写操作 func sendData(sendch chan<- int) { sendch <- 10 //不能读 //<-sendch close(sendch) //显式关闭信道 } //只读操作 func readData(sendch <-chan int) { <-sendch } func main() { sendch := make(chan int) go sendData(sendch) v, ok := <-sendch //ok 返回 true 表示成功的接收到了发送的数据,如果 ok 返回 false 则表示信道已经被关闭。 v1, ok1 := <-sendch fmt.Println(v, ok) //10 true fmt.Println(v1, ok1) //0 false }
(7)遍历信道
信道支持range for遍历
package main import ( "fmt" "time" ) func producer(chnl chan int) { defer close(chnl) //程序执行结束关闭信道 for i := 0; i < 10; i++ { time.Sleep(300 * time.Millisecond) //一秒写一次 chnl <- i //写操作 } } func main() { ch := make(chan int) go producer(ch) //接收ch信道中的数据,直到该信道关闭。 for v := range ch { fmt.Println(v) } }
也可以自定for循环遍历信道
for { v, ok := <-ch //读操作 fmt.Println(v, ok) if ok == false { //当读取不到数据跳出循环 break } }
(8)缓冲信道
语法结构
ch := make(chan type, cap)
cap为容量。
•缓冲信道支持len()和cap()
•只能先缓冲信道写容量以内的数据
•只能读缓冲信道长度以内的数据
func main() { //创建一个容量为3的缓冲信道 ch := make(chan string, 3) ch <- "naveen" ch <- "paul" fmt.Println("capacity is", cap(ch)) //capacity is 3 fmt.Println("length is", len(ch)) //length is 2 fmt.Println("read value", <-ch) //read value naveen fmt.Println("new length is", len(ch)) //new length is 1 }
(9)WaitGroup
假设我们有 3 个并发执行的 Go 协程(由Go 主协程生成)。Go 主协程需要等待这 3 个协程执行结束后,才会终止。这就可以用 WaitGroup 来实现。
package main import ( "fmt" "sync" "time" ) func process(i int, wg *sync.WaitGroup) { fmt.Println("started Goroutine ", i) time.Sleep(2 * time.Second) fmt.Printf("Goroutine %d ended\n", i) //Done方法减少WaitGroup计数器的值,应在线程的最后执行。 wg.Done() } /* WaitGroup用于等待一组线程的结束。 父线程调用Add方法来设定应等待的线程的数量。 每个被等待的线程在结束时应调用Done方法。 同时,主线程里可以调用Wait方法阻塞至所有线程结束。 */ func main() { no := 3 var wg sync.WaitGroup //并发协程 for i := 0; i < no; i++ { /* Add方法向内部计数加上delta,delta可以是负数; 如果内部计数器变为0,Wait方法阻塞等待的所有线程都会释放, 如果计数器小于0,方法panic。 */ wg.Add(1) go process(i, &wg) } //Wait方法阻塞直到WaitGroup计数器减为0。 wg.Wait() fmt.Println("over") }
(9)select
select 语句用于在多个发送/接收信道操作中进行选择。select 语句会一直阻塞,直到发送/接收操作准备就绪。
•如果有多个信道操作准备完毕,select 会随机地选取其中之一执行。
•空的select会触发死锁因此它会一直阻塞,导致死锁。
package main import ( "fmt" "time" ) func server1(ch chan string) { time.Sleep(1 * time.Second) ch <- "from server1" } func server2(ch chan string) { time.Sleep(1 * time.Second) ch <- "from server2" } func main() { output1 := make(chan string) output2 := make(chan string) go server1(output1) go server2(output2) //随机选择 select { case s1 := <-output1: fmt.Println(s1) case s2 := <-output2: fmt.Println(s2) } }
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Go 并发 -- 信道
- 调查:工控系统设备面临边信道严重威胁
- SSH加入抵御边信道攻击的功能
- CVE-2018-3639 | 最新侧信道攻击详细分析
- 基于时延的盲道研究:受限环境下的内容回传信道
- 模型压缩:识别感知的深度神经网络信道裁剪 | 论文分享
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。