Go中的channel

channel
Go语言中的通道(channel)是一种特殊的类型。
在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。

通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。

(1)channel本身是一个队列,先进先出
(2)线程安全,不需要加锁
(3)本身是有类型的,string, int 等,如果要存多种类型,则定义成 interface类型
(4)channel是引用类型,必须make之后才能使用,一旦 make,它的容量就确定了,不会动态增加!!它和map,slice不一样

特点:
(1)一旦初始化容量,就不会改变了。
(2)当写满时,不可以写,取空时,不可以取。
(3)发送将持续阻塞直到数据被接收
把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go 程序运行时能智能地发现一些永远无法发送成功的语句并做出提示
(4)接收将持续阻塞直到发送方发送数据。
如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
(5)每次接收一个元素。
通道一次只能接收一个数据元素。

1、关于 channel的声明和使用的代码:
package main
import (
    "fmt"
)

func main() {

    //演示一下管道的使用
    //1. 创建一个可以存放3个int类型的管道
    var intChan chan int
    intChan = make(chan int, 3)

    //2. 看看intChan是什么
    fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)


    //3. 向管道写入数据
    intChan<- 10
    num := 211
    intChan<- num
    intChan<- 50
    // //如果从channel取出数据后,可以继续放入
    <-intChan
    intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量


    //4. 看看管道的长度和cap(容量)
    fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3

    //5. 从管道中读取数据

    var num2 int
    num2 = <-intChan 
    fmt.Println("num2=", num2)
    fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))  // 2, 3

    //6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock

    num3 := <-intChan
    num4 := <-intChan

    //num5 := <-intChan

    fmt.Println("num3=", num3, "num4=", num4/*, "num5=", num5*/)

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
1
这句代码显示:channel其实和指针一样,本身存放在一个内存单元中,有它的地址,而它的值是一个 int类型的地址。

2、注意空接口类型的 channel
package main
import (
    "fmt"
)

type Cat struct {
    Name string
    Age int
}

