Go并发4种方法简明讲解


Posted in Golang onApril 06, 2022

一、goroutine

1、协程(Coroutine)

Golang 在语言层面对并发编程进行了支持,使用了一种协程(goroutine)机制,

协程本质上是一种用户态线程,不需要操作系统来进行抢占式调度,但是又寄生于线程中,因此系统开销极小,可以有效的提高线程的任务并发性,而避免多线程的缺点。但是协程需要语言上的支持,需要用户自己实现调度器,因为在Go语言中,实现了调度器所以我们可以很方便的能过 go关键字来使用协程。

func main() {
	for i := 0; i <10; i++ {
		go func(i int) {
			for  {
				fmt.Printf("Hello goroutine %d\n",i)
			}
		}(i)
	}
	time.Sleep(time.Millisecond)
}

最简单的一个并发编程小例子,并发输出一段话。

我们同时开了10个协程进行输出,每次在fmt.printf时交出控制权(不一定每次都会交出控制权),回到调度器中,再由调度器分配。

2、goroutine 可能切换的点

  • I/O,Select
  • channel
  • 等待锁
  • 函数调用
  • runtime.Gosched()

我们看一个小例子:

func main() {
	var a [10]int
	for i := 0; i <10; i++ {
		go func(i int) {
			for  {
				a[i]++
			}
		}(i)
	}
	time.Sleep(time.Millisecond)
	fmt.Println(a)
}

在这里,代码直接锁死,程序没有退出,因为在执行函数中没有协程的切换,因为 main函数也是一个协程。

如果想要程序退出,可以通过 runtime.Gosched()函数,在执行函数中添加一行。

for  {
  a[i]++
  runtime.Gosched()
}

加上这个函数之后,代码是可以正常执行了,但是真的是正常执行吗?不一定,我们可以使用 -reac命令来看一下数据是否有冲突:

Go并发4种方法简明讲解

这说明数据还是有冲突的,数组a中的元素一边在做自增,一边在输出。解决这个问题,我们只能使用 channel 来解决。

二、Channel

Channel 中 Go语言在语言级别提供了对 goroutine 之间通信的支持,我们可以使用 channel 在两个或者多个goroutine之间进行信息传递,能过 channel 传递对像的过程和调用函数时的参数传递行为一样,可以传递普通参数和指针。

Channel 有两种模式:

var ch1 = make(chan int)		// 无缓冲 channel,同步
var ch2 = make(chan int, 2)	// 有缓冲 channel, 异步

无缓冲的方式,数据进入 channel 只要没有被接收,就会处在阻塞状态。

var ch1 = make(chan int)		// 无缓冲 channel,同步
ch1 <- 1
ch1 <- 2
//  error: all goroutines are asleep - deadlock!
fmt.Println(<-ch1)

如果想要运行,必须要再开一个协程不停的去请求数据:

var ch1 = make(chan int)		// 无缓冲 channel,同步
go func() {
  for  {
    n := <-ch1
    fmt.Println(n)
  }
}()
ch1 <- 1
ch1 <- 2

有缓冲的方式,只要缓冲区没有满就可以一直进数据,缓冲区在填满之后没有接收也会处理阻塞状态。

func bufferChannel() {
	var ch2 = make(chan int,2)
	ch2<-1
	ch2<-2
	fmt.Println(ch2)
  // 不加这一行的话,是可以正常运行的
	ch2<-3			// error: all goroutines are asleep - deadlock!
}

1、chaanel 指定方向

比如我现在有一个函数创建一个 channel,并且不断的需要消费channel中的数据:

func worker(ch chan int) {
	for {
		fmt.Printf("hello goroutine worker %d\n", <-ch)
	}
}

func createWorker() chan int{
	ch := make(chan int)
	go worker(ch)
	return ch
}

func main() {
	ch := createWorker()
	ch<-1
	ch<-2
	ch<-3
	time.Sleep(time.Millisecond)
}

这个函数我是要给别人用的,但是我怎么保证使用 createWorker 函数创建的 channel 都是往里面传入数据的呢?

