Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go语言并发实现

Go语言并发实现完整代码示例

作者:星星懒得动

并发是Go语言最核心、最亮眼的特性,Go从语言层面原生支持并发,不像Java/Python等语言需要依赖线程库、协程框架,这篇文章主要介绍了Go语言并发实现的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

并发/并行

并发:多线程程序在单核心的 cpu 上运行,称为并发;

并行:多线程程序在多核心的 cpu 上运行,称为并行。

并发与并行并不相同,并发主要由切换时间片来实现“同时”运行,并行则是直接利用多核实现多线程的运行,Go程序可以设置使用核心数,以发挥多核计算机的能力。

简单比喻:

并发像一个人同时煮饭、洗衣服和打电话,通过任务切换来完成多项工作

并行像三个人分别煮饭、洗衣服和打电话,真正同时进行

go语言:只需要简单几行,可以轻松写出一堆并发函数

实现

go routines

下面是几个小例子,方便理解异步

示例一

package main

import "fmt"

func someFunc(name string){
	fmt.Println(name)
}

func main(){
	someFunc("Lucy")
	fmt.Println("hello")
}

运行结果:

在main函数中调用someFunc函数,主函数会被卡住,直到这个函数执行完毕才会继续,所以会先打印Lucy,然后打印hello

示例二

使用go routine可以使主函数不用等这个函数执行就能结束,主线程可以将任务分叉出去

package main

import "fmt"

func someFunc(name string){
	fmt.Println(name)
}

func main(){
	go someFunc("Lucy")
	go someFunc("Lili")
	fmt.Println("hello")
}

运行结果

然后会发现上面的结果都没有打印,只打印hello。这是因为主进程开了一些子进程,子进程会自己跑起来,执行任务,而主进程把任务派发出去立马会忙自己的事。

示例三

可以给他加个延deng

package main

import "fmt"
import "time"

func someFunc(name string){
	fmt.Println(name)
}

func main(){
	go someFunc("Lucy")
	go someFunc("Lili")
	fmt.Println("hello")
	time.Sleep(time.Second * 2)
}

运行结果

可以看到someFunc的打印,但是多运行几次,可以看到,结果是随机的,因为他们都是异步执行的

小结:go 语言采用的是fork join并发模型,下图能展示他的工作原理,展示了主函数的生命周期

fork 指的是在程序中的任意地方可以将执行的子分支与父节点同时运行
join指的是,在未来的某时刻,这些并发的分支可以合并在一起

channels

简单来说,channel 就像是 goroutine 之间的管道消息队列,让不同的并发执行单元能够安全地交换数据。

channel的主要用途:

创建channel

使用make函数创建,可以指定channel中传递的数据类型以及可选的缓冲区大小。

ch := make(chan int)    // 无缓冲channel
chBuffered := make(chan int, 10) // 有缓冲channel,缓冲区大小为10

发送数据:

使用<-操作符,将数据发送到channel。

ch &lt;- 42   // 将42发送到channel ch

接收数据:

使用<-操作符,从channel中接收数据。

value := &lt;-ch   // 从channel ch接收数据并赋值给value

关闭channel:

使用close函数。关闭后,不能再向channel发送数据,但可以继续接收直到数据被取完。

close(ch)

Channel 关闭的本质变化

关闭 Channel 的效果:

操作关闭前关闭后
发送数据 ch <- data正常(缓冲区未满时)panic
接收数据 <-ch阻塞(无数据时)立即返回零值 + ok=false
再次关闭 close(ch)-panic
range 遍历 for v := range ch正常遍历自动退出循环

关闭 Channel 后的接收行为总结

Channel 状态接收操作 value, ok := <-ch说明
关闭且有数据value=数据, ok=true能正常取出剩余数据
关闭且无数据value=零值, ok=false立即返回零值
未关闭且有数据value=数据, ok=true正常接收
未关闭且无数据阻塞等待直到有数据或 channel 关闭

总结

这种设计让 Go 的 channel 能够优雅地实现"数据流结束"的信号传递!

最佳实践

记住:向已关闭的 channel 发送数据是 Go 中的经典错误,一定要避免!

通道分类

无缓冲channel与有缓冲channel:

