Go 语言入门很简单 -- 14. Go 并发初识 #私藏项目实操分享#

简介: Go 语言入门很简单 -- 14. Go 并发初识 #私藏项目实操分享#

大型程序通常由许多较小的子程序组成。  例如,Web 服务器处理来自 Web 浏览器的请求并提供 HTML 网页作为响应。  每个请求都像一个小程序一样被处理。对于像这样的程序,最理想的是能够 在同一时间运行它们的小型组件(在  网络服务器的情况下,处理多个请求)。同时在一个以上的任务上取得进展 被称为并发性。

image.png

线程

线程是操作系统为您提供的一项功能,可让您并行运行程序的各个部分。 假设您的程序由两个主要部分组成,第 1 部分和第 2 部分,并且您编写的代码使得第 1 部分在 线程一 上运行,而第 2 部分在 线程二 上运行。 在这种情况下,程序的两个部分将同时并行运行; 下图说明了它的外观:

image.png

现代软件中真正独立的线程数量与程序需要执行的并发软件数量之间存在差距。 在现代软件中,您可能需要数千个程序同时独立运行,即使您的操作系统可能只提供四个线程!

什么叫并发

并发指在同一时间内可以执行多个任务。并发编程含义比较广泛,包含多线程编程、多进程编程及分布式程序等。

在 Go 中,并发意味着您的程序能够将自身切割成更小的部分,然后能够在不同时间运行不同的独立部分,目标是根据可用资源的数量尽快执行所有任务。

Go 中使用 goroutineschannel 来支持并发。

Goroutines

一个 goroutine 是一个能够与其他函数同时运行的函数。与其他函数同时运行。要创建一个 goroutine 我们使用关键字 go,后面跟着一个函数调用:

package main
import "fmt"
func f(n int) {
    for i := 0; i < 10; i++ {
        fmt.Println(n, ":", i)
    }
}
func main() {
    go f(0)
    var input string
    fmt.Scanln(&input)
}

该程序由两个 goroutine 组成。 第一个 goroutine 是隐式的,是主函数本身。 当我们调用 go f(0) 时会创建第二个 goroutine。 通常,当我们调用一个函数时,我们的程序会执行函数中的所有语句,然后返回到调用后的下一行。 使用  goroutine,我们立即返回到下一行,而不是等待函数完成。 这就是调用 Scanln 函数的原因;  没有它,程序将在有机会打印所有数字之前退出。

Goroutines 是轻量级的,我们可以轻松地创建数以千计的 Goroutines。 我们可以通过这样做来修改我们的程序,以运行 10 个 goroutines:

func main() {
    for i := 0; i < 10; i++ {
        go f(i)
    }
    var input string
    fmt.Scanln(&input)
}

你可能已经注意到,当你运行这个程序时,它似乎是按顺序而不是同时运行 goroutine。 让我们使用 time.Sleeprand.Intn 为函数添加一些延迟:

package main
import (
    "fmt"
    "math/rand"
    "time"
)
func f(n int) {
    for i := 0; i < 10; i++ {
        fmt.Println(n, ":", i)
        amt := time.Duration(rand.Intn(250))
        time.Sleep(time.Millisecond * amt)
    }
}
func main() {
    for i := 0; i < 10; i++ {
        go f(i)
    }
    var input string
    fmt.Scanln(&input)
}

f 打印出从0到10的数字,在每个数字之后等待 0到250毫秒之间。这些程序现在应该同时运行。

Channels

通道为两个 goroutine 提供了一种通信方式,并使它们的执行同步。下面是一个使用通道的示例程序:

该程序将永远打印 “ping”(按回车键停止)。 通道类型用关键字 chan 表示,后跟通道上传递的事物的类型(在这种情况下,我们传递的是字符串)。 <-(左箭头)运算符用于在通道中发送和接收消息。