如果外面有人消费了这个 channel 中的数据,我们怎么限制?

这个时候,我们就可以给返回的channel 加上方向,指明这个 channel 中能往里传入数据,不能从中取数据:

func worker(ch <-chan int) {
	for {
		fmt.Printf("hello goroutine worker %d\n", <-ch)
	}
}

func createWorker() chan<- int{
	ch := make(chan int)
	go worker(ch)
	return ch
}

我们可以在返回 channel 的地方加上方向,指明返回的函数只能是一个往里传入数据,不能从中取数据。

并且我们还可以给专门消费的函数加上一个方向,指明这个函数只能出不能进。

2、channel 关闭

在使用 channel 的时候,随说我们可以等待channel中的函数使用完之后自己结束,或者等待 main 函数结束时关闭所有的 goroutine 函数,但是这样的方式显示不够优雅。

当一个数据我们明确知道他的结束时候,我们可以发送一个关闭信息给这个 channel ,当这个 channel 接收到这个信号之后,自己关闭。

// 方法一
func worker(ch <-chan int) {
	for {
		if c ,ok := <- ch;ok{
			fmt.Printf("hello goroutine worker %d\n", c)
		}else {
			break
		}
	}
}
// 方法二
func worker(ch <-chan int) {
	for c := range ch{
		fmt.Printf("hello goroutine worker %d\n", c)
	}
}

func main() {
	ch := createWorker()
	ch<-1
	ch<-2
	ch<-3
	close(ch)
	time.Sleep(time.Millisecond)
}

通过 Closeb函数,我们可以能过 channel 已经关闭,并且我们还可以通过两种方法判断通道内是否还有值。

三、Select

当我们在实际开发中,我们一般同时处理两个或者多个 channel 的数据,我们想要完成一个那个 channel 先来数据,我们先来处理个那 channel 怎么办呢?

此时,我们就可以使用 select 调度:

func genInt() chan int {
	ch := make(chan int)
	go func() {
		i := 0
		for {
			// 随机两秒以内生成一次数据
			time.Sleep(time.Duration(rand.Intn(2000)) * time.Millisecond)
			ch <- i
			i++
		}
	}()
	return ch
}

func main() {
	var c1 = genInt()
	var c2 = genInt()
	for {
		select {
		case n := <-c1:
			fmt.Printf("server 1 generator %d\n", n)
		case n := <- c2:
			fmt.Printf("server 2 generator %d\n", n)
		}
	}
}

1、定时器

for {
		tick := time.Tick(time.Second)
		select {
		case n := <-c1:
			fmt.Printf("server 1 generator %d\n", n)
		case n := <-c2:
			fmt.Printf("server 2 generator %d\n", n)
		case <-tick:
			fmt.Println("定时每秒输出一次!")
		}
	}

2、超时

for {
		tick := time.Tick(time.Second)
		select {
		case n := <-c1:
			fmt.Printf("server 1 generator %d\n", n)
		case n := <-c2:
			fmt.Printf("server 2 generator %d\n", n)
		case <-tick:
			fmt.Println("定时每秒输出一次!")
		case <-time.After(1300 * time.Millisecond): // 如果 1.3秒内没有数据进来,那么就输出超时
			fmt.Println("timeout")
		}
	}

四、传统的并发控制

1、sync.Mutex

type atomicInt struct {
	value int
	lock sync.Mutex
}

func (a *atomicInt) increment() {
	a.lock.Lock()
	defer a.lock.Unlock()		// 使用 defer 解锁,以防忘记
	a.value++
}

func main() {
	var a atomicInt
	a.increment()
	go func() {
		a.increment()
	}()
	time.Sleep(time.Millisecond)
	fmt.Println(a.value)
}

2、sync.WaitGroup

type waitGrouInt struct {
	value int
	wg sync.WaitGroup
}

func (w *waitGrouInt) addInt() {
	w.wg.Add(1)
	w.value++
}

func main() {
	var w waitGrouInt
	for i := 0; i < 10; i++ {
		w.addInt()
		w.wg.Done()
	}
	w.wg.Wait()
	fmt.Println(w.value)
}

 更多关于Go并发简明讲解请查看下面的相关链接

