go——通道

相比Erlang,go并未实现严格的并发安全。
允许全局变量、指针、引用类型这些非安全内存共享操作,就需要开发人员自行维护数据一致和完整性。
Go鼓励使用CSP通道,以通信来代替内存共享,实现并发安全。
作为CSP核心,通道(channel)是显式地,要求操作双方必须知道数据类型和具体通道,并不关心另一端操作者身份和数量。
可如果另一端未准备妥当,或消息未能及时处理时,会阻塞当前端。
相比起来,Actor是透明地,它不在乎数据类型及通道,只要知道接收者信箱即可。
默认就是异步方式,发送方消息是否被接收和处理并不关心。
从底层实现上来说,通道只是一个队列。
同步模式下,发送和接收双方配对,然后直接复制数据给对方。
如果配对失败,则置入等待队列,直到另一方出现后才被唤醒。
异步模式抢夺地则是数据缓冲槽。发送方要求有空槽可供写入,而接收方则要求有缓冲数据可读。
需求不符时,同样加入等待队列,直到有另一方写入数据或腾出空槽后被唤醒。

除传递消息(数据)外,通道还常被用作事件通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
 
import "fmt"
 
func main() {
    done := make(chan struct{}) //消息传递通道
    c := make(chan string)      //数据传输通道
 
    go func() {
        s := <-c //接收消息
        fmt.Println(s)
        close(done) //关闭同道,作为结束通知
    }()
 
    c <- "hi" //发送消息
    <-done    //阻塞,直到数据或管道关闭
}

  

同步模式必须有配对操作的goroutine出现,否则会一直阻塞。
而异步模式在缓冲区未满或数据未读前,不会阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
 
import "fmt"
 
func main() {
    c := make(chan int, 3) //创建带有三个缓冲槽地异步通道
 
    c <- 1 //缓冲区未满不会阻塞
    c <- 2
 
    fmt.Println(<-c) //缓冲区尚有数据,不会阻塞
    fmt.Println(<-c)
}

  

多数时候,异步通道有助于提升性能,减少队伍阻塞。
缓冲区大小仅是内部属性,不属于类型组成部分。
另外通道变量本身就是指针,可用相等操作符判断是否为同一对象或nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
 
import (
    "fmt"
    "unsafe"
)
 
func main() {
    var a, b chan int = make(chan int, 3), make(chan int)
    var c chan bool
 
    fmt.Println(a == b) //槽位不同
    fmt.Println(c == nil)
 
    fmt.Printf("%p, %d\n", a, unsafe.Sizeof(a))
}
 
/*
false
true
0xc000080080, 8
*/  

虽然可传递指针来避免数据复制,但须额外注意数据复制安全。

内置函数cap和len返回缓冲区大小和当前已缓存数量。
对于同步同步通道而言都返回0,据此可判断通道是同步还是异步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
 
import "fmt"
 
func main() {
    a, b := make(chan int), make(chan int, 3)
 
    b <- 1
    b <- 2
 
    fmt.Println("a:", len(a), cap(a))
    fmt.Println("b:", len(b), cap(b))
}
 
/*
a: 0 0  //给定槽位数量的就是异步
b: 2 3
*/

  

收发

除使用简单的发送和接收操作符外,还可以用ok-idom或range模式处理数据。

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
package main
 
import "fmt"
 
func main() {
    done := make(chan struct{})
    c := make(chan int)
 
    go func() {
        defer close(done)
 
        for {
            x, ok := <-c
            if !ok {
                return
            }
            fmt.Println(x)
        }
        // for x := range c {
        //  fmt.Println(x)
        // }
         
    }()
 
    c <- 1
    c <- 2
    c <- 3
    close(c)
    <-done
}  

对于循环接收数据,range模式更简洁一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
 
import "fmt"
 
func main() {
    done := make(chan struct{})
    c := make(chan int)
 
    go func() {
        defer close(done)
 
        for x := range c {
            fmt.Println(x)
        }
    }()
 
    c <- 1
    c <- 2
    c <- 3
    close(c)
    <-done
}

 

及时用close函数关闭通道引发结束通知,否则可能会导致死锁。
通知可以是群体性的。也未必就是通知结束,可以是任何需要表达的事件。

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
package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
func main() {
    var wg sync.WaitGroup
    ready := make(chan struct{})
 
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
 
            fmt.Println(id, ":ready")
            <-ready  //接收消息
            fmt.Println(id, ":running...")
        }(i)
    }
 
    time.Sleep(time.Second)
    fmt.Println("ready? Go!")
 
    close(ready)  //关闭通道,发出消息
 
    wg.Wait()
}
 
 
/*
0 :ready
1 :ready
2 :ready
ready? Go!
0 :running...
2 :running...
1 :running...
*/  

 一次性事件用close效率更好,没有多余开销。连续或多样性事件,
