脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|shell|

服务器之家 - 脚本之家 - Golang - 超实用的Golang通道指南之轻松实现并发编程

超实用的Golang通道指南之轻松实现并发编程

2023-04-26 15:15金刀大菜牙 Golang

Golang 中的通道是一种高效、安全、灵活的并发机制,用于在并发环境下实现数据的同步和传递。本文主要介绍了如何利用通道轻松实现并发编程,需要的可以参考一下

1. 什么是 Golang 通道

Golang 中的通道是一种高效、安全、灵活的并发机制,用于在并发环境下实现数据的同步和传递。通道提供了一个线程安全的队列,只允许一个 goroutine 进行读操作,另一个 goroutine 进行写操作。通过这种方式,通道可以有效地解决并发编程中的竞态条件、锁问题等常见问题。

通道有两种类型:有缓冲通道和无缓冲通道。在通道创建时,可以指定通道的容量,即通道缓冲区的大小,如果不指定则默认为无缓冲通道。

2. Golang 通道的基本语法

Golang 通道的基本语法非常简单,使用 make 函数来创建一个通道:

?
1
ch := make(chan int)

这行代码创建了一个名为 ch 的通道,通道的数据类型为 int。通道的读写操作可以使用箭头符号 <-,<- 表示从通道中读取数据,-> 表示向通道中写入数据。例如:

?
1
2
3
ch := make(chan int)
ch <- 1 // 向通道中写入数据1
x := <- ch // 从通道中读取数据,并赋值给变量x

3. Golang 通道的缓冲机制

在 Golang 中,通道还支持缓冲机制。通道的缓冲区可以存储一定量的数据,当缓冲区满时,向通道写入数据将阻塞。当通道缓冲区为空时,从通道读取数据将阻塞。使用缓冲机制可以增加程序的灵活性和并发性能。

缓冲区大小为 0 的通道称为无缓冲通道。无缓冲通道的发送和接收操作都是阻塞的,因此必须有接收者准备好接收才能进行发送操作,反之亦然。这种机制确保了通道的同步性,即在通道操作前后,发送者和接收者都会被阻塞,直到对方做好准备。

3.1 有缓冲通道

有缓冲通道的创建方式为:

?
1
ch := make(chan int, 3)

这行代码创建了一个名为 ch 的通道,通道的数据类型为 int,通道缓冲区的大小为 3。向有缓冲通道写入数据时,如果缓冲区未满,则写操作将成功,程序将继续执行。如果缓冲区已满,则写操作将阻塞,直到有空闲缓冲区可用。

从有缓冲通道读取数据时,如果缓冲区不为空,则读操作将成功,程序将继续执行。如果缓冲区为空,则读操作将阻塞,直到有数据可读取。

3.2 无缓冲通道

无缓冲通道的创建方式为:

?
1
ch := make(chan int)

这行代码创建了一个名为ch的通道,通道的数据类型为 int,通道缓冲区的大小为 0。无缓冲通道的发送和接收操作都是阻塞的,因此必须有接收者准备好接收才能进行发送操作,反之亦然。

4. Golang 通道的超时和计时器

在并发编程中,常常需要对通道进行超时和计时操作。Golang 中提供了 time 包来实现超时和计时器。

4.1 超时机制

在 Golang 中,可以使用 select 语句和 time.After 函数来实现通道的超时操作。例如:

?
1
2
3
4
5
6
select {
    case data := <-ch:
        fmt.Println(data)
    case <-time.After(time.Second):
        fmt.Println("timeout")
}

这段代码中,select 语句监听了通道 ch 和 time.After(time.Second) 两个信道,如果 ch 中有数据可读,则读取并输出数据;如果等待 1 秒钟后仍然没有数据,则超时并输出 timeout。

4.2 计时器机制

Golang 中提供了 time 包来实现计时器机制。可以使用 time.NewTimer(duration) 函数创建一个计时器,计时器会在 duration 时间后触发一个定时事件。例如:

?
1
2
3
timer := time.NewTimer(time.Second * 2)
<-timer.C
fmt.Println("Timer expired")

