Go语言入门:深入了解Go语言|青训营

语言进阶

并发VS并行

并发和并行是计算机领域中两个重要的概念,它们描述了程序执行的方式和效果。

并发(Concurrency)指的是同时处理多个任务的能力。在并发模型中,任务之间可以交替执行,每个任务都有可能在任意时刻被中断或暂停,然后切换到另一个任务。并发的关键在于任务的调度和切换,通过合理的调度算法,可以让多个任务看起来是同时在执行,提高系统的吞吐量和响应性。并发常用于处理I/O密集型的任务,例如网络请求、文件读写等。

并行(Parallelism)指的是同时执行多个任务的能力。在并行模型中,任务真正地同时执行在多个处理器或多核系统上,每个任务都有独立的执行流。并行的关键在于硬件资源的并行处理能力,通过利用多个处理器或多核系统,可以实现真正的并行计算。并行常用于处理计算密集型的任务,例如图像处理、科学计算等。

Go语言是一种开源的编程语言,它具有并发编程的内置支持,可以轻松地编写并发和并行程序。Go语言的并发模型基于Goroutine和Channel。

Go语言的并发模型具有以下优势:

  1. 简单易用:Go语言提供了简洁的并发编程模型,Goroutine和Channel的使用非常简单,开发者可以轻松地编写并发程序。

  2. 高效性能:Go语言的Goroutine是轻量级的线程,创建和销毁的开销很小,可以高效地支持大量的并发执行。此外,Go语言的调度器具有自动抢占式调度特性,可以在多个Goroutine之间自动切换,提高程序的并发性能。

  3. 安全性:Go语言通过Channel提供了一种安全的数据传递和同步机制,避免了并发访问共享数据时的竞态条件和死锁问题。开发者可以使用Channel来实现Goroutine之间的协作和同步,编写出更加健壮和安全的并发程序。

  4. 扩展性:Go语言的并发模型可以很好地利用多核处理器和多核系统的并行计算能力。通过并行执行多个Goroutine,可以充分利用硬件资源,提高程序的处理能力和响应速度。

Go语言通过Goroutine和Channel提供了简单、高效、安全和可扩展的并发编程模型,使开发者能够轻松地编写出高性能的并发和并行程序。这使得Go语言在处理并发任务和构建高性能服务器等领域具有显著的优势。

Goroutine

在传统的操作系统中,线程(Thread)是执行程序的最小单位。每个线程都有自己的执行环境(寄存器、栈等),并且由操作系统进行调度和管理。线程之间的切换需要操作系统的介入,这会引入一定的开销。

与线程相比,协程(Coroutine)是一种更加轻量级的执行单位。协程是由程序员控制的,可以在代码层面进行调度和切换。协程的切换不需要操作系统的介入,因此开销更小。

Goroutine是Go语言中的协程实现,它是一种轻量级的线程。与传统的线程相比,Goroutine具有以下特点:

  1. 轻量级:Goroutine的创建和销毁的开销非常小,可以高效地创建大量的Goroutine。相比之下,传统线程的创建和销毁开销较大。

  2. 高效调度:Go语言的运行时调度器(Scheduler)负责管理和调度Goroutine的执行。调度器使用了一种称为M:N调度的策略,即将M个Goroutine调度到N个操作系统线程上执行。这种调度方式可以在少量的操作系统线程上高效地调度大量的Goroutine。

  3. 并发性:Goroutine可以并发地执行,多个Goroutine之间可以同时运行。在多核处理器上,多个Goroutine可以真正地并行执行,充分利用硬件资源。与传统的线程相比,Goroutine的并发性能更好。

  4. 通信和同步:Goroutine之间通过Channel进行通信和同步。Channel提供了一种安全的数据传递和共享状态的机制,避免了竞态条件和死锁等并发编程中常见的问题。

通过使用Goroutine,开发者可以轻松地编写出高效、安全和可扩展的并发程序。Goroutine的设计使得并发编程变得更加简单,开发者可以将复杂的并发逻辑拆分成多个独立的Goroutine,并通过Channel进行协作和通信,实现高效的并发处理。

注意,Goroutine并不是真正意义上的线程,它是由Go运行时环境管理的一种抽象概念。Go语言的运行时环境负责调度和管理Goroutine的执行,开发者无需关注底层的线程管理和调度细节,可以专注于编写并发逻辑。

