实践内容:性能调优实战|青训营

80 阅读6分钟

在性能方面,Go语言有以下优势:

1.并发执行: 通过使用Goroutine,可以很方便地实现并发执行,充分利用多核处理器的性能。 高效的调度器:Go语言的调度器(Scheduler)可以在不同的Goroutine之间高效地调度,以实现任务的快速切换和执行。

2.内存管理: Go语言的垃圾回收器(Garbage Collector)可以自动管理内存的分配和释放,避免了手动管理内存的麻烦和错误。

3.标准库支持: Go语言的标准库提供了很多高性能的包,如net/http、database/sql等,可以方便地进行网络编程和数据库操作。

4.并发执行 Goroutine:Goroutine是Go语言提供的轻量级线程,可以在并发编程时创建成千上万个Goroutine,而不会过多消耗系统资源。Goroutine通过go关键字启动,它们可以在相同的地址空间中运行,并共享内存,避免了线程切换的开销。 Channel:Channel是Goroutine之间进行通信的机制,它可以在不同的Goroutine之间传递数据。Channel可以保证并发安全,有效地解决了多个Goroutine之间的同步和数据竞争问题。 Select语句 :Select语句用于处理多个Channel的并发操作,它可以等待多个Channel中的任意一个发送或接收数据,从而实现非阻塞的多路复用。 sync包:Go语言的sync包提供了一系列的同步原语,如Mutex、RWMutex、Cond等。这些原语可以保证多个Goroutine之间的同步和互斥访问,从而避免数据竞争问题。 使用Goroutine和Channel来实现并发任务的处理

代码

package main
import (
	"fmt"
	"sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()

	for j := range jobs {
		// 模拟耗时任务
		result := j * 2

		// 将结果发送到结果Channel
		results <- result
		fmt.Printf("Worker %d processed job %d, result: %d\n", id, j, result)
	}
}

func main() {
	const numJobs = 10
	numWorkers := 3
	// 创建jobs和results通道
	jobs := make(chan int, numJobs)
	results := make(chan int, numJobs)
	// 创建WaitGroup用于等待所有Goroutine完成
	var wg sync.WaitGroup
	// 启动多个worker Goroutine
	for i := 1; i <= numWorkers; i++ {
		wg.Add(1)
		go worker(i, jobs, results, &wg)
	}
	// 提供要处理的任务
	for j := 1; j <= numJobs; j++ {
		jobs <- j
	}
	close(jobs)
	// 等待所有worker完成
	wg.Wait()
	// 关闭results通道
	close(results)
	// 从results通道接收结果
	for r := range results {
		fmt.Printf("Received result: %d\n", r)
	}
}

这个示例展示了如何使用Goroutine和Channel实现任务的并发处理,在这个过程中,没有显式地进行锁操作,通过Goroutine之间的通信来实现了并发安全。同时,通过sync.WaitGroup来等待所有的worker Goroutine完成。

互斥锁(Mutex)和读写锁(RWMutex)是 Go 语言中用于并发控制的重要机制。它们可以帮助我们对共享资源进行同步,并确保在并发访问时的正确性。使用这些锁机制,可以避免数据竞争,并提高程序的性能。 互斥锁代码

package main

import (
	"fmt"
	"sync"
	"time"
)
var (
	counter int
	mutex   sync.Mutex
	wg      sync.WaitGroup
)
func main() {
	start := time.Now()
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go increment()
	}
	wg.Wait()
	fmt.Println("Counter:", counter)
	fmt.Println("Time taken:", time.Since(start))
}

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

在上述示例中,使用互斥锁保护了 counter 变量的并发访问。每个 Goroutine 都会调用 increment 函数来增加 counter 的值。通过使用互斥锁,我们确保了每次访问 counter 变量时的原子性,避免了数据竞争。互斥锁允许多个 Goroutine 并发读取 counter 的值,但只允许一个 Goroutine 操作 counter 的增加。

读写锁代码

