浅谈Go语言并发

116 阅读7分钟

简介

相比于Java和C语言,Go的并发并没有那么昂贵。具体的讲,Go语言用goroutines映射到线程中。 今天我主要来用代码来帮大家浅浅理解一下。代码中的评论可以让大家更好的理解。

  1. 创建程序
  2. 同步
    1. 等待组
    2. 互斥
import "fmt" 
import "time" func main() 
{ 
sayHello()
// just run this function
// green thread, create abstrction go thread 
// goroutines 
// map goroutines onto operating thread go sayHello() time.Sleep(100 * time.Millisecond) 
go sayHello() //用go关键词来开启并发的过程
time.Sleep(100 * time.Millisecond)
}


func main() { 
    var msg = "Hello" 
    go func() { 
        fmt.Pritnln(msg) 
        }() 
        msg = "Goodbye" 
        time.Sleep(100 * time.Millisecond) } // try to avoid this. Print Goodbye
    //GO语言自己会把自己搞混,在这里我们输出的是在main函数里的而不是我们想要的hello

func main() { 
    var msg = "Hello" 
    go func(msg string) { 
        fmt.Pritnln(msg) 
    }(msg) 
    msg = "Goodbye" time.Sleep(100 * time.Millisecond) } 
// print hello 为了避免这个问题,我们可以带上parameter,这个参数默认的就是一开始定义的