CSP(Communicating Sequential Processes)

CSP(Communicating Sequential Processes)是一种并发编程模型,它在Go语言中得到了广泛应用。CSP模型由计算机科学家Tony Hoare在1978年提出,它强调通过通信来进行并发处理,而不是共享内存。Go语言中的CSP模型主要通过GoroutineChannel来实现。

在CSP模型中,系统中的并发处理单元被抽象为顺序执行的进程(Process),每个进程都有自己的执行流程。不同进程之间通过Channel进行通信,Channel是一种类型安全的、阻塞的、同步的数据结构。进程可以通过Channel发送数据给其他进程,也可以通过Channel接收其他进程发送的数据。通过Channel的发送和接收操作,进程之间可以进行数据的传递和同步。

在Go语言中,Goroutine是轻量级的执行单元,可以看作是一种特殊的进程。每个Goroutine都有自己的执行流程,可以独立运行。Goroutine之间通过Channel进行通信和同步,实现数据的传递和共享状态的同步。

在Go语言中,可以使用内置的make函数创建一个Channel,例如:

go 复制代码
ch := make(chan int)

上述代码创建了一个传递整数类型数据的Channel。通过Channel的发送操作和接收操作,可以在不同的Goroutine之间进行数据传递。发送操作使用<-运算符,接收操作使用<-运算符,例如:

go 复制代码
ch <- 10 // 发送数据到Channel
x := <-ch // 从Channel接收数据

在CSP模型中,Channel的发送和接收操作都是阻塞的。当发送操作执行时,如果Channel已满(缓冲区已满),发送操作将阻塞,直到有其他GoroutineChannel中接收数据。当接收操作执行时,如果Channel为空(缓冲区为空),接收操作将阻塞,直到有其他GoroutineChannel发送数据。

通过使用GoroutineChannel,可以实现并发程序中的通信和同步。开发者可以将复杂的并发逻辑拆分成多个独立的Goroutine,通过Channel进行数据传递和共享状态的同步,实现高效、安全和可扩展的并发处理。

CSP模型的优点在于明确的通信和同步机制,避免了共享内存带来的竞态条件和死锁问题。通过使用Channel进行通信,不同的Goroutine之间可以实现松耦合的协作,提高程序的可读性和可维护性。

Go语言中的CSP模型通过GoroutineChannel提供了一种简单、高效、安全的并发编程模型。开发者可以利用CSP模型编写出高性能、可靠的并发程序,充分发挥多核处理器和多核系统的并行计算能力。

Channel

当谈到Go语言中的并发编程和CSP模型时,Channel是其中最重要的概念之一。Channel是一种类型安全的、阻塞的、同步的数据结构,用于在Goroutine之间进行通信和同步。它是实现Go语言中并发编程的核心机制之一,使得不同的Goroutine可以安全地交换数据,实现数据传递和共享状态的同步。

在Go语言中,可以使用内置的make函数来创建一个Channel,语法如下:

go 复制代码
ch := make(chan T)

这里的T表示Channel中传递的数据的类型。例如,如果要创建一个传递整数类型数据的Channel,可以这样做:

go 复制代码
ch := make(chan int)

Channel支持两种基本操作:发送(send)和接收(receive)。发送操作用于将数据发送到Channel中,接收操作用于从Channel中接收数据。发送操作和接收操作都使用箭头符号(<-)。

发送操作:

go 复制代码
ch <- data // 将数据data发送到Channel ch

接收操作:

go 复制代码
result := <-ch // 从Channel ch接收数据,并将数据赋值给result变量

需要注意的是,发送操作和接收操作都是阻塞的。当发送操作执行时,如果Channel已满(缓冲区已满),发送操作将阻塞,直到有其他GoroutineChannel中接收数据。当接收操作执行时,如果Channel为空(缓冲区为空),接收操作将阻塞,直到有其他GoroutineChannel发送数据。

在默认情况下,Channel是无缓冲的,这意味着发送操作和接收操作将会进行同步,发送操作会等待接收者接收数据,直到数据被接收,发送操作才会完成。类似地,接收操作会等待发送者发送数据,直到数据被发送,接收操作才会完成。

