并发编程初步 | 青训营笔记

54 阅读4分钟

一、并发编程

1.Goroutine(协程)

goroutine 是一种轻量级的线程(线程是栈mb级别的, 而协程是栈kb级别的), 可以与其他 goroutine 运行在相同的空间地址,易于创建而且可以在几纳秒内启动和销毁。

一个goroutine的示例:

package main

import "time"

func hello(i int) {
	println("goroutine: ", i)
}

func main() {
	for i := 0; i < 5; i++ {
		go hello(i)
	}
        
	time.Sleep(time.Second)
}

我们使用go语句将后面的函数在一个新创建的goroutine中运行, go语句本身会立即完成。 当一个程序启动时,其主函数会在一个单独的goroutine中运行,我们叫它main goroutine。主函数返回时,所有的goroutine都会被直接打断,程序退出。

goroutine的运行顺序和时间是不确定的, 执行两次程序:

image.png 会发现两次执行的结果不一样, 这也是为什么在示例中需要让main函数调用time.Sleep(time.Second), 这保证了其他的协程都能成功运行。如果将其注释掉再次运行, 可能会出现下面的情况:

image.png

main函数提前退出了, 有一些goroutine没有执行就被直接中断了。

2.Channel

goroutine是一种轻量级的线程, 所以也可以共享资源。在go中提倡使用通信来共享内存而不是通过共享内存来实现通信

image.pnggo中使用channel来实现不同goroutine之间的数据共享

创建channel, go中提供了两种类型的通道

make(chan int) // 创建一个无缓冲的int类型通道
make(chan int, 3) // 创建一个有缓冲的int类型的通道, 一次最多可以存储3个数据

使用channel的例子:

package main

func main() {
	src := make(chan int)
	dest := make(chan int, 3)
	
        // 发送 0 - 9 数字 
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i
		}
	}()

        // 从src中接收数据, 计算数字的平方
	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()

        // 打印结果
	for i := range dest {
		println(i)
	}
}

运行结果:

image.png

当一个值传入到一个chan中而没有被其他goroutine接收时, 就会进入阻塞状态, 阻止当前goroutine代码的进一步运行, 直到其值被接收。当一个goroutinechan中接收值,而该chan中没有数据时也会进入阻塞状态

在案例中srcdest可以看作生产者和消费者, 而消费者的逻辑一般比生产者复杂, 运行时间慢, 为了避免生产者(src)发生阻塞, 消费者(dest)中使用的是有缓冲的chan, 一次可以存储多个数据, 这样就可以减少src发生阻塞的时间, 提高执行效率

3. 并发安全 Lock

chan是并发安全的, 在go也可以使用互斥锁(Mutex)来实现并发安全

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	lock sync.Mutex
	x    int64
)

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

func main() {
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	fmt.Println("x = ", x)
}

上面程序的addWithLock()x进行1000次+1操作, 启动五个协程并发执行

结果:

image.png

结果说明使用Mutex可以保证并发安全

4.WaitGroup

在第一个例子中, 因为goroutine的执行顺序和执行时间都不确定, 所以我们在main中要调用time.Sleep, 但不是每次我们设置的时间都能保证所有的goroutine执行完毕, 这时我们使用WaitGroup就可以解决这个问题

WaitGroup的三个方法:

  • Add - 设置计数器的数目 (goroutine的数量)
  • Done - 当一个goroutine执行完毕时, 计数器 - 1
  • Wait - 阻塞直到计数器为0

修改第一个例子:

package main

import (
	"sync"
)

var wg sync.WaitGroup

func hello(i int) {
	println("goroutine: ", i)
}

func main() {
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done()
			hello(j)
		}(i)
	}
	wg.Wait()
}

运行结果:

image.png

二、总结

以上就是今天学习的go中并发编程的基础知识。本人是初学者, 所以文章中一些表述可能会有错误。如果您发现了文章中的错误或有任何改进建议,欢迎在评论区留言,我会认真考虑并及时修改, 非常感谢🥰

最后, 如果本文写得还可以, 帮我点个免费的赞👍(关注更好🥰