Go语言中并发相关| 青训营

72 阅读5分钟

1. 协程(Goroutine)

协程是轻量级线程,KB级别,可以高效地利用内存资源,Go语言中用go关键字开启一个协程,通过通道进行安全地并发通信和数据传递。

线程是轻量级进程,MB级别,其创建和销毁需要操作系统的支持。线程的并发执行需要使用锁,信号量等同步原语。

协程代码示例

package main

import (
	"fmt"
	"time"
)

func main() {
	// 创建10个协程并发执行任务
	for i := 0; i < 10; i++ {
		go worker(i)
	}

	// 等待协程执行完毕
	time.Sleep(2 * time.Second)
	fmt.Println("All workers have completed.")
}

func worker(id int) {
	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Second)
	fmt.Printf("Worker %d completed\n", id)
}

运行上述代码,输出的顺序不固定,主线程只会等待足够的时间,不会等待所有协程执行完

2. 通道

通道分为有缓冲通道和无缓冲通道,用 make 函数创建通道,函数中的参数用于指定通道的数据类型和缓冲区大小。

向通道发送和接收数据用<-运算符

package main

import (
	"fmt"
)

func main() {
	// 创建一个通道
	ch := make(chan int)

	// 启动一个协程发送数据到通道
	go func() {
                defer close(ch) /* 延迟关闭通道,defer用于确保通道被访问时还存在,放在第一                                   行确保协程执行结束后关闭通道*/
		for i := 0; i < 5; i++ {
			fmt.Println("Sending:", i)
			ch <- i // 发送数据到通道
		}
	}()

	// 从通道接收数据并打印
	for value := range ch {
		fmt.Println("Received:", value)
	}
}

向通道发送数据时如果通道已满,则发送操作会阻塞,直到有空间可以发送;当从通道接收数据时,如果通道为空,则接收操作会阻塞,直到有数据可接收。这种同步阻塞的特性使得协程可以有效地进行同步操作。对于有缓冲区的通道,在发送数据时,如果通道未满,则发送操作立即完成;接收数据时,如果通道不空,则接收操作立即完成。

通道可以限制为单向的,即只能接收或发送数据。通过在通道类型前加上 <- 表示只能发送,加在后面表示只能接收。例如:chan<- int 表示只能发送 int 类型的通道,<-chan int 表示只能接收 int 类型的通道。

可以通过调用 close 函数来关闭通道,关闭后的通道不能再发送数据,但可以继续接收已经发送的数据。接收关闭的通道会立即得到零值。

3. Sync包下并发操作

3.1 Lock

Sync包下的锁有互斥锁Mutex和读写锁RWMutex

3.1.1 mutex互斥锁

  • 互斥锁用于保护临界区,确保只有一个协程可以进入临界区执行代码,从而避免多个协程同时访问共享资源导致的数据竞争和错误。

  • 创建互斥锁:可以使用 var mutex sync.Mutex 来声明和创建一个互斥锁变量。

  • 加锁和解锁:在进入临界区之前,通过调用 mutex.Lock() 来获取互斥锁,锁定当前协程,确保只有一个协程可以进入临界区。在临界区执行完毕后,通过调用 mutex.Unlock() 来释放互斥锁,允许其他协程进入临界区。

  • 注意事项:

    • 在调用 Lock() 之后,确保一定会调用 Unlock(),否则可能会导致死锁。
    • 不要在临界区内进行阻塞操作,以免造成其他协程的长时间等待。

3.1.2 RWmutex读写锁

  • 读写锁是一种更高级的锁机制,可以同时支持多个协程进行读操作,但只允许一个协程进行写操作。

  • 创建读写锁:可以使用 var rwMutex sync.RWMutex 来声明和创建一个读写锁变量。

  • 读锁和写锁:

    • 读锁(共享锁)可以被多个协程获取,只要没有协程持有写锁。通过调用 rwMutex.RLock() 来获取读锁,调用 rwMutex.RUnlock() 来释放读锁。
    • 写锁(排他锁)只允许一个协程获取,在写锁被持有时,其他协程无法获取读锁或写锁。通过调用 rwMutex.Lock() 来获取写锁,调用 rwMutex.Unlock() 来释放写锁。
  • 注意事项:

    • 读操作频繁且不会改变共享资源时,多个协程可以同时获取读锁,提高并发性能。
    • 写操作会阻塞其他协程的读和写操作,只有写锁被释放后,其他操作才能继续。
    • 不要在持有读锁或写锁的情况下再次获取对应的锁,否则会导致死锁。
package main

import (
	"fmt"
	"sync"
)

var (
	counter    int
	wg         sync.WaitGroup
	mutex      sync.Mutex
	rwMutex    sync.RWMutex
)

func main() {
	// 互斥锁示例
	wg.Add(2)
	go incrementWithMutex("goroutine 1")
	go incrementWithMutex("goroutine 2")
	wg.Wait()

	fmt.Println("Counter value with mutex:", counter)

	// 读写锁示例
	wg.Add(5)
	go readFromCounter("reader 1")
	go readFromCounter("reader 2")
	go readFromCounter("reader 3")
	go writeToCounter("writer 1")
	go writeToCounter("writer 2")
	wg.Wait()

	fmt.Println("Counter value with RWMutex:", counter)
}

// 使用互斥锁递增 counter
func incrementWithMutex(name string) {
	defer wg.Done()//函数执行完毕后通知waitgroup

	for i := 0; i < 5; i++ {
		mutex.Lock()         // 加锁
		counter++            // 临界区递增操作
		fmt.Println(name, "incremented counter to", counter)
		mutex.Unlock()       // 解锁
	}
}

// 使用读写锁读取 counter
func readFromCounter(name string) {
	defer wg.Done()

	for i := 0; i < 3; i++ {
		rwMutex.RLock()      // 获取读锁
		fmt.Println(name, "read counter value as", counter)
		rwMutex.RUnlock()    // 释放读锁
	}
}

// 使用读写锁递增 counter
func writeToCounter(name string) {
	defer wg.Done()

	for i := 0; i < 2; i++ {
		rwMutex.Lock()       // 获取写锁
		counter++            // 临界区递增操作
		fmt.Println(name, "incremented counter to", counter)
		rwMutex.Unlock()     // 释放写锁
	}
}

3.2 WaitGroup

sync.WaitGroup 是 Go 语言中用于等待一组协程完成工作的同步原语。它提供了线程等待功能,可以在协程之间同步执行,并且可以确保在协程完成工作之前不会继续执行主协程或其他协程。

WaitGroup 提供了以下三个方法:

  1. Add(delta int):用于添加需要等待的协程数量,将 WaitGroup 的计数器加上 delta。通常在创建一个协程之前调用 Add(1),表示要等待一个协程的完成。
  2. Done():用于通知 WaitGroup 协程的工作已经完成,将计数器减一。通常在协程的最后调用 Done() 来标记协程的工作完成。
  3. Wait():用于阻塞主协程,直到 WaitGroup 的计数器归零。Wait() 方法会一直阻塞,直到计数器变为零,表示所有协程的工作都已完成。
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	// 添加一个协程到 WaitGroup 计数器
	wg.Add(1)

	go func() {
		defer wg.Done()

		// 协程的工作逻辑
		fmt.Println("Hello from goroutine")
	}()

	fmt.Println("Waiting for goroutine to finish...")
	wg.Wait()//阻塞,直到计数器归零,即所有协程执行完毕

	fmt.Println("All goroutines have finished")
}