此外,还可以创建带有缓冲区的Channel,通过在make函数中传递第二个参数来指定缓冲区的大小,例如:

go 复制代码
ch := make(chan int, 10) // 创建一个带有缓冲区大小为10的整数类型Channel

带有缓冲区的Channel允许在缓冲区未满时进行发送操作,或者在缓冲区非空时进行接收操作,只有在缓冲区已满时的发送操作和在缓冲区为空时的接收操作才会阻塞。

Channel的关闭操作用于通知接收者Channel已经没有更多的数据发送。关闭Channel后,接收者仍然可以继续从Channel中接收已经发送的数据,直到所有数据都被接收完毕。关闭Channel后,再进行发送操作会导致panic。

关闭Channel:

go 复制代码
close(ch)

Channel的关闭是一种广播机制,即所有的接收者都能够收到关闭的通知。因此,关闭一个已经关闭的Channel会导致panic。

Channel在Go语言中是并发安全的,多个Goroutine可以同时对一个Channel进行发送和接收操作而不会造成数据竞态(data race)等并发问题。这使得Channel成为一种非常有用的数据传递和共享状态的机制。

Channel是一种强大的并发编程工具,它通过提供阻塞的、同步的数据传递机制,实现了Goroutine之间的通信和同步。Channel的使用使得并发编程变得简单而安全,有助于开发高效、可靠的并发程序。

并发安全Lock

在Go语言中,为了保证多个Goroutine并发访问共享资源时的数据安全性,我们可以使用并发安全的锁机制。锁是一种同步原语,它允许多个Goroutine按顺序访问共享资源,防止多个Goroutine同时修改数据而导致的竞态条件(race condition)和数据不一致性问题。

Go语言中提供了两种常用的锁:互斥锁(Mutex)和读写锁(RWMutex)。

  1. 互斥锁(Mutex): 互斥锁是最简单的锁类型,它在同一时刻只允许一个Goroutine访问共享资源。当一个Goroutine获得了互斥锁的锁定(Lock)后,其他尝试获得锁的Goroutine将被阻塞,直到持有锁的Goroutine释放锁(Unlock)为止。

互斥锁的使用示例:

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var counter int
var mutex sync.Mutex

func increment() {
	mutex.Lock()
	counter++
	mutex.Unlock()
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	}
	wg.Wait()
	fmt.Println("Counter:", counter)
}

在上述示例中,我们使用互斥锁(sync.Mutex)来保护共享变量counter的访问。通过mutex.Lock()mutex.Unlock()来分别锁定和解锁互斥锁。这样,多个Goroutine并发执行increment函数时,只有一个Goroutine可以访问counter,其他Goroutine会被阻塞,直到锁被释放。

  1. 读写锁(RWMutex): 读写锁是互斥锁的一种改进,它允许多个Goroutine同时读取共享资源,但在写入时仍然需要互斥。这种情况下,多个Goroutine可以同时获取读锁,但只有当没有任何读锁或写锁时,才能获取写锁。

读写锁的使用示例:

go 复制代码
package main

import (
	"fmt"
	"sync"
)

var counter int
var rwLock sync.RWMutex

func read() {
	rwLock.RLock()
	fmt.Println("Counter:", counter)
	rwLock.RUnlock()
}

func write() {
	rwLock.Lock()
	counter++
	rwLock.Unlock()
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			read()
		}()
	}

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			write()
		}()
	}

	wg.Wait()
}

在上面的代码中,我们使用读写锁(sync.RWMutex)来保护共享变量counter的读写访问。通过rwLock.RLock()rwLock.RUnlock()来分别获取和释放读锁,通过rwLock.Lock()rwLock.Unlock()来分别获取和释放写锁。

无锁和有锁的区别和最终影响:

  1. 无锁: 在没有锁的情况下,多个Goroutine可以同时访问共享资源。这种情况下,由于没有互斥机制,可能会发生竞态条件(race condition),导致数据不一致性和错误结果。

  2. 有锁: 使用锁可以保证共享资源的安全访问,防止多个Goroutine同时修改共享资源。在使用锁的情况下,多个Goroutine会按顺序获取锁,并且只有持有锁的Goroutine可以修改共享资源,其他Goroutine会被阻塞。这样可以避免竞态条件,确保数据的一致性和正确性。