//介绍并行,已经锁
import "sync" 
var wg = sync.WaitGroup{} 
func main() { 
    var msg = "Hello" wg.Add(1) 
// 向等待组 wg 添加一个等待任务。 /* go func(msg string) {...}(msg):创建了一个匿名的 Go 协程, 使用闭包的方式捕获了 msg 变量的值,并在协程中打印该值,然后调用 wg.Done() 表示任务完成。 */ go func(msg string) { fmt.Pritnln(msg) wg.Done() }(msg) msg= "Goodbye" wg.Wait() // 等待所有的任务完成。 }

// 不用sleep的原因 
// 这是因为在使用 go func() 创建协程时,该协程会在调度后尽可能快地执行,
//因此在 go func() 中打印 msg 的值时,通常 msg 的值还没有被修改为 "Goodbye",
//而是保持着 "Hello"。 这个行为是由于 Go 语言的特性,在这种情况下,
//主线程和协程之间会共享变量 msg。 请注意,尽管在大多数情况下可能会打印出 "Hello",
//但这并不能保证一定如此。 在更复杂的程序中,可能会发生竞争条件(race condition),
//导致 msg 的值不稳定。 为了避免这种不确定性,
//最好在主线程和协程之间通过通道或者其他同步方式传递数据,而不是直接共享变量 



--- // mutilple goroutine 
// 导入 "sync" 包,用于等待所有 goroutine 完成 import "sync" 
// 创建一个全局的等待组,用于确保所有 goroutine 
//执行完成后再结束 main 函数 var wg = sync.WaitGroup{}
// 创建一个全局的计数器变量 var counter = 0

var m = sync.RWMutex{}
func main() { // 循环创建 10 组 goroutine,每组包含两个 goroutine 
    for i := 0; i < 10; i++ { wg.Add(2) 
        // 每组两个 goroutine,因此等待组需要添加 2 个等待任务 
        m.RLock() 
        go sayHello() 
        // 启动一个 goroutine 来执行 sayHello() 函数 
        m.lock() 
        go increment()
    // 启动另一个 goroutine 来执行 increment() 函数 }
        wg.Wait() 
    // 等待所有 goroutine 执行完成,确保 main 函数不会过早地结束 
}

// sayHello 函数用于打印当前计数器的值 
func sayHello() { 
    // 打印 "Hello" 以及当前计数器的值 
    fmt.Println("Hello #", counter) 
    m.RUnLock() 
    wg.Done() /
    // 完成一个等待任务,表示该 goroutine 执行完成 } 
    // increment 函数用于将计数器增加 1 func increment() { counter++ // 将计数器加 1
    m.Unlock() 
    wg.Done() 
    // 完成一个等待任务,表示该 goroutine 执行完成 
}

func increment() { 
    counter++ // 将计数器加 1 
    m.Unlock()
    wg.Done() 
    // 完成一个等待任务,表示该 goroutine 执行完成 
}

Mutex

在并发编程中,当多个 goroutine 同时访问或修改共享资源时, 可能会导致竞态条件(race condition)和数据不一致的问题。 竞态条件是指多个并发操作执行的先后顺序会影响最终结果的情况,这会导致程序表现出不可预测的行为。 为了避免竞态条件和确保数据一致性,我们使用互斥锁(Mutex)。 互斥锁是一种同步原语,它允许我们保护共享资源,一次只允许一个 goroutine 访问被保护的资源。 当一个 goroutine 获得了互斥锁的所有权, 其他 goroutine 就必须等待该 goroutine 释放锁之后才能获得锁继续执行。 这样就保证了对共享资源的互斥访问,避免了竞态条件。在前面的代码示例中, 全局的计数器变量 counter 被多个 goroutine 并发地递增。如果没有使用互斥锁,就可能会发生以下情况: 两个 goroutine 同时读取 counter 的值,得到相同的结果,假设都是 0。 然后两个 goroutine 都对 counter 加 1,得到结果都是 1。 但实际上,期望的结果应该是 2(每个 goroutine 分别将 counter 从 0 加到 1)。 这就是典型的竞态条件问题,导致了数据不一致。 使用互斥锁可以解决这个问题, 它可以确保在任意时刻只有一个 goroutine 能够修改 counter, 其他 goroutine 必须等待互斥锁的释放。 修正后的代码示例中使用了 sync.WaitGroup 和 sync.Mutex, 这样保证了 counter 变量的并发安全性。当每个 goroutine 运行 increment() 函数时, 它首先会获取互斥锁的所有权,然后递增 counter 的值,最后释放互斥锁。 这样,每个 goroutine 通过互斥锁依次对 counter 进行递增,确保了正确的计数结果。

var m = sync.RWMutex{} 
func main() { 
// 循环创建 10 组 goroutine,每组包含两个 goroutine 
    for i := 0; i < 10; i++ { 
    wg.Add(2) 
    // 每组两个 goroutine,因此等待组需要添加 2 个等待任务 
    m.RLock() 
    go sayHello() 
    // 启动一个 goroutine 来执行 
    sayHello() 函数 
    m.lock() 
    go increment() 
    // 启动另一个 goroutine 来执行 
    increment() 函数
}
wg.Wait() 
// 等待所有 goroutine 执行完成,确保 main 函数不会过早地结束 
}
// sayHello 函数用于打印当前计数器的值 
func sayHello() {
    // 打印 "Hello" 以及当前计数器的值 
    fmt.Println("Hello #", counter) 
    m.RUnLock() 
    wg.Done() 
// 完成一个等待任务,表示该 goroutine 执行完成 
} // increment 函数用于将计数器增加 1 func increment() { counter++ // 将计数器加 1 m.Unlock() wg.Done() // 完成一个等待任务,表示该 goroutine 执行完成 }

读写锁

当涉及到并发访问共享资源时,读写锁(RWMutex)是一种用于管理并发访问的同步原语。RWMutex 允许多个 goroutine 并发地读取共享资源,但只允许一个 goroutine 写入(修改)共享资源。这种机制在读操作频繁而写操作较少的场景下,可以提高并发性能,因为多个 goroutine 可以同时读取资源,而只有写入时需要互斥。

RWMutex 提供了三个方法来管理读写锁:

  1. RLock():获取读锁。多个 goroutine 可以同时调用 RLock() 方法,只要没有 goroutine 持有写锁。当有 goroutine 持有写锁时,其他 goroutine 尝试获取读锁将被阻塞,直到写锁被释放。
  2. RUnlock():释放读锁。每个成功获取读锁的 goroutine 都必须调用 RUnlock() 来释放读锁。在读锁被完全释放之前,任何尝试获取写锁的 goroutine 都将被阻塞。
  3. Lock():获取写锁。当没有任何读锁或写锁被持有时,一个 goroutine 可以获取写锁,这将阻止其他所有的 goroutine(包括读锁的持有者)进一步获取读锁或写锁,直到写锁被释放。

RWMutex 的典型应用场景是在读操作频繁但写操作较少的情况下。当有多个 goroutine 同时需要读取共享资源时,它们可以通过获取读锁而并发执行,不会相互阻塞。而当某个 goroutine 需要写入共享资源时,它必须获取独占的写锁,这会阻塞其他所有 goroutine 的读写操作,直到写入完成并释放写锁为止。

值得注意的是,RWMutex 并不能保证公平性,即不保证等待的 goroutine 获得锁的顺序。因此,在高度竞争的情况下,可能会导致某些 goroutine 长时间地等待,这可能会引起饥饿问题。如果需要公平性,可以使用 sync.Mutex 结合其他同步原语来实现。