可传递不同数据标志实现,还可以使用sync.Cloud实现单播或广播事件。


对于closed或nil通道,发送和接收操作都有相应规则。
向已关闭通道发送数据,引发panic。
从已关闭接收数据,返回已缓冲数据或零值。
无论收发,nil通道都会阻塞。

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
package main
 
import (
    "fmt"
)
 
func main() {
    c := make(chan int, 3)
 
    c <- 10
    c <- 20
    close(c)
 
    for i := 0; i < cap(c)+1; i++ {
        x, ok := <-c
        fmt.Println(i, ":", ok, x)
    }
}
 
/*
0 : true 10
1 : true 20
2 : false 0
3 : false 0
*/  

注意,重复关闭或关闭nil通道都会引发panic错误。



单向


通道默认是双向的,并不区分发送和接收端。
但某些时候,我们可限制收发操作的方向类获得更严谨的操作逻辑。
尽管可用make创建单向通道,但那没有任何意义。
通常使用类型转换来获取单向通道,并分别赋予操作双方。

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
package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
 
    c := make(chan int)
    var send chan<- int = c
    var recv <-chan int = c
 
    go func() {
        defer wg.Done()
 
        for x := range recv {
            fmt.Println(x)
        }
    }()
 
    go func() {
        defer wg.Done()
        defer close(c)
 
        for i := 0; i < 3; i++ {
            send <- i
        }
    }()
 
    wg.Wait()
}
 
/*
0
1
2
*/
 
/*

  

不能再单向通道上做逆向操作。

1
2
3
4
5
6
7
8
func main() {
    c := make(chan int, 2)
    var send chan<- int = c
    var recv <-chan int = c
 
    <-send
    recv <- 1
}

  

close不能用于接收端。

1
2
3
4
5
6
func main() {
    c := make(chan int, 2)
    var recv <-chan int = c
 
    close(recv)
}

  

无法将单向通道重新转换回去。

1
2
3
4
5
6
7
8
9
func main() {
    var a,b clan int
    a := make(chan int, 2)
    var send chan<- int = a
    var recv <-chan int = a
 
    b = (chan int)(recv)
    b = (chan int)(send)
}

  

选择

如果同时处理多个通道,可选用select语句。它会随机选择一个可用通道做收发操作。

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
package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
 
    a, b := make(chan int), make(chan int) //创建两个通道
 
    go func() {
        defer wg.Done()
 
        for {
            var ( //定义三个变量
                name string
                x    int
                ok   bool
            )
 
            select { //随机选择一个通道接收消息
            case x, ok = <-a:
                name = "a"
            case x, ok = <-b:
                name = "b"
            }
 
            if !ok { //如果任一通道关闭,则终止接收
                return
            }
 
            fmt.Println(name, x)
        }
    }()
 
    go func() {
        defer wg.Done()
        defer close(a)
        defer close(b)
 
        for i := 0; i < 10; i++ {
            select {
            case a <- i: //随机发送10次消息
            case b <- i * 10:
            }
        }
    }()
    wg.Wait()
}
 
/*
a 0
b 10
b 20
a 3
a 4
a 5
a 6
b 70
a 8
b 90
*/

  

如果等全部通道消息处理结束,可将已完成通道设置为nil,这样它就会被阻塞,不再被select选中。

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
package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    var wg sync.WaitGroup
    wg.Add(3)
 
    a, b := make(chan int), make(chan int)
 
    go func() {
        defer wg.Done()
 
        for {
            select {
            case x, ok := <-a:
                if !ok {
                    a = nil
                    break
                }
                fmt.Println("a", x)
            case x, ok := <-b:
                if !ok {
                    b = nil
                    break
                }
                fmt.Println("b", x)
            }
            if a == nil && b == nil {
                return
            }
 
        }
    }()
 
    go func() {
        defer wg.Done()
        defer close(a)
 
        for i := 0; i < 3; i++ {
            a <- i
        }
    }()
 
    go func() {
        defer wg.Done()
        defer close(b)
        for i := 0; i < 5; i++ {
            a <- i
        }
 
    }()
    wg.Wait()
 
}

  

即便是同一通道,也会随机选择case执行。

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
package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    var wg sync.WaitGroup
    wg.Add(2)
 
    c := make(chan int)
 
    go func() { //接收端
        defer wg.Done()
 
        for {
            var v int
            var ok bool
 
            select { //随机选择case
            case v, ok = <-c:
                fmt.Println("a1:", v)
            case v, ok = <-c:
                fmt.Println("a2:", v)
            }
            if !ok {
                return
            }
        }
    }()
 
    go func() { //发送端
        defer wg.Done()
        defer close(c)
 
        for i := 0; i < 10; i++ { //随机选择case
            select {
            case c <- i:
            case c <- i * 10:
            }
        }
    }()
    wg.Wait()
}
 