需要注意的是,过度使用锁可能会导致性能下降,因为锁会限制并发性 。因此,在设计并发程序时,需要权衡使用锁的频率和范围,以提高程序的并发性能。有时候可以使用更细粒度的锁来减少锁的竞争,或者使用无锁的数据结构来避免锁的开销。在实际应用中,对于不同的场景,选择合适的锁策略是一个需要仔细考虑的问题。

WaitGroup

在Go语言中,sync.WaitGroup是一种用于等待一组Goroutine完成执行的同步原语。它提供了一个简单的方法来等待多个Goroutine的结束,以便在它们全部完成后再继续执行主线程或其他操作。WaitGroup内部维护一个计数器,可以通过调用Add方法来增加计数器的值,通过调用Done方法来减少计数器的值,通过调用Wait方法来阻塞直到计数器的值为零。

WaitGroup的常用方法有三个:

  1. Add(delta int): 增加计数器的值,delta可以为正数或负数。在启动一个新的Goroutine之前,通过调用Add(1)来增加计数器的值。当Goroutine完成时,通过调用Add(-1)来减少计数器的值。

  2. Done(): 减少计数器的值,相当于Add(-1)

  3. Wait(): 阻塞调用该方法的Goroutine,直到计数器的值变为零。一般在主线程或其他Goroutine中调用Wait()方法来等待所有增加的计数器都被减少到零,从而确保所有的Goroutine都已经完成执行。

下面通过一段简单的代码示例来演示sync.WaitGroup的使用:

go 复制代码
package main

import (
	"fmt"
	"sync"
	"time"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Second)
	fmt.Printf("Worker %d finished\n", id)
}

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go worker(i, &wg)
	}

	wg.Wait()
	fmt.Println("All workers have finished")
}

在上述示例中,我们定义了一个worker函数来模拟一个需要执行一段时间的任务。在main函数中,我们启动了5个Goroutine来执行这个任务,并使用sync.WaitGroup来等待所有的Goroutine完成。在每个Goroutine启动前,我们通过wg.Add(1)来增加计数器的值,表示有一个Goroutine需要等待。在worker函数中,通过defer wg.Done()来在Goroutine执行完成后减少计数器的值。最后,我们在主线程中调用wg.Wait()来等待所有的Goroutine执行完成。

运行上述代码,你会看到类似如下的输出:

css 复制代码
Worker 2 started
Worker 4 started
Worker 3 started
Worker 5 started
Worker 1 started
Worker 1 finished
Worker 5 finished
Worker 2 finished
Worker 3 finished
Worker 4 finished
All workers have finished

可以看到,所有的Goroutine都顺利执行完成,并且在所有Goroutine执行完成后,主线程继续执行,打印了"All workers have finished"的信息。

使用sync.WaitGroup,我们可以在并发程序中很方便地等待多个Goroutine完成执行,确保在它们全部完成后再继续执行后续操作。这在需要等待多个Goroutine完成后进行结果汇总或其他后续处理的情况下非常有用。

相关推荐
Find3 个月前
MaxKB 集成langchain + Vue + PostgreSQL 的 本地大模型+本地知识库 构建私有大模型 | MarsCode AI刷题
青训营笔记
理tan王子3 个月前
伴学笔记 AI刷题 14.数组元素之和最小化 | 豆包MarsCode AI刷题
青训营笔记
理tan王子3 个月前
伴学笔记 AI刷题 25.DNA序列编辑距离 | 豆包MarsCode AI刷题
青训营笔记
理tan王子3 个月前
伴学笔记 AI刷题 9.超市里的货物架调整 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵3 个月前
分而治之,主题分片Partition | 豆包MarsCode AI刷题
青训营笔记
三六3 个月前
刷题漫漫路(二)| 豆包MarsCode AI刷题
青训营笔记
tabzzz3 个月前
突破Zustand的局限性:与React ContentAPI搭配使用
前端·青训营笔记
Serendipity5653 个月前
Go 语言入门指南——单元测试 | 豆包MarsCode AI刷题;
青训营笔记
wml3 个月前
前端实践-使用React实现简单代办事项列表 | 豆包MarsCode AI刷题
青训营笔记
用户44710308932423 个月前
详解前端框架中的设计模式 | 豆包MarsCode AI刷题
青训营笔记