Golang 相关文章推荐
浅谈Golang 嵌套 interface 的赋值问题
Apr 29 Golang
完美解决golang go get私有仓库的问题
May 05 Golang
Go 自定义package包设置与导入操作
May 06 Golang
Golang Gob编码(gob包的使用详解)
May 07 Golang
Golang生成Excel文档的方法步骤
Jun 09 Golang
go web 预防跨站脚本的实现方式
Jun 11 Golang
Golang获取List列表元素的四种方式
Apr 20 Golang
Golang解析JSON对象
Apr 30 Golang
深入理解 Golang 的字符串
May 04 Golang
Go Grpc Gateway兼容HTTP协议文档自动生成网关
Jun 16 Golang
Go语言怎么使用变长参数函数
Jul 15 Golang
Go归并排序算法的实现方法
Apr 06 #Golang
golang操作rocketmq的示例代码
Apr 06 #Golang
victoriaMetrics库布隆过滤器初始化及使用详解
如何解决goland,idea全局搜索快捷键失效问题
golang为什么要统一错误处理
简单聊聊Golang中defer预计算参数
Mar 25 #Golang
Go 中的空白标识符下划线
You might like
很好用的PHP数据库类
2009/05/27 PHP
php中使用preg_replace函数匹配图片并加上链接的方法
2013/02/06 PHP
PHP调试的强悍利器之PHPDBG
2016/02/22 PHP
PHP5.6.8连接SQL Server 2008 R2数据库常用技巧分析总结
2019/05/06 PHP
JavaScript 编写匿名函数的几种方法
2010/02/21 Javascript
jQuery的实现原理的模拟代码 -3 事件处理
2010/08/03 Javascript
DIV菜单层实现代码
2010/11/19 Javascript
javascript避免数字计算精度误差的方法详解
2014/03/05 Javascript
EasyUi datagrid 实现表格分页
2015/02/10 Javascript
jQuery使用after()方法在元素后面添加多项内容的方法
2015/03/26 Javascript
js预加载图片方法汇总
2015/06/15 Javascript
jQuery添加和删除输入文本框标签代码
2016/05/20 Javascript
使用json来定义函数,在里面可以定义多个函数的实现方法
2016/10/28 Javascript
vue.js,ajax渲染页面的实例
2018/02/11 Javascript
通过 JS 判断页面是否有滚动条的实现方法
2018/04/05 Javascript
JavaScript实现京东购物放大镜和选项卡效果的方法分析
2018/07/05 Javascript
ES6 Generator函数的应用实例分析
2019/06/26 Javascript
Electron实现应用打包、自动升级过程解析
2020/07/07 Javascript
[03:42]2014DOTA2国际邀请赛 第三日比赛排位扑朔迷离
2014/07/12 DOTA
[02:21]十步杀一人,千里不留行——DOTA2全新英雄天涯墨客展示
2018/08/29 DOTA
python实现逻辑回归的方法示例
2017/05/02 Python
python 实现交换两个列表元素的位置示例
2019/06/26 Python
numpy数组广播的机制
2019/07/12 Python
python实现双色球随机选号
2020/01/01 Python
Python的in,is和id函数代码实例
2020/04/18 Python
python的Jenkins接口调用方式
2020/05/12 Python
css3 给页面加个半圆形导航条主要利用旋转和倾斜样式
2014/02/10 HTML / CSS
斯洛伐克家具和时尚装饰品购物网站:Butlers.sk
2019/09/08 全球购物
二年级体育教学反思
2014/01/15 职场文书
幼儿园父亲节活动方案
2014/03/11 职场文书
优秀毕业生就业推荐信
2014/05/22 职场文书
信访工作汇报材料
2014/10/27 职场文书
催款函怎么写
2015/06/24 职场文书
python绘制箱型图
2021/04/27 Python
netty 实现tomcat的示例代码
2022/06/05 Servers
Windows Server 2008配置防火墙策略详解
2022/06/28 Servers