这段代码创建了一个计时器,设定时间为 2 秒钟,当计时器到达 2 秒钟时,会向 timer.C 信道中发送一个定时事件,程序通过 <-timer.C 语句等待定时事件的到来,并在接收到定时事件后输出 “Timer expired”。

5. Golang 通道的传递

在 Golang 中,通道是一种引用类型,可以像普通变量一样进行传递。例如:

?
1
2
3
4
5
6
7
8
9
10
11
func worker(ch chan int) {
    data := <-ch
    fmt.Println(data)
}
 
func main() {
    ch := make(chan int)
    go worker(ch)
    ch <- 1
    time.Sleep(time.Second)
}

这段代码中,main 函数中创建了一个名为ch的通道,并启动了一个 worker goroutine,向 ch 通道中写入了一个数据 1。worker goroutine 中通过 <-ch 语句从 ch 通道中读取数据,并输出到控制台中。

6. 单向通道

在 Golang 中,可以通过使用单向通道来限制通道的读写操作。单向通道只允许读或写操作,不允许同时进行读写操作。例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func producer(ch chan<- int) {
    ch <- 1
}
 
func consumer(ch <-chan int) {
    data := <-ch
    fmt.Println(data)
}
 
func main() {
    ch := make(chan int)
    go producer(ch)
    go consumer(ch)
    time.Sleep(time.Second)
}

这段代码中,produce r函数和 consumer 函数分别用于向通道中写入数据和从通道中读取数据。在函数的参数中,使用了单向通道限制参数的读写操作。在 main 函数中,创建了一个名为 ch 的通道,并启动了一个 producer goroutine 和一个 consumer goroutine,producer 向 ch 通道中写入数据1,consumer 从 ch 通道中读取数据并输出到控制台中。

7. 关闭通道

在 Golang 中,可以使用 close 函数来关闭通道。关闭通道后,通道的读写操作将会失败,读取通道将会得到零值,写入通道将会导致 panic 异常。例如:

?
1
2
3
4
5
6
7
8
9
10
ch := make(chan int)
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}()
for data := range ch {
    fmt.Println(data)
}

这段代码中,创建了一个名为 ch 的通道,并在一个 goroutine 中向通道中写入数据 0 到 4,并通过 close 函数关闭通道。在主 goroutine 中,通过 for...range 语句循环读取通道中的数据,并输出到控制台中,当通道被关闭时,for...range 语句会自动退出循环。

在关闭通道后,仍然可以从通道中读取已经存在的数据,例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ch := make(chan int)
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}()
for {
    data, ok := <-ch
    if !ok {
        break
    }
    fmt.Println(data)
}

这段代码中,通过循环读取通道中的数据,并判断通道是否已经被关闭。当通道被关闭时,读取操作将会失败,ok 的值将会变为 false,从而退出循环。

8. 常见的应用场景

通道是 Golang 并发编程中的重要组成部分,其常见的应用场景包括:

8.1 同步数据传输

通道可以被用来在不同的 goroutine 之间同步数据。当一个 goroutine 需要等待另一个goroutine 的结果时,可以使用通道进行数据的传递。例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
 
import "fmt"
 
func calculate(a, b int, result chan int) {
    result <- a + b
}
 
func main() {
    result := make(chan int)
    go calculate(10, 20, result)
    fmt.Println(<-result)
}

在这个例子中,我们使用通道来进行 a+b 的计算,并将结果发送给主函数。在主函数中,我们等待通道中的结果并输出。

8.2 协调多个 goroutine

通道也可以用于协调多个 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
package main
 
import (
    "fmt"
    "sync"
)
 
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker", id, "processing job", j)
        results <- j * 2
    }
}
 
func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
 
    // 开启三个worker goroutine
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
 
    // 发送9个任务到jobs通道中
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)
 
    // 输出每个任务的结果
    for a := 1; a <= 9; a++ {
        <-results
    }
}

在这个例子中,我们使用通道来协调三个 worker goroutine 之间的任务处理。每个 worker goroutine 从 jobs 通道中获取任务,并将处理结果发送到 results 通道中。主函数负责将所有任务发送到 jobs 通道中,并等待所有任务的结果返回。

8.3 控制并发访问