/*
a1: 0
a2: 1
a2: 2
a2: 3
a2: 40
a1: 50
a1: 6
a2: 7
a2: 8
a2: 9
a2: 0
*/

  

当所有通道都不可用时,select会执行default语句。
如此可避开select阻塞,但须注意处理外层循环,以免陷入空耗。

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
package main
 
import (
    "fmt"
    "time"
)
 
func main() {
    done := make(chan struct{})
    c := make(chan int)
 
    go func() {
        defer close(done)
 
        for {
            select {
            case x, ok := <-c:
                if !ok {
                    return
                }
                fmt.Println("data:", x)
            default: //避免select阻塞
            }
            fmt.Println(time.Now())
            time.Sleep(time.Second)
        }
    }()
 
    time.Sleep(time.Second * 5)
    c <- 100
    close(c)
 
    <-done
}
 
/*
2018-12-03 06:52:57.1009398 +0800 CST m=+0.007029001
2018-12-03 06:52:58.1185419 +0800 CST m=+1.024631101
2018-12-03 06:52:59.1187182 +0800 CST m=+2.024807401
2018-12-03 06:53:00.1190807 +0800 CST m=+3.025169901
2018-12-03 06:53:01.1194511 +0800 CST m=+4.025540301
data: 100
2018-12-03 06:53:02.1198158 +0800 CST m=+5.025905001
*/

  

用default处理一些默认逻辑。

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
package main
 
import (
    "fmt"
)
 
func main() {
    done := make(chan struct{})
 
    data := []chan int{
        make(chan int, 3),
    }
 
    go func() {
        defer close(done)
 
        for i := 0; i < 10; i++ {
            select {
            case data[len(data)-1] <- i:
            default:
                data = append(data, make(chan int, 3))
            }
        }
    }()
 
    <-done
 
    for i := 0; i < len(data); i++ {
        c := data[i]
        close(c)
        for x := range c {
            fmt.Println(x)
        }
    }
}

 

通常使用工厂方法将goroutine和通道绑定。

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
package main
 
import (
    "fmt"
    "sync"
)
 
type receiver struct {
    sync.WaitGroup
    data chan int
}
 
func newReceiver() *receiver {
    r := &receiver{
        data: make(chan int),
    }
 
    r.Add(1)
    go func() {
        defer r.Done()
        for x := range r.data {
            fmt.Println("recv:,", x)
        }
    }()
    return r
}
 
func main() {
    r := newReceiver()
    r.data <- 1
    r.data <- 2
 
    close(r.data)
    r.Wait()
}
 
/*
recv:, 1
recv:, 2
*/

  

鉴于通道本身就是一个并发安全的队列,可用作ID generator、Pool等用途。

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
package main
 
import (
     
)
 
type pool chan []byte
 
func newPool(cap int) pool {
    return make(chan []byte, cap)
}
 
func (p pool) get() []byte {
    var v []byte
     
    select {
        case v = <-p:     //返回
        default:          //返回失败,新建
            v = make([]byte, 10)
    }
     
    return v
}
 
func (p pool) put(b []byte) {
    select {
        case p <- b:   //放回
        default:   //放回失败,放弃
    }
}

  

用通道实现信号量。

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
package main
 
import (
    "fmt"
    "runtime"
    "sync"
    "time"
)
 
func main() {
    runtime.GOMAXPROCS(4)
    var wg sync.WaitGroup
 
    sem := make(chan struct{}, 2) //最多允许两个并发同时执行
    for i := 0; i < 5; i++ {
        wg.Add(1)
 
        go func(id int) {
            defer wg.Done()
 
            sem <- struct{}{}
            defer func() { <-sem }()
 
            time.Sleep(time.Second * 2)
            fmt.Println(id, time.Now())
        }(i)
    }
 
    wg.Wait()
}
 
/*
4 2018-12-03 07:23:18.054693 +0800 CST m=+2.004868201
0 2018-12-03 07:23:18.054693 +0800 CST m=+2.004868201
1 2018-12-03 07:23:20.0942525 +0800 CST m=+4.044427701
3 2018-12-03 07:23:20.0942525 +0800 CST m=+4.044427701
2 2018-12-03 07:23:22.0948917 +0800 CST m=+6.045066901
*/

  

 

posted @   明王不动心  阅读(467)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
历史上的今天:
2017-12-11 第12条:不要在for和while循环后面写else块
2017-12-11 将序列中的元素连起来——程序的思想(顺着电脑的思想)——闯关
2017-12-11 动态删除列表中的元素
2017-12-11 文件操作脚本(一)
2017-12-11 函数(1)
2017-12-11 构造器以及解构器
2017-12-11 文件传输协议FTP
点击右上角即可分享
微信分享提示