在青训营学习后端开发之余,我也尝试学习一些其他的内容,包括一些并发操作,包括锁机制、同步机制等,在这里分享一下我的见解
1. 什么是并发?并发和并行有什么区别?
这是万年八股题了。。。
- 并发(Concurrency): 并发指的是同时处理多个任务的能力。在并发中,多个任务交替执行,每个任务在一段时间内运行,然后切换到下一个任务。虽然看起来好像同时运行,但实际上是通过快速的切换在多个任务之间进行切换。并发适用于解决多任务、多用户的情况,可以提高系统的响应能力。
- 并行(Parallelism): 并行指的是同时执行多个任务,每个任务在不同的处理器上运行,实现真正的同时执行。并行适用于处理大量数据、复杂计算等需要高度计算能力的场景。
总结起来:并发是单核做分时任务,并行是多核可以同时处理。
2. Goroutine和并发安全
Goroutine是Go语言中的轻量级执行单元,通过使用关键字go可以轻松地创建和启动Goroutine。只需在函数调用前加上go关键字,就可以使该函数在一个新的Goroutine中运行。
数据竞争问题: 数据竞争问题可能导致程序的不稳定性、崩溃和不可预测的结果。例如,如果多个Goroutine同时读写一个变量,就可能导致变量的值不一致或出现意外的情况。
下面是一个数据竞争的示例,启动了多个Goroutine来同时对counter变量进行增加操作。由于没有进行同步控制,多个Goroutine可能会同时修改counter变量,导致数据竞争。
package main
import (
"fmt"
"time"
)
func main() {
var counter int
for i := 0; i < 100; i++ {
go func() {
counter++
}()
}
// 等待所有Goroutine执行完成
time.Sleep(time.Second)
fmt.Println("Counter:", counter)
}
解决方案:加锁
为了解决数据竞争问题,可以使用互斥锁来保护counter变量的访问:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var counter int
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
通过使用sync.Mutex互斥锁,我们确保在任意时刻只有一个Goroutine可以访问和修改counter变量,避免了数据竞争。这样可以保证数据的一致性和正确性。
3.Channel
Channel是Go语言中用于在不同Goroutine之间进行通信和数据传递的管道。它提供了一种安全的方式来传递数据,确保并发执行的多个Goroutine之间的同步和协调。
创建Channel: 可以使用内置的make函数来创建一个Channel。语法如下:
ch := make(chan T)
其中,T表示Channel中传递的数据类型。
通过Channel在Goroutine之间通信: 通过Channel,不同的Goroutine可以安全地传递数据。一个Goroutine可以将数据发送到Channel,另一个Goroutine可以从Channel中接收数据。数据的发送和接收操作会自动进行同步,确保数据的安全传递。
下面是一个简单的示例,展示了如何在两个Goroutine之间通过Channel进行通信:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
ch <- "Hello from Goroutine!"
}()
msg := <-ch
fmt.Println(msg)
}
4. 并发模式
【这个我没怎么用过】 扇出(Fan-out)和扇入(Fan-in)模式以及Worker Pool模式是常见的并发编程模式,它们可以有效地利用goroutine来实现并发任务处理。
-
扇出(Fan-out)模式:
扇出模式将一个任务分发给多个goroutine并行处理。它的工作流程如下:- 一个生产者goroutine产生任务。
- 产生的任务被发送到一个任务通道(channel)中。
- 多个消费者goroutine从任务通道中获取任务,并并行地处理这些任务。
- 处理结果可以通过另一个通道发送回给生产者goroutine或其他处理逻辑进行合并。
扇出模式可以提高任务的并发处理能力,加速任务的执行速度。每个消费者goroutine可以独立地处理任务,充分利用系统资源。
-
扇入(Fan-in)模式:
扇入模式是扇出模式的逆过程,它将多个独立的goroutine的处理结果合并为一个结果。它的工作流程如下:- 多个生产者goroutine并行地处理任务,并将处理结果发送到各自的结果通道中。
- 一个消费者goroutine从多个结果通道中获取处理结果,并将这些结果合并为一个结果。
扇入模式可以将多个并行处理的结果合并为一个结果,适用于需要聚合多个并发处理结果的场景。
-
Worker Pool模式:
Worker Pool模式将一组固定数量的goroutine(工人)用于处理任务队列。它的工作流程如下:- 一个生产者goroutine将任务发送到任务队列中。
- 一组固定数量的工人goroutine从任务队列中获取任务,并处理这些任务。
- 处理结果可以发送回给生产者goroutine或其他处理逻辑。
Worker Pool模式通过限制并发的goroutine数量,可以控制任务的处理速度,避免资源过度消耗。它适用于需要限制并发度的场景,例如限制对外部资源的并发访问。
4.select语句
select语句是Go语言中用于处理多个通道操作的语法结构,它可以实现多路复用和超时等待的功能。通过select语句,可以同时等待多个通道的事件,并在其中任意一个通道就绪时执行相应的操作。
select语句的特性如下:
- 多路复用:select语句可以同时等待多个通道操作,当其中任意一个通道就绪时,对应的case语句将被执行。如果多个通道同时就绪,select会随机选择一个case执行。
- 非阻塞操作:如果没有任何通道操作就绪,select语句会立即执行default语句或阻塞(如果没有default语句)。这可以避免通道的阻塞,使程序能够继续执行其他逻辑。
- 超时等待:通过结合select语句和time包中的定时器,可以实现超时等待功能。可以在select语句中添加一个用于定时的通道操作,当定时器超时时,对应的case语句将被执行,从而实现超时处理。 下面是select语句的基本语法:
select {
case <-channel1:
// 当channel1有数据可读时执行的操作
case data := <-channel2:
// 当channel2有数据可读时执行的操作,可以通过data变量获取读取的数据
case channel3 <- data:
// 当channel3可以写入数据时执行的操作,将data写入channel3
default:
// 当没有任何通道操作就绪时执行的操作
}
使用select语句可以灵活地处理多个通道的事件,并避免由于某个通道阻塞导致整个程序无法继续执行的问题。同时,通过结合定时器,还可以实现超时等待的功能,避免长时间等待导致程序性能下降。