当多个 goroutine 需要并发访问某些共享资源时,通道可以用来控制并发访问。通过使用通道,可以避免出现多个 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
package main
 
import (
    "fmt"
    "sync"
)
 
var (
    balance int
    wg      sync.WaitGroup
    mutex   sync.Mutex
)
 
func deposit(amount int) {
    mutex.Lock()
    balance += amount
    mutex.Unlock()
    wg.Done()
}
 
func main() {
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go deposit(100)
    }
    wg.Wait()
    fmt.Println("balance:", balance)
}

在这个例子中,我们使用互斥锁来控制对 balance 变量的并发访问。每个 goroutine 负责将 100 元存入 balance 变量中。使用互斥锁可以确保在任意时刻只有一个 goroutine 能够访问 balance 变量。

8.4 模拟事件驱动

通道也可以用来模拟事件驱动的机制。例如,可以使用通道来模拟一个事件队列,当某个事件发生时,可以将事件数据放入通道中,然后通过另一个 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
package main
 
import (
    "fmt"
    "time"
)
 
func eventLoop(eventChan <-chan string) {
    for {
        select {
        case event := <-eventChan:
            fmt.Println("Event received:", event)
        case <-time.After(5 * time.Second):
            fmt.Println("Timeout reached")
            return
        }
    }
}
 
func main() {
    eventChan := make(chan string)
 
    // 模拟事件发生
    go func() {
        time.Sleep(2 * time.Second)
        eventChan <- "Event 1"
        time.Sleep(1 * time.Second)
        eventChan <- "Event 2"
        time.Sleep
        1 * time.Second
        eventChan <- "Event 3"
        time.Sleep(4 * time.Second)
        eventChan <- "Event 4"
    }()
    eventLoop(eventChan)
}

在这个例子中,我们使用通道来模拟事件的发生。eventLoop 函数使用 select 语句监听 eventChan 通道和 5 秒超时事件。当 eventChan 收到事件时,eventLoop 函数将事件打印出来。如果 5 秒内没有收到事件,则 eventLoop 函数结束。主函数负责创建 eventChan 通道,并模拟事件的发生。

8.5 批量处理任务

?
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"
    "sync"
)
 
func processTask(task int) {
    fmt.Println("Processing task", task)
}
 
func main() {
    tasks := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    // 定义并发数为3的批量处理函数
    batchSize := 3
    var wg sync.WaitGroup
    taskChan := make(chan int)
    for i := 0; i < batchSize; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for task := range taskChan {
                processTask(task)
            }
        }()
    }
 
    // 将任务分发到taskChan通道中
    for _, task := range tasks {
        taskChan <- task
    }
    close(taskChan)
 
    wg.Wait()
}

在这个例子中,我们使用通道来批量处理任务。首先定义了一个包含 10 个任务的数组。然后,我们定义了一个并发数为 3 的批量处理函数,它从 taskChan 通道中获取任务,并将任务处理结果输出。主函数负责将所有任务发送到 taskChan 通道中,并等待所有任务处理结束。注意,我们使用了 sync.WaitGroup 来等待所有批量处理函数的 goroutine 结束。

8.6 实现发布/订阅模式

?
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
package main
 
import "fmt"
 
type eventBus struct {
    subscriptions map[string][]chan string
}
 
func newEventBus() *eventBus {
    return &eventBus{
        subscriptions: make(map[string][]chan string),
    }
}
 
func (eb *eventBus) subscribe(eventType string, ch chan string) {
    eb.subscriptions[eventType] = append(eb.subscriptions[eventType], ch)
}
 
func (eb *eventBus) unsubscribe(eventType string, ch chan string) {
    subs := eb.subscriptions[eventType]
    for i, sub := range subs {
        if sub == ch {
            subs[i] = nil
            eb.subscriptions[eventType] = subs[:i+copy(subs[i:], subs[i+1:])]
            break
        }
    }
}
 
func (eb *eventBus) publish(eventType string, data string) {
    for _, ch := range eb.subscriptions[eventType] {
        if ch != nil {
            ch <- data
        }
    }
}
 
