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 的缓冲通道。通常通道是同步的; 通道的双方将等待,直到另一方准备就绪。

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

 

相关文章
|
3月前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
314 4
|
4月前
|
Linux Go iOS开发
Go语言100个实战案例-进阶与部署篇:使用Go打包生成可执行文件
本文详解Go语言打包与跨平台编译技巧,涵盖`go build`命令、多平台构建、二进制优化及资源嵌入(embed),助你将项目编译为无依赖的独立可执行文件,轻松实现高效分发与部署。
|
3月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟蒋星熠Jaxonic,Go语言探索者。深耕云计算、微服务与并发编程,以代码为笔,在二进制星河中书写极客诗篇。分享Go核心原理、性能优化与实战架构,助力开发者掌握云原生时代利器。#Go语言 #并发编程 #性能优化
493 43
Go语言深度解析:从入门到精通的完整指南
|
4月前
|
Cloud Native 安全 Java
Go语言深度解析:从入门到精通的完整指南
🌟 蒋星熠Jaxonic,执着的星际旅人,用Go语言编写代码诗篇。🚀 Go语言以简洁、高效、并发为核心,助力云计算与微服务革新。📚 本文详解Go语法、并发模型、性能优化与实战案例,助你掌握现代编程精髓。🌌 从goroutine到channel,从内存优化到高并发架构,全面解析Go的强大力量。🔧 实战构建高性能Web服务,展现Go在云原生时代的无限可能。✨ 附技术对比、最佳实践与生态全景,带你踏上Go语言的星辰征途。#Go语言 #并发编程 #云原生 #性能优化
|
6月前
|
JSON 中间件 Go
Go语言实战指南 —— Go中的反射机制:reflect 包使用
Go语言中的反射机制通过`reflect`包实现,允许程序在运行时动态检查变量类型、获取或设置值、调用方法等。它适用于初中级开发者深入理解Go的动态能力,帮助构建通用工具、中间件和ORM系统等。
340 63
|
5月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
5月前
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。
|
7月前
|
数据可视化 测试技术 Go
Go 语言测试与调试:`go test` 工具用法
`go test` 是 Go 语言内置的测试工具,支持单元测试、基准测试、示例测试等功能。本文详解其常用参数、调试技巧及性能测试命令,并提供实际项目中的应用示例与最佳实践。
|
Java 编译器 Go
一起学Golang系列(五)初次接触Go语言可能遇到的各种坑!
前面介绍了Go语言的基础语法,所谓磨刀不误砍柴工,希望大家还是能熟悉掌握这些基础知识,这样后面真正学起Go来才会得心应手。 作为初学者。Go语言的语法有些和java类似,但也有很多不一样的地方。刚开始都会遇到各种各样的坑。下面就来总结下学习go语言的过程中,遇到的各种坑。
一起学Golang系列(五)初次接触Go语言可能遇到的各种坑!
|
3月前
|
存储 安全 Java
【Golang】(4)Go里面的指针如何?函数与方法怎么不一样?带你了解Go不同于其他高级语言的语法
结构体可以存储一组不同类型的数据,是一种符合类型。Go抛弃了类与继承,同时也抛弃了构造方法,刻意弱化了面向对象的功能,Go并非是一个传统OOP的语言,但是Go依旧有着OOP的影子,通过结构体和方法也可以模拟出一个类。
259 1