1.go语言的基础语法
Go语言在语言层面就实现了并发,下面我们来探究一下,了解一下这个GO语言的基础语法和常用特性的解析,并给出我的一些建议。
1.1进程和线程的关系
进程/线程
进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
线程是进程的一个执行实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
一个进程可以包含多个线程,这些线程共享进程的资源。因此,一个进程可以被看作是一个或多个线程的容器。
1.2 并发和并行
并发是指多个任务在同一时间段内执行,但不一定是同时执行的。这些任务在时间上交替执行,通过时间片轮转等机制来实现看似同时进行的效果。
并行是指多个任务在同一时刻真正同时执行,每个任务都在独立的处理单元上执行。
1.3 线程和协程
协程是一种更轻量级的并发执行单位,与线程不同,它不依赖于操作系统的线程或进程。协程的管理是由程序员控制的。协程通常在同一个线程内执行,它们使用协作式多任务处理方式,即一个协程主动让出执行权给另一个协程,而不是被操作系统的调度器强制切换。
协程就是程序员只需要定义很多个任务,让系统去帮助我们把这些任务分配到CPU上实现并发执行的一个机制。
goroutine是由Go的运行时(runtime)调度和管理的。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。
2go语言的常用特性
2goroutine
goroutine的基本语法如下:
go 函数名( 参数列表 )
下面是一个简单使用的例子
// 启动一个协程
go func() {
fmt.Println("Goroutine started.")
time.Sleep(time.Second) // 模拟一些工作
fmt.Println("Goroutine finished.")
}()
// 等待一段时间,以便让协程有时间执行
time.Sleep(2 * time.Second)
fmt.Println("Main goroutine finished.")
}
其输出结果如下:
这是同时输出多个协程的例子。
func hello() {
fmt.Println("Hello Goroutine!")
}
func main() {
for i := 0; i < 10; i++ {
go hello(i)
}
fmt.Println("main goroutine done!")
time.Sleep(time.Second * 2)
}
多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。
2.1 GMP
GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。 下面是对于GMP的解释: G(Goroutine) :
-
Goroutine 是 Go 语言中的轻量级并发执行单位,类似于线程。不同于传统操作系统线程,Go 的 goroutines 更轻量且使用更少的资源。 M(Machine Thread) :
-
Machine Thread 是指操作系统线程。在 Go 语言中,每个 M 都对应着一个操作系统线程,用于执行 goroutines。M 负责协程的实际执行,包括切换协程、执行调度和垃圾回收等任务。
P(Processor) :
- Processor 是逻辑处理器的意思,它是与 M 相关联的。每个 M 可以关联到一个或多个 P。 P 负责调度 goroutines 到 M 上执行,并且还负责管理 goroutines 的运行队列。P 也可以被看作是 Go 运行时的调度单位,负责将 goroutines 分配到 M 上以进行实际执行
2.2 并发操作和安全锁
有时候在Go代码中可能会存在多个goroutine同时操作一个资源,这种情况会发生竞态问题。我们现实生活中一个车牌可能同时被很多人抢一样的道理。
下面举一个简单的并发操作的例子
var x int64
var wg sync.WaitGroup
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
上面的这个代码在实际过程中,会和别的函数互相竞争,实际结果和我们想到的结果可能存大量的不同。
为了解决这个问题,因此我们提出了互斥锁。
var (
counter int
mutex sync.Mutex
)
func increment() {
mutex.Lock() // 获取互斥锁
defer mutex.Unlock()
counter++
fmt.Println("Incremented to:", counter)
}
func main() {
for i := 0; i < 5; i++ {
go increment() // 启动多个协程增加 counter
}
time.Sleep(time.Second)
}
在这个示例中,我们创建了一个包含互斥锁的 sync.Mutex 变量 mutex,以控制对 counter 变量的访问。increment 函数使用 Lock 获取互斥锁,确保只有一个协程可以执行递增操作,然后在函数结束时使用 Unlock 释放互斥锁。
在 main 函数中,我们启动了多个协程来调用 increment 函数,通过输出可以观察到每个协程都会递增 counter,但由于互斥锁的存在,它们会依次执行,确保不会发生竞争条件。
互斥锁是一种常用的并发控制工具,但需要注意在使用时避免死锁和性能问题。合理地选择互斥锁的作用范围和位置,以及保证锁的释放时机。
2.3读写互斥锁
互斥锁是一种保护共享资源的机制,它确保在任何时刻只有一个协程可以访问被保护的资源。然而,在许多情况下,实际的并发应用中,读取操作远远多于写操作,并且这些读取操作并不会修改共享资源的内容。在这些情况下,使用互斥锁可能会导致性能下降,因为它会阻止多个协程同时读取。
为了解决这个问题,引入了读写锁(RWMutex)。读写锁允许多个协程同时获取读锁并读取共享资源,但只有一个协程能够获取写锁进行修改操作。这种机制在读多写少的场景下能够提高并发性能。
读写锁分为两种类型:读锁和写锁。当一个协程获取读锁时,其他协程也可以获取读锁继续读取,但获取写锁的协程会被阻塞。而当一个协程获取写锁时,其他协程无论是获取读锁还是写锁,都必须等待写锁释放。
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
// lock.Lock()
rwlock.Lock()
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
rwlock.Unlock() // 解写锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func read() {
// lock.Lock() // 加互斥锁
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
// lock.Unlock() // 解互斥锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
}
不过我们需要合理加锁,我们使用读写互斥锁,读取一次数据时机是188ms,如果从数据库当中读取可能会更长,需要适当考虑安全性问题的同时,考虑效率性。
3.关于如何学习和使用goroutine和通道的思考和建议
- 理解并发的概念: 在开始学习 Goroutine 和通道之前,确保您理解了并发和并行的基本概念。了解并发是如何通过同时执行多个任务来提高系统性能和资源利用率的。
- 掌握 Goroutine: 了解 Goroutine 是 Go 语言中的轻量级线程,能够在单个操作系统线程内并发执行。 学习如何使用
go关键字创建和启动 Goroutine。 理解 Goroutine 调度和资源共享的机制,以及如何避免竞争条件和数据竞争。 - 注意并发安全性: 并发编程可能涉及竞争条件、死锁、活锁等问题。确保您理解这些问题,并学习如何避免和解决它们。使用互斥锁、读写锁等来保护共享资源,避免数据竞争。