Go 语言进阶与依赖管理 | 青训营

79 阅读4分钟

高并发的关键--协程

package main

import (
	"fmt"
  "time"
)

//goroutine
func hello(i int){
  println("hello goroutine :" + fmt.Sprint(i))
}
func HelloGoRoutine(){
  for i := 0;i<5;i++ {
    go func(j int){ //go关键字为一个函数创建协程
      hello(j)
    }(i)
  }
  time.Sleep(time.Second)
}


func main(){
  HelloGoRoutine()
}

使用go关键字对某一个函数创建一个协程进行执行,可以体现go语言高并发的特点。

channel创建与使用

在这个例子中,我们首先使用make函数创建了两个通道src和dest。通道的类型是int,表示它们可以传递整数类型的数据。其中,dest通道的缓冲区大小为3,表示它最多可以缓存3个数据。

接下来,我们使用一个goroutine来向src通道中发送数据。具体来说,我们使用一个for循环,向通道中依次发送0到9这10个整数。由于通道的发送操作是阻塞的,因此在缓冲区满或者接收方没有准备好时,发送操作会一直阻塞。

然后,我们又使用一个goroutine来从src通道中接收数据,并将每个数据的平方传递给dest通道。具体来说,我们使用一个for循环和range语句,从src通道中依次接收数据。由于通道的接收操作也是阻塞的,因此在通道中没有数据可用时,接收操作会一直阻塞。

在生产和消费的场景中,使用带缓冲的通道可以带来以下几个优点:

  1. 减少锁的竞争:在没有缓冲的通道中,发送和接收操作都是阻塞的,这意味着它们必须在同一时刻进行。如果发送方和接收方的速度不一致,就会导致其中一个方面一直阻塞等待,从而降低程序的性能。而使用带缓冲的通道可以缓解这个问题,因为缓冲区可以存储一定数量的数据,这样发送方和接收方就可以异步地进行,从而减少锁的竞争,提高程序的吞吐量。
  2. 提高程序的响应速度:由于缓冲区的存在,生产者可以快速地将数据发送到缓冲区中,然后继续执行其他任务,而不需要等待消费者处理。同样地,消费者也可以从缓冲区中快速地接收数据,并进行后续的处理。这样可以有效地提高程序的响应速度,特别是在处理大量数据的场景中。
  3. 降低系统开销:在生产和消费的场景中,如果使用无缓冲的通道,就需要频繁地进行上下文切换和内存分配操作,这会导致系统开销增加。而使用带缓冲的通道可以减少这些操作的次数,从而减轻系统的负担,提高程序的运行效率。

需要注意的是,带缓冲的通道并不适用于所有的生产和消费场景。如果生产者和消费者的速度差异较大,或者缓冲区的大小过小,都可能导致数据丢失或者程序出现错误。因此,在选择通道类型时,需要根据具体的场景进行评估和选择。

package main

import (
	"fmt"
)

func CalSquare() {
	src := make(chan int)
	dest := make(chan int, 3)

	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()

	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()

	for x := range dest {
		fmt.Println(x)
	}
}

func main() {
	CalSquare()
}

Lock的使用方法

lock sync.Mutex

lock.Lock()

协程执行操作

lock.unLock()

根据下面代码的输出结果可以发现,使用lock可以让5个协程完全进行x自增1的操作共10000次,而不加锁的x最后的值在7000~8000左右,是由于goroutine是并发执行的,因此在不使用锁的情况下,多个goroutine可能会同时对x进行修改,从而导致数据竞争和结果的不确定性。而使用锁可以避免这种情况的发生,保证数据的正确性。

package main

import (
	"sync"
	"time"
)

var (
	x    int64
	lock sync.Mutex
)

func addWithLock() {
	for i := 0; i < 2000; i++ {
		lock.Lock()
		x += 1
		lock.Unlock()
	}
}

func addWithoutLock() {
	for i := 0; i < 2000; i++ {
		x += 1
	}
}

func Add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("WithoutLock:", x)

	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("WithLock:", x)
}

func main() {
	Add()
}

waiting group相关

WaitGroup在go语言中,用于线程同步,单从字面意思理解,wait等待的意思,group组、团队的意思,WaitGroup就是指等待一组,等待一个系列执行完成后才会继续向下执行。
先说说WaitGroup的用途:它能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成。
WaitGroup总共有三个方法:Add(delta int),Done(),Wait()。简单的说一下这三个方法的作用。
Add: 添加或者减少等待goroutine的数量
Done: 相当于Add(-1)
wait: 执行阻塞,直到所有的WaitGroup数量变成0

// Add(delta int):向WaitGroup中添加delta个等待的goroutine。这个方法可以用于在启动goroutine之前,先向WaitGroup中添加等待的数量。

// Done():表示一个等待的goroutine已经完成了。当一个goroutine完成时,需要调用Done方法来通知WaitGroup,以便它可以更新内部计数器。

// Wait():等待所有等待的goroutine完成。Wait方法会阻塞当前的goroutine,直到所有已添加的等待的goroutine都执行完毕。

package main

import (
	"fmt"
	"sync"
)

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done() //完成时计数器-1
	fmt.Printf("Worker %d starting\n", id)
	// 模拟一些工作
	for i := 0; i < 3; i++ {
		fmt.Printf("Worker %d working...\n", id)
	}
	fmt.Printf("Worker %d finished\n", id)
}

func main() {
	// 创建一个WaitGroup
	var wg sync.WaitGroup

	// 启动5个goroutine
  wg.Add(5)
	for i := 1; i <= 5; i++ {
		
		go worker(i, &wg)
	}

	// 等待所有goroutine执行完成
	wg.Wait()

	fmt.Println("All workers finished")
}