func main() {
    eb := newEventBus()
 
    ch1 := make(chan string)
    ch2 := make(chan string)
 
    eb.subscribe("event1", ch1)
    eb.subscribe("event2", ch2)
 
    go func() {
        for {
            select {
            case data := <-ch1:
                fmt.Println("Received event1:", data)
            case data := <-ch2:
                fmt.Println("Received event2:", data)
            }
        }
    }()
 
    eb.publish("event1", "Event 1 data")
    eb.publish("event2", "Event 2 data")
 
    eb.unsubscribe("event1", ch1)
 
    eb.publish("event1", "Event 1 data after unsubscribe")
 
    // 等待事件处理完成
    fmt.Scanln()
}

在这个例子中,我们使用通道来实现发布/订阅模式。定义了一个 eventBus 结构体,它包含了一个 subscriptions map,用来存储事件类型和订阅该事件类型的所有通道。我们可以通过 subscribe 函数向某个事件类型添加订阅通道,通过 unsubscribe 函数取消订阅通道,通过 publish 函数向某个事件类型发布事件。

在主函数中,我们创建了两个通道 ch1 和 ch2,并通过 subscribe 函数订阅了 "event1" 和 "event2" 两个事件类型。然后,我们启动了一个 goroutine,使用 select 语句监听 ch1 和 ch2 通道,将接收到的事件打印出来。接着,我们使用 publish 函数分别向 "event1" 和 "event2" 发布了事件。最后,我们使用 unsubscribe 函数取消了对 "event1" 事件类型的 ch1 通道的订阅,再次使用 publish 函数向 "event1" 发布了事件。注意,我们使用了 fmt.Scanln() 来等待事件处理完成,以避免程序在事件处理完毕前退出。

9. 总结

通道是 Go 中非常重要的并发原语,可以有效地管理并发访问共享数据,避免数据竞争。通过通道,可以实现同步和异步的消息传递,实现不同 goroutine 之间的通信。在使用通道时,需要注意通道的基本语法、缓冲机制、超时和计时器、通道的传递、单向通道和关闭通道等知识点,并根据实际场景选择合适的通道模式,以提高程序的并发性能和稳定性。

以上就是超实用的Golang通道指南之轻松实现并发编程的详细内容,更多关于Golang通道实现并发编程的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7225230646246981692

延伸 · 阅读

精彩推荐
  • Golanggolang程序进度条实现示例详解

    golang程序进度条实现示例详解

    这篇文章主要为大家介绍了golang程序实现进度条示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪...

    jonylee11292022-08-29
  • Golanggolang 墙上时钟与单调时钟的实现

    golang 墙上时钟与单调时钟的实现

    本文主要介绍了golang 墙上时钟与单调时钟的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面...

    浪狼郎9352022-07-21
  • GolangGo实现双向链表的示例代码

    Go实现双向链表的示例代码

    这篇文章主要介绍了Go实现双向链表的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面...

    link1st3352020-05-28
  • GolangGo语言基础go doc命令用法及示例详解

    Go语言基础go doc命令用法及示例详解

    这篇文章主要为大家介绍了Go语言基础go doc命令的用法及示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步...

    枫少文4682021-12-08
  • Golanggolang使用信号量热更新的实现示例

    golang使用信号量热更新的实现示例

    这篇文章主要介绍了golang使用信号量热更新的实现示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧 ...

    wshelly4862020-05-15
  • Golanggolang实现各种情况的get请求操作

    golang实现各种情况的get请求操作

    这篇文章主要介绍了golang实现各种情况的get请求操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    千年恨8432021-03-07
  • GolangGo语言json编码驼峰转下划线、下划线转驼峰的实现

    Go语言json编码驼峰转下划线、下划线转驼峰的实现

    这篇文章主要介绍了Go语言json编码驼峰转下划线、下划线转驼峰的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价...

    雪山飞猪4782020-07-16
  • Golang深度解密Go语言中字符串的使用

    深度解密Go语言中字符串的使用

    在编程语言中,字符串发挥着重要的角色。这篇文章就来带大家一起深度解密Go语言中的字符串,文中的示例代码讲解详细,需要的可以参考一下...

    古明地觉5052022-11-22