【青训营】Go的并发编程

本文章整理自——字节跳动青年训练营(第五届)后端组

1.线程和协程

操作系统中有三个重要的概念,分别是进程、线程和协程。其中进程和线程的区别请移步操作系统专栏,现在主要叙述线程和协程的区别。
简单来说,协程又称为用户态线程(以下的线程均指的是内核级线程),它比线程更加轻量化,使用起来更灵活,具有更高的性能。具体来说,协程的各种操作所需要的开销要比线程少,因此具有更高的性能。协程线程是内核态的,栈是MB级别的;协程是用户态的,其栈是KB级别的。一个线程可以控制多个协程,Go语言自动完成协程的创建,Go一次可以创建上万条协程,因此Go在针对高并发场景上有优势

Go使用如下方法创建协程

go func(形式参数){
	函数体
}(实际参数)

简单来说,就是在函数func前加go关键字以创建协程

协程的简单例子如下:

import (
	"fmt"
	"time"
)

// 协程的简单例子
func hello(i int) {
	println("Hello goroutine" + fmt.Sprint(i))
}

func main() {
	for i := 0; i < 5; i++ {
		// 在函数func前加go关键字以创建协程
		go func(j int) {
			hello(j)
		}(i) //此处填入协程的实际参数
	}
	time.Sleep(time.Second)
}

其输入如下:

Hello goroutine1
Hello goroutine4
Hello goroutine2
Hello goroutine0
Hello goroutine3

其数字i是完全随机的,可以看出协程的并发性

2.协程的通信CSP

GO提倡通过通信共享内存而不是使用共享内存通信,两者区别如下
在这里插入图片描述

3.通道Channel

通道是Go进行通信的重要手段,通道的创建如下

make(chan 元素类型, [缓冲区大小]// example
make(chan int)	//无缓冲通道
make(chan char, 2)

在这里插入图片描述
无缓冲通道中1发送的信息回立即传送到2中,会出现同步问题。而有缓冲的通道则会先放置到缓冲区中再送入2,带缓冲通道的可以解决一些速度不匹配问题

下面使用一个例子实现生产者消费者问题:
生产者:负责生成数字,并且将数字放入到缓冲区src中
消费者:从src中取出数字,并将其取平方
生产者消费者问题详解可以看这个:https://blog.csdn.net/weixin_45434953/article/details/127044788

package main

func main() {
	src := make(chan int)     // 无缓冲通道
	dest := make(chan int, 3) // 缓冲区为3的通道

	// 生产者协程,用于生产数字
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i //将数据i冲入通道src中
		}
	}()

	// 消费者进程,取出src中的数字并且将其平方后放入dist通道
	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i //将i的平方冲入dist通道中
		}
	}()

	for i := range dest {
		println(i)
	}
}

通过结果可以看出,通道可以保证输出的顺序

4.并发安全:锁Lock

使用锁可以确保对临界资源的互斥访问,从而避免同步互斥发生的数据不匹配问题,以下是一个简单的同步互斥问题:
我们需要启动5个协程,每个协程对x进行2000次+1的操作,x=x+1的操作可以分解为:1.运算器取得x的值 2.运算器执行x=x+1,并将结果写回寄存器 3.将运算器的值写回内存

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 main() {
	x = 0
	// 启动5个协程,他们需要互斥地访问x
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("WithLock:", x)
	x = 0
	// 启动5个协程,他们不需要互斥地访问x
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("WithoutLock:", x)
}

其输出的结果是:

WithLock: 10000
WithoutLock: 7050

分析以上代码片段:
如果使用addWithoutLock()函数,假设某时刻x=10,然后协程a执行了一次x=x+1,协程b执行了一次x=x+1,此时x的值应该是12。但是假设如下情况:协程a开始运行,根据分解操作,在执行到第二步的时候,cpu切换到执行协程b,此时协程a的寄存器中x=11,但是该值尚未写回内存,因此协程b从内存中取得的x的值还是10,进行x=x+1后,x=11并且将其写回内存,协程b执行结束,切换到未执行结束的协程a执行,此时a将寄存器中的x=11写回内存,则最终内存中x=11,显然这不是我们想要的结果,这就是同步导致的数据冲突,因此WithoutLock的结果最终是小于10000的

如果使用addWithLock函数,主要区别是,x只有在取得锁后才能对其进行操作,在进行x=x+1的之前,需要取得锁lock,执行结束后,才释放锁。这使得x=x+1的操作是一气呵成的,不会被中断的,这种又叫做原子操作。这避免了上述的状况。

5.WaitGroup

Go提供了WaitGroup来实现并发任务的同步,其中主要的三个方法:

Add(delta int)	//有多少个并发的协程
Done()		// 表示协程已完成,会将计数器的值-1
Wait()	//在计数器为0之前,一直阻塞不向下执行

这类似于一个计数器,刚开始使用Add表示有n个协程,而Done方法表示协程已完成,会将n–,当n!=0的时候,会一直触发Wait()方法,使得程序阻塞在Wait处;当n=0的时候表示所有协程均已完成,程序会继续执行Wait之后的语句

示例如下:

import (
	"fmt"
	"sync"
)

func hello(i int) {
	println("hello goroutine:" + fmt.Sprint(i))
}

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

这是对本文第一个例子的改写,没有使用time.Sleep(time.Second),因此性能更好

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值