无缓冲 Channel有缓冲 Channel
同步性强同步弱同步
阻塞时机发送/接收必须配对缓冲区满/空时才阻塞
使用场景精确的goroutine同步生产消费解耦、流量控制
类比电话通话邮件信箱

select

在Go语言中,select语句用于在多个通道操作(发送或接收)之间进行选择。它会一直等待,直到其中一个通道操作可以执行,然后执行该操作。如果多个通道操作都可以执行,它会随机选择一个执行。

select语句是Go语言中处理并发编程的重要工具,它允许goroutine同时等待多个通道操作,从而可以实现多路复用。

基本语法

select {
case sendOrReceive1:
    // 执行相应的操作
case sendOrReceive2:
    // 执行相应的操作
default:
    // 如果以上都没有准备就绪,则执行default
}

特点

使用场景

package main

import "fmt"

func main(){

	mychannel := make(chan string)
	anotherChannel := make(chan string)

	go func() {
		mychannel &lt;- "data"  //发送数据到channel
	}()

	go func() {
		anotherChannel &lt;- "data 2"  //发送数据到channel
	}()

	// 主goroutine 从channel接收数据
	select {
	case msgFromMychannel := &lt;- mychannel:
		fmt.Println(msgFromMychannel)
	case msgFromAnotherChannel := &lt;- anotherChannel:
		fmt.Println(msgFromAnotherChannel)

	}
	
}

超时控制

func main() {
    ch := make(chan string)
    
    go func() {
        time.Sleep(3 * time.Second)  // 模拟耗时操作
        ch &lt;- "操作完成"
    }()
    
    select {
    case result := &lt;-ch:
        fmt.Println("成功:", result)
    case &lt;-time.After(2 * time.Second):  // 2秒超时
        fmt.Println("操作超时!")
    }
}

运行结果

for-select loop

示例一

package main

import "fmt"

func main(){
	charchannel := make(chan string, 3) //带缓冲
	chars := []string{"a","b","c"}

	for _,s := range chars{
		select{
		case charchannel &lt;-s:
		}
	}
	close(charchannel) //关闭通道之后也能读

	for result := range charchannel {
		fmt.Println(result)
	}
}

运行结果

示例二:无限循环协程

package main

import "fmt"
import "time"

func someFunc(){
	for{
		select{
		default:
			fmt.Println("ongoing")
		}
	}
}

func main() {
	go someFunc()
	time.Sleep(time.Second * 10)

}

运行结果:无线循环打印,但是因为主goroutine 10s退出了,所以终止了:

done channel

示例三

在someFunc中,有一个无限循环,循环中使用select监听两个case:一个是从done中接收数据,另一个是default。 

select语句会非阻塞地检查各个case。如果done通道有数据可读(或者被关闭),那么case <- done这个分支就会执行,然后return退出循环

package main

import "fmt"
import "time"

func someFunc(done &lt;- chan bool){ //将done传递给someFunc
	for{
		select{//非阻塞检查各个case
		case &lt;- done:
			return
		default:
			fmt.Println("ongoing")
		}
	}
}

func main() {
	done := make(chan bool)
	go someFunc(done)
	time.Sleep(time.Second * 3)
	close(done) //关闭done通道

}

示例四

package main
import "fmt"
import "time"

func doWork(done chan bool) {
    fmt.Println("Working...")
    time.Sleep(2 * time.Second)
    fmt.Println("Done!")
    done &lt;- true // 发送完成信号
}

func main() {
    done := make(chan bool)
    go doWork(done)
    &lt;-done // 等待完成信号(阻塞直到收到数据)
    fmt.Println("Work completed!")
}

运行结果: 打印Working之后sleep 2s 打印Done

pipelines

Pipeline(管道) 是 Go 并发编程中的一种重要模式,它通过连接多个阶段的 channel 操作来处理数据流,每个阶段都是一组执行相同函数的 goroutine。

Pipeline 的核心概念

Pipeline 就像工厂的流水线,数据像零件一样在不同工位(阶段)间传递和处理

数据源 → 阶段1 → 阶段2 → ... → 阶段N → 结果

Go Pipeline 的核心价值:

Pipeline 模式让并发编程变得:

总结 

到此这篇关于Go语言并发实现的文章就介绍到这了,更多相关Go语言并发实现内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文