package main
import (
    "fmt"
    "time"
)
func pinger(c chan string) {
    for i := 0; ; i++ {
        c <- "ping"
    }
}
func printer(c chan string) {
    for {
        msg := <-c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}
func main() {
    var c chan string = make(chan string)
    go pinger(c)
    go printer(c)
    var input string
    fmt.Scanln(&input)
}

c <- "ping" 表示着发送 “ping”。

msg := <- c 表示接收一个消息并把这个消息保存到 msg 中。

fmt 一行中也可以被这样写: fmt.Println(<-c),这样的话 msg := <- c 就可以删掉了。

使用这样的通道可以同步两个 goroutine。 当 pinger 尝试在通道上发送消息时,它将等待 printer 准备好接收消息。 (这被称为阻塞)让我们向程序添加另一个发送者,看看会发生什么。 添加这个功能:

package main
import (
    "fmt"
    "time"
)
func pinger(c chan string) {
    for i := 0; ; i++ {
        c <- "ping"
    }
}
func ponger(c chan string) {
    for i := 0; ; i++ {
        c <- "pong"
    }
}
func printer(c chan string) {
    for {
        msg := <-c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}
func main() {
    var c chan string = make(chan string)
    go pinger(c)
    go ponger(c)
    go printer(c)
    var input string
    fmt.Scanln(&input)
}

该程序现在将轮流打印“ping”和“pong”。

通道方向

我们可以在通道类型上指定一个方向,从而将其限制为发送或接收。 例如 pinger 的函数签名可以改成这样:

func pinger(c chan<- string)

此时,c 只能发送,如果尝试从 c 接收的话会导致编译出错。同样的我们可以更改 printer :

func printer(c <-chan string)

没有这些限制的通道称为双向通道。 可以将双向通道传递给采用仅发送或仅接收通道的函数,但反之则不然。

Select

Go 有一个名为 select 的特殊语句,它的工作方式类似于 switch ,但只适用于通道:

package main
import (
    "fmt"
    "time"
)
func main() {
    c1 := make(chan string)
    c2 := make(chan string)
    go func() {
        for {
            c1 <- "from c1 "
            time.Sleep(2 * time.Second)
        }
    }()
    go func() {
        for {
            c2 <- "from c2 "
            time.Sleep(3 * time.Second)
        }
    }()
    go func() {
        for {
            select {
            case msg1 := <-c1:
                fmt.Println(msg1)
            case msg2 := <-c2:
                fmt.Println(msg2)
            }
        }
    }()
    var input string
    fmt.Scanln(&input)
}

该程序每 2 秒打印一次“from c1”,每 3 秒打印一次“from c2”。 select 选择第一个准备好的通道并从它接收(或发送到它)。 如果多个通道准备就绪,则它会随机选择要接收的通道。 如果没有通道准备好,则语句阻塞,直到一个通道可用。

$ go run main.go
from c2
from c1
from c1
from c2
from c1
from c2
from c1
from c1
from c2
from c1
from c2
from c1
from c1
from c2

select 语句通常用于实现超时:

select {
case msg1 := <- c1:
    fmt.Println("Message 1", msg1)
case msg2 := <- c2:
    fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
    fmt.Println("timeout")
}

time.After 创建一个通道,在给定的持续时间之后将在其上发送当前时间。 (我们对时间不感兴趣,所以我们没有将它存储在变量中)我们还可以指定一个默认情况:

select {
case msg1 := <- c1:
    fmt.Println("Message 1", msg1)
case msg2 := <- c2:
    fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
    fmt.Println("timeout")
    default:
    fmt.Println("nothing ready")
}

默认情况下,如果没有任何一个通道准备好。

缓冲通道

也可以在创建通道时将第二个参数传递给 make 函数:

c := make(chan int, 1)

这将创建一个容量为 1 的缓冲通道。通常通道是同步的; 通道的双方将等待,直到另一方准备就绪。

缓冲通道是异步的; 除非通道已满,否则发送或接收消息不会等待。

 

目录
打赏
0
0
0
0
6
分享
相关文章
|
16天前
|
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
Go 语言入门指南:切片
|
1月前
|
Go语言入门:分支结构
本文介绍了Go语言中的条件语句,包括`if...else`、`if...else if`和`switch`结构,并通过多个练习详细解释了它们的用法。`if...else`用于简单的条件判断;`if...else if`处理多条件分支;`switch`则适用于基于不同值的选择逻辑。特别地,文章还介绍了`fallthrough`关键字,用于优化重复代码。通过实例如判断年龄、奇偶数、公交乘车及成绩等级等,帮助读者更好地理解和应用这些结构。
37 15
Go语言中的并发编程:从入门到精通###
本文深入探讨了Go语言中并发编程的核心概念与实践技巧,旨在帮助读者从理论到实战全面掌握Go的并发机制。不同于传统的技术文章摘要,本部分将通过一系列生动的案例和代码示例,直观展示Go语言如何优雅地处理并发任务,提升程序性能与响应速度。无论你是Go语言初学者还是有一定经验的开发者,都能在本文中找到实用的知识与灵感。 ###
Go语言中的并发编程:从入门到精通
本文将深入探讨Go语言中并发编程的核心概念和实践,包括goroutine、channel以及sync包等。通过实例演示如何利用这些工具实现高效的并发处理,同时避免常见的陷阱和错误。
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
在数字化时代,构建高可靠性服务架构至关重要。本文探讨了如何利用Go语言的高效性、并发支持、简洁性和跨平台性等优势,通过合理设计架构、实现负载均衡、构建容错机制、建立监控体系、优化数据存储及实施服务治理等步骤,打造稳定可靠的服务架构。
95 1
探索Go语言中的并发模式:goroutine与channel
在本文中,我们将深入探讨Go语言中的核心并发特性——goroutine和channel。不同于传统的并发模型,Go语言的并发机制以其简洁性和高效性著称。本文将通过实际代码示例,展示如何利用goroutine实现轻量级的并发执行,以及如何通过channel安全地在goroutine之间传递数据。摘要部分将概述这些概念,并提示读者本文将提供哪些具体的技术洞见。
Go语言的并发特性
【10月更文挑战第26天】Go语言的并发特性
37 1
Go语言:高效并发的编程新星
【10月更文挑战第21】Go语言:高效并发的编程新星
88 7
深入浅出Go语言的并发之道
在本文中,我们将探索Go语言如何优雅地处理并发编程。通过对比传统多线程模型,我们将揭示Go语言独特的goroutine和channel机制是如何简化并发编程,并提高程序的效率和稳定性。本文不涉及复杂的技术术语,而是用通俗易懂的语言,结合生动的比喻,让读者能够轻松理解Go语言并发编程的核心概念。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等