Golang学习笔记之并发.协程(Goroutine)、信道(Channel)

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)
有缓冲信道
ch := make(chan T,cap)

(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)
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容