package main
import (
	"fmt"
	"sync"
	"time"
)
var (
	counter int
	rwMutex sync.RWMutex
	wg      sync.WaitGroup
)
func main() {
	start := time.Now()
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}
	wg.Wait()
	fmt.Println("Counter:", counter)
	fmt.Println("Time taken:", time.Since(start))
}
func read() {
	defer wg.Done()
	rwMutex.RLock()
	defer rwMutex.RUnlock()= counter
}
在上述示例中,使用读写锁保护了 counter 变量的并发读取。每个 Goroutine 都会调用 read 函数来读取 counter 的值。通过使用读写锁,我们允许多个 Goroutine 并发读取 counter 的值,而当有 Goroutine 写入 counter 时,其他 Goroutine 将被阻塞。运行这个程序可以得到正确的结果,并且相比不使用读写锁的情况下,性能更好。因为读写锁允许并发读取操作,提高了程序的并发性能。

调度器代码

package main
import (
	"fmt"
	"runtime"
	"sync"
)
func main() {
	// 设置使用多核心
	runtime.GOMAXPROCS(runtime.NumCPU())
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 1; i <= 1000000; i++ {
			fmt.Println("Routine 1:", i)
		}
	}()
	go func() {
		defer wg.Done()
		for i := 1; i <= 1000000; i++ {
			fmt.Println("Routine 2:", i)
		}
	}()
	wg.Wait()
}

在上述示例中,创建了两个并发执行的Goroutine,并分别输出一百万次的循环计数。通过调用runtime.GOMAXPROCS(runtime.NumCPU()),并且设置调度器使用了多核心,以便更好地并行执行Goroutine。可实现两个Goroutine交替地输出自己的计数值。这是因为调度器在不同的操作系统线程上轮流调度Goroutine的执行,使得它们可以并发地进行工作。 通过调度器的高效调度,我们可以充分利用多核心并发执行Goroutine,提高程序的性能。它能够将任务迅速切换和执行,有效地利用CPU资源,避免阻塞和等待时间,提高了程序的响应性能和吞吐量。

2.内存管理

Go语言的垃圾回收器可以自动管理内存的分配和释放,这对性能优化有以下好处:

1.去除手动内存管理的负担:在其他编程语言中,我们需要手动分配和释放内存。

2.手动管理内存可能会产生内存泄漏、野指针等问题,对程序的性能和稳定性造成影响.

3.Go语言的垃圾回收器可以自动处理内存分配和释放,减轻了程序员的负担,并避免了因手动管理内存而引起的错误。

内存回收的效率和准确性:

Go语言的垃圾回收器使用了基于三色标记的并发标记-清除算法,能够在运行时检测和回收不再使用的内存。它在GC过程中进行并发标记,以减少停顿时间,并且只回收不再使用的内存,避免了频繁的内存分配和释放。这样可以提高内存的利用率和程序的整体性能。

Go语言垃圾回收器对性能优化作用实例

package main
import (
	"fmt"
	"runtime"
)
func main() {
	s := make([]int, 1000000) // 创建一个大型切片
	fmt.Println("Before GC")
	printMemStats()
	// 手动触发GC
	runtime.GC()
	fmt.Println("After GC")
	printMemStats()
}
func printMemStats() {
	var stats runtime.MemStats
	runtime.ReadMemStats(&stats)
	fmt.Printf("Alloc: %d bytes\n", stats.Alloc)          // 当前内存分配的字节数
	fmt.Printf("TotalAlloc: %d bytes\n", stats.TotalAlloc) // 累计分配的字节数
	fmt.Printf("HeapAlloc: %d bytes\n", stats.HeapAlloc)    // 堆上分配的字节数
	fmt.Printf("HeapSys: %d bytes\n", stats.HeapSys)        // 当前堆的总字节数
	fmt.Printf("NumGC: %d\n", stats.NumGC)                  // 执行的GC次数
}

在上述示例中,创建了一个大型切片 s,然后手动触发了一次垃圾回收。通过调用 runtime.GC(),显式地告诉垃圾回收器进行一次清理。 通过运行这个程序,并查看GC前后的内存统计信息,你会发现在GC之后,已被回收的内存数量增加,即垃圾回收器释放了不再使用的内存。这样可以确保内存得到有效管理,避免内存泄漏和资源浪费,提高程序的性能和稳定