【青训营学习笔记】go是如何实现高并发的?有哪些方式? | 青训营

61 阅读3分钟

在Go语言中,高并发是其设计初衷之一,它通过轻量级的Goroutines和内置的并发原语来实现高效的并发编程。以下是一些在Go中实践高并发的方法和技巧:

Goroutines和Channels

使用Goroutines来启动轻量级线程,可以同时执行多个任务,而不会占用太多系统资源。 使用Channels来在Goroutines之间进行通信和同步,确保数据安全和协调执行。


func main() {
	ch := make(chan int)

	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
			time.Sleep(time.Second)
		}
		close(ch)
	}()

	for value := range ch {
		fmt.Println("Received:", value)
	}
}

避免共享状态

在并发编程中,共享状态容易引发竞态条件和数据竞争。尽量避免共享状态,或者通过使用锁、互斥体或通道来保护共享资源的访问。

使用互斥锁(Mutex)

当需要多个Goroutines访问共享资源时,可以使用互斥锁来确保同时只有一个Goroutine可以访问该资源,以避免竞态条件。

var counter int
var mu sync.Mutex

func increment() {
	mu.Lock()
	defer mu.Unlock()
	counter++
}

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

使用读写锁(RWMutex)

当资源需要更多的读操作而较少的写操作时,使用读写锁可以提高性能。多个Goroutines可以同时读取资源,但只有一个可以进行写入。

var (
	data    map[string]string
	dataMu  sync.RWMutex
)

func readData(key string) string {
	dataMu.RLock()
	defer dataMu.RUnlock()
	return data[key]
}

func writeData(key, value string) {
	dataMu.Lock()
	defer dataMu.Unlock()
	data[key] = value
}

func main() {
	data = make(map[string]string)
	for i := 0; i < 5; i++ {
		go func(i int) {
			key := fmt.Sprintf("key%d", i)
			value := fmt.Sprintf("value%d", i)
			writeData(key, value)
		}(i)
	}

	time.Sleep(time.Second) // Wait for goroutines to complete
	dataMu.RLock()
	fmt.Println("Data:", data)
	dataMu.RUnlock()
}

使用并发安全的数据结构

Go标准库提供了许多并发安全的数据结构,如sync.Map用于安全地存储映射,atomic包用于原子操作等。

限制Goroutines数量

当你的程序需要处理大量任务时,不要无限制地创建Goroutines。可以使用worker池或者Goroutine池来限制并发数量,以避免资源耗尽。

使用Select语句

Select语句用于处理多个通道的操作,能够在不同的通道上等待数据并执行相应的操作,避免了阻塞。

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(time.Second)
		ch1 <- 1
	}()
	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- 2
	}()

	select {
	case val := <-ch1:
		fmt.Println("Received from ch1:", val)
	case val := <-ch2:
		fmt.Println("Received from ch2:", val)
	case <-time.After(3 * time.Second):
		fmt.Println("Timeout")
	}
}

超时和取消

使用context包来管理Goroutines的生命周期,可以在超时或需要取消时优雅地终止Goroutines的执行。

func worker(ctx context.Context) {
	select {
	case <-ctx.Done():
		fmt.Println("Worker canceled")
	case <-time.After(2 * time.Second):
		fmt.Println("Worker completed")
	}
}

func main() {
	parentCtx := context.Background()
	ctx, cancel := context.WithTimeout(parentCtx, time.Second)
	defer cancel()

	go worker(ctx)

	select {
	case <-ctx.Done():
		fmt.Println("Main canceled")
	}
}

性能调优

使用Go的性能分析工具(如pprof)来识别性能瓶颈,对瓶颈进行优化,例如减少内存分配、降低锁的竞争等。

避免全局变量

全局变量可能引发竞态条件,尽量避免在多个Goroutines之间共享全局状态。

使用带缓冲的通道

带缓冲的通道可以提高通信的效率,因为发送和接收操作可以在不同的Goroutines中异步进行。

func main() {
	ch := make(chan int, 3) // 带缓冲的通道,容量为3

	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println("Sending:", i)
			ch <- i
			time.Sleep(time.Second)
		}
		close(ch)
	}()

	for value := range ch {
		fmt.Println("Received:", value)
	}
}

合理的并发设计

在设计阶段考虑并发,合理拆分任务,避免瓶颈,充分利用硬件资源。