并发与并行
并发(Concurrency)和并行(Parallelism)是并发编程中两个重要的概念,它们有着明显的区别:
- 并发指的是多个任务在同一时间段内交替进行,通过任务间的切换来实现多个任务的执行。在并发中,任务之间可以是相互独立的,彼此不需要同时执行,只需在一定时间内交替执行。
- 并行指的是多个任务同时执行,每个任务在不同的处理器上运行,并同时进行。在并行中,任务之间必须是同时进行的,通过利用多核处理器或分布式系统的能力来实现任务的并行执行。
并发编程的基本概念
-
线程(Thread):线程是操作系统调度的最小执行单位,一个进程可以包含多个线程。多个线程可以在同一进程内共享相同的内存空间,因此可以方便地共享数据和通信。
-
进程(Process):进程是正在运行的程序实例,每个进程都有独立的内存空间和系统资源。不同进程之间的数据共享和通信需要通过进程间通信(IPC)机制来实现。
-
协程(Coroutine):协程是一种用户级的轻量级线程,由编程语言或库提供支持。协程可以在单个线程内部实现并发,通过协程的切换来实现任务的交替执行。
-
同步(Synchronization):同步是一种机制,用于协调多个线程或进程的执行顺序和访问共享资源的顺序。常见的同步机制包括互斥锁、条件变量、信号量和屏障等。
-
互斥(Mutex):互斥是一种同步机制,用于保护共享资源的访问。只有一个线程或进程可以持有互斥锁,其他线程或进程必须等待互斥锁释放后才能访问共享资源。
共享资源和竞态条件是并发编程中重要的概念:
-
共享资源是多个线程或进程需要访问和共享的数据、变量或资源。多个线程或进程可以同时读取共享资源,但在至少一个线程或进程进行写操作时,需要确保其他线程或进程不会同时进行读或写操作。
-
竞态条件(Race Condition)是指多个线程或进程同时访问共享资源,并且最终的结果依赖于执行的顺序和时间。当多个线程或进程对共享资源进行读写操作时,如果没有合适的同步机制保护,可能导致意外的结果或错误。
为了避免竞态条件,需要使用适当的同步机制,如互斥锁、条件变量或原子操作等。通过对共享资源进行合理的同步和互斥,可以保证线程或进程之间的正确交互,避免数据的不一致性和意外的结果。
同时,需要注意在并发编程中正确地处理同步和互斥的逻辑,避免死锁(Deadlock)和活锁(Livelock)等问题,以确保程序的正确性和性能。
Go语言并发编程
两个关键概念是协程(Goroutine)和通道(Channel):
-
协程(Goroutine):
- 协程是轻量级的并发执行单元,由Go语言运行时(Goroutine Scheduler)进行调度。
- 通过关键字
go可以创建一个新的协程,并在函数调用前加上go关键字即可。 - 协程之间的切换开销非常小,可以高效地创建大量的协程。
- 协程之间通过共享内存来进行通信,但需要借助同步原语来保证数据访问的安全性。
-
通道(Channel):
- 通道是用于协程之间通信和同步的机制,可以在不同协程之间传递数据。
- 通道可以是带有特定类型的数据传输管道,通过
make函数创建。 - 通过通道的发送和接收操作实现协程之间的数据传递,发送和接收操作会阻塞协程的执行,直到对应的接收或发送操作可以进行。
Go语言还提供了一些并发原语和常用技术来辅助并发编程:
-
WaitGroup:
- WaitGroup用于等待一组协程的完成,可以阻塞主协程,直到指定数量的协程执行完毕。
- 通过Add方法增加等待的协程数量,通过Done方法减少协程数量,通过Wait方法进行阻塞等待。
-
Mutex:
- Mutex是一种互斥锁,用于保护共享资源的访问,避免竞态条件。
- 通过Lock方法获取锁,通过Unlock方法释放锁,一次只能有一个协程持有该锁。
-
Atomic:
- Atomic提供了原子操作的支持,用于对共享变量进行原子读写和更新操作。
- 原子操作是不可中断的单一操作,可以在没有锁的情况下进行并发安全的操作。
除了上述原语和技术之外,Go语言还提供了其他并发相关的工具和库,如Once、RWMutex、Ticker、Timer等,可以根据具体需求选择合适的工具来实现并发编程。
通过使用协程和通道以及其他并发原语和技术,Go语言使得并发编程更加简洁和高效。开发者可以通过并发编程在Go语言中轻松处理并发任务,并保证数据访问的安全性和协程间的通信与同步。
代码详细学习
启动单个goroutine
func hello() {
fmt.Println("hello goroutine")
}
func TestGoRoutine4(t *testing.T) {
go hello()
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}
运行结果
启动多个goroutine
var wg sync.WaitGroup // 等待组
func sayHello(i int) {
defer wg.Done() // goroutine执行完毕后,将等待组中的数量减1
fmt.Println("hello goroutine", i)
}
func TestGoRoutine5(t *testing.T) {
for i := 0; i < 10; i++ {
wg.Add(1) // 添加一个goroutine, 等待组中的数量加1
go sayHello(i)
}
wg.Wait() // 等待所有goroutine执行完毕
fmt.Println("main goroutine done!")
}
sync.WaitGroup类型在sync包中定义,提供了一些用于等待一组协程完成的方法。下面是WaitGroup的详细 API 说明:
-
func (wg *WaitGroup) Add(delta int)- 功能:增加等待的协程数量
- 参数:
delta,要增加的数量,可以为负数 - 说明:每个
Add()调用会增加等待的协程数量,通常在启动协程之前调用
-
func (wg *WaitGroup) Done()- 功能:标记一个协程完成
- 说明:每个协程完成时,需要调用
Done()方法进行标记,相当于减少等待的协程数量
-
func (wg *WaitGroup) Wait()- 功能:阻塞等待所有协程完成
- 说明:调用
Wait()方法会阻塞当前协程,直到等待的协程数量变为零,即所有协程都完成
WaitGroup通过上述方法提供了对等待协程的操作,可以使用它们来实现对一组协程的同步等待。
一般的使用流程是:
- 在主协程中创建一个
WaitGroup实例。 - 在启动协程之前,使用
Add()方法增加等待的协程数量。 - 在每个协程的最后,使用
Done()方法标记协程完成。 - 在主协程中调用
Wait()方法,阻塞等待所有协程完成。
通过WaitGroup,可以轻松实现等待一组协程完成的逻辑,以确保在协程完成后再执行后续的操作。
channel
协程之间的通信是并发编程中非常重要的一部分,Go语言通过通道(Channel)提供了一种方便、安全的协程间通信机制。
通道是一种类型化的数据结构,可以在协程之间传递数据。通过通道,协程可以发送和接收数据,实现了数据的同步和共享。
下面是使用通道进行协程间通信的基本操作:
- 创建通道:
make(chan 元素类型, [缓冲大小])
-
发送数据到通道:
ch <- datadata是要发送到通道的数据。 -
从通道接收数据:
data := <-ch将通道中的数据接收到
data变量中。 -
关闭通道:
close(ch)通道可以被显式关闭,以表示没有更多的数据将被发送到通道中。关闭通道后,仍然可以从通道接收剩余的数据。
协程通过发送和接收操作来实现通信,这些操作会在必要时阻塞协程的执行,直到对应的接收或发送操作可以进行。这种阻塞和同步的机制可以有效地协调协程之间的执行。
// 协程之间的通信 : 通过channel,channel是goroutine之间的通信方式
func TestGoRoutine2(t *testing.T) {
ch := make(chan int)
for i := 0; i < 5; i++ {
go func(j int) {
ch <- j
}(i)
}
for i := 0; i < 5; i++ {
fmt.Println(<-ch)
}
}
// 解决同步问题
func TestGoRoutine3(t *testing.T) {
ch := make(chan int)
for i := 0; i < 5; i++ {
go func(j int) {
ch <- j
}(i)
}
for i := 0; i < 5; i++ {
fmt.Println(<-ch) // 一直阻塞,直到有数据
}
}
func TestCalSquare(t *testing.T) {
src := make(chan int)
dest := make(chan int, 3)
go func() {
// 关闭channel,关闭后不能再写入数据,但是可以读取数据,在for range中读取数据时,当channel中没有数据时,会自动退出循环
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
fmt.Println(i)
}
}