func main() {

    //定义一个存放任意数据类型的管道 3个数据
    //var allChan chan interface{}
    allChan := make(chan interface{}, 3)

    allChan<- 10
    allChan<- "tom jack"
    cat := Cat{"小花猫", 4}
    allChan<- cat

    //我们希望获得到管道中的第三个元素,则先将前2个推出
    <-allChan
    <-allChan

    newCat := <-allChan //从管道中取出的Cat是什么?

    fmt.Printf("newCat=%T , newCat=%v\n", newCat, newCat)
    //下面的写法是错误的!编译不通过
    //fmt.Printf("newCat.Name=%v", newCat.Name)
    //使用类型断言
    a := newCat.(Cat) 
    fmt.Printf("newCat.Name=%v", a.Name)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
定义 interface类型的空接口,可以接收任意类型的数据,但是在取出来的时候,必须断言!
a := newCat.(Cat)

3、channel的关闭:close( )
关闭之后,不能再写入,只能读。
只能由发送者执行这句代码

4、channel的遍历: for … range
通道的数据接收一共有以下 4 种写法。

阻塞接收数据
阻塞模式接收数据时,将接收变量作为<-操作符的左值,格式如下:
data := <-ch
1
执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。

非阻塞接收数据(有问题啊,还是会报错deadlock)
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:
data, ok := <-ch
1
data:表示接收到的数据。未接收到数据时,data 为通道类型的零值。
ok:表示是否接收到数据。

非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要实现接收超时检测,可以配合 select 和计时器 channel进行,可以参见后面的内容。

golang中for循环遍历channel时需要注意的问题详解
pdf

0星
超过10%的资源
55KB

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

使用通道做并发同步的写法,可以参考下面的例子:

package main

import (
    "fmt"
)

func main() {

    // 构建一个通道
    ch := make(chan int)

    // 开启一个并发匿名函数
    go func() {

        fmt.Println("start goroutine")

        // 通过通道通知main的goroutine
        ch <- 0

        fmt.Println("exit goroutine")

    }()

    fmt.Println("wait goroutine")

    // 等待匿名goroutine
    <-ch

    fmt.Println("all done")

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
循环接收
通道的数据接收可以借用 for range 语句进行多个元素的接收操作,格式如下:
for data := range ch {
}
1
2
通道 ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过 for 遍历获得的变量只有一个,即上面例子中的 data。

package main
import (
    "fmt"
)

func main() {

    intChan := make(chan int, 3)
    intChan<- 100
    intChan<- 200
    close(intChan) // close
    //这时不能够再写入数到channel
    //intChan<- 300
    fmt.Println("okook~")
    //当管道关闭后,读取数据是可以的
    n1 := <-intChan
    fmt.Println("n1=", n1)


    //遍历管道
    intChan2 := make(chan int, 100)
    for i := 0; i < 100; i++ {
        intChan2<- i * 2  //放入100个数据到管道
    }

    //遍历管道不能使用普通的 for 循环
    // for i := 0; i < len(intChan2); i++ {

    // }
    //在遍历时,如果channel没有关闭,则会出现deadlock的错误
    //在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
    close(intChan2)
    for v := range intChan2 { //没有下标
        fmt.Println("v=", v)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
在遍历管道之前要先关闭管道,不然会出现deadlock的错误

应用1


Go语言range关键字循环时的坑
pdf

0星
超过10%的资源
43KB

下载


开两个管道;
当写协程完成工作之后,close数据管道,读协程对数据管道 intChan的数据读完之后,就向退出管道 exitChan 写入一个 true,close掉;

主线程循环检测退出管道里是否有数据,如果有,说明读协程完成,主程序就可以退出了。

package main

import (
    "fmt"
)

//write Data
func writeData(intChan chan int) {
    for i := 1; i <= 50; i++ {
        //放入数据
        intChan <- i //
        fmt.Println("writeData ", i)
        //time.Sleep(time.Second)
    }
    close(intChan) //关闭
}

//read data
func readData(intChan chan int, exitChan chan bool) {

    for {
        v, ok := <-intChan
        if !ok {
            break
        }
        // time.Sleep(time.Second)
        fmt.Printf("readData 读到数据=%v\n", v)
    }
    //readData 读取完数据后,即任务完成
    exitChan <- true
    close(exitChan)

}

func main() {

    //创建两个管道
    intChan := make(chan int, 10)
    exitChan := make(chan bool, 1)

    go writeData(intChan)
    go readData(intChan, exitChan)

    //time.Sleep(time.Second * 10)
    for {
        _, ok := <-exitChan
        if !ok {
            break
        }
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
应用2


定义三个管道:
intChan :放8000个数
primeChan:放素数
exitChan :4个协程运行完毕的标志

package main

import (
    "fmt"
    "time"
)

//向 intChan放入 1-8000个数
func putNum(intChan chan int) {

    for i := 1; i <= 80000; i++ {
        intChan <- i
    }

    //关闭intChan
    close(intChan)
}

// 从 intChan取出数据,并判断是否为素数,如果是,就
//     //放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {

    //使用for 循环
    // var num int
    var flag bool //
    for {
        //time.Sleep(time.Millisecond * 10)
        num, ok := <-intChan //intChan 取不到..

        if !ok {
            break
        }
        flag = true //假设是素数
        //判断num是不是素数
        for i := 2; i < num; i++ {
            if num%i == 0 { //说明该num不是素数
                flag = false
                break
            }
        }

        if flag {
            //将这个数就放入到primeChan
            primeChan <- num
        }
    }

    fmt.Println("有一个primeNum 协程因为取不到数据,退出")
    //这里我们还不能关闭 primeChan
    //向 exitChan 写入true
    exitChan <- true

}

func main() {

    intChan := make(chan int, 1000)
    primeChan := make(chan int, 20000) //放入结果
    //标识退出的管道
    exitChan := make(chan bool, 4) // 4个

    start := time.Now().Unix()

    //开启一个协程,向 intChan放入 1-8000个数
    go putNum(intChan)
    //开启4个协程,从 intChan取出数据,并判断是否为素数,如果是,就
    //放入到primeChan
    for i := 0; i < 4; i++ {
        go primeNum(intChan, primeChan, exitChan)
    }

    //这里我们主线程,进行处理
    //直接
    go func() {
        for i := 0; i < 4; i++ {
            <-exitChan
        }

        end := time.Now().Unix()
        fmt.Println("使用协程耗时=", end-start)

        //当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
        close(primeChan)
    }()

    //遍历我们的 primeChan ,把结果取出
    for {
        res, ok := <-primeChan
        if !ok {
            break
        }
        //将结果输出
        fmt.Printf("素数=%d\n", res)
    }

    fmt.Println("main线程退出")

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
有一个primeNum 协程因为取不到数据,退出
使用协程耗时= 3
main线程退出

1
2
3
4
5
6
7
存数字和计算素数比较简单,不提
开启4个协程,运算素数,效率比单个线程高几倍!

go func() {
        for i := 0; i < 4; i++ {
            <-exitChan
        }

        end := time.Now().Unix()
        fmt.Println("使用协程耗时=", end-start)

        //当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
        close(primeChan)
    }()
1
2
3
4
5
6
7
8
9
10
11
这里定义了一个匿名协程,作用是检测4个协程 有没有完成运行,取不出来就会阻塞,等待协程完成。也可以这样:
if len(exitChan) == 4
————————————————
版权声明:本文为CSDN博主「始梦的少年」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44883180/article/details/124413145

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言channel、sync.WaitGroup和context是三个非常重要的并发编程工具。下面我将对它们进行详细介绍。 ### Channel Go语言channel是一种在多个goroutine之间进行通信的机制。也可以说,channel是一种数据结构,它可以让一个goroutine向另一个goroutine发送一个值,同时还可以让另一个goroutine从channel接收这个值。在Go语言,使用make函数创建一个channel。例如: ```go ch := make(chan int) ``` 这行代码创建了一个类型为int的channel。可以在goroutine使用ch <- value语句向channel发送一个整数,例如: ```go go func() { ch <- 1 }() ``` 可以使用value := <- ch语句从channel接收一个整数,例如: ```go value := <- ch ``` 这行代码会阻塞,直到有一个整数被发送到这个channel为止。需要注意的是,如果没有接收者,发送操作会一直阻塞,直到有接收者为止;如果没有发送者,接收操作也会一直阻塞,直到有发送者为止。 ### sync.WaitGroup sync.WaitGroup是Go语言的一个同步工具,它可以等待一组goroutine完成工作。在WaitGroup,每个goroutine的工作完成后,都需要调用Done方法。主goroutine可以在Wait方法上阻塞,等待所有的goroutine完成工作。例如: ```go var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { // do some work wg.Done() }() } wg.Wait() ``` 这行代码创建了一个WaitGroup,并且启动了10个goroutine进行工作。每个goroutine完成工作后,都会调用wg.Done方法,主goroutine在wg.Wait上阻塞,等待所有的goroutine完成工作。 ### context context是Go语言的一个用于传递请求范围数据的机制。在一个请求处理,可以使用context携带一些请求数据,同时也可以使用context取消请求处理。例如: ```go func handleRequest(ctx context.Context) { // do some work select { case <-ctx.Done(): // handle cancelation default: // continue working } } ``` 这行代码定义了一个处理请求的函数,该函数接收一个context参数。如果context被取消,处理请求的函数将会停止工作。例如: ```go ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(time.Second) cancel() }() handleRequest(ctx) ``` 这行代码创建了一个带有取消功能的context,并且启动了一个goroutine在1秒后取消context。handleRequest函数会使用这个context来处理请求,并且如果context被取消,handleRequest函数会立刻停止工作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值