Golang 并发简介
并发概要
随着多核CPU的普及, 为了更快的处理任务, 出现了各种并发编程的模型, 主要有以下几种:
| 模型名称 | 优点 | 缺点 |
|---|---|---|
| 多进程 | 简单, 隔离性好, 进程间几乎无影响 | 开销最大 |
| 多线程 | 目前使用最多的方式, 开销比多进程小 | 高并发模式下, 效率会有影响 |
| 异步 | 相比多线程而言, 可以减少线程的数量 | 编码要求高, 需要对流程分割合理 |
| 协程 | 用户态线程, 不需要操作系统来调度, 所以轻量, 开销极小 | 需要语言支持 |
协程介绍
协程是个抽象的概念, 可以映射到到操作系统层面的进程, 线程等概念.
由于协程是用户态的线程, 不用操作系统来调度, 所以不受操作系统的限制, 可以轻松的创建百万个, 因此也被称为 "轻量级线程".
在 golang 中, 协程不是由库实现的, 而是受语言级别支持的, 因此, 在 golang 中, 使用协程非常方便.
下面通过例子演示在 golang 中, 如何使用协程来完成并发操作.
golang 并发
实现方式
golang 中, 通过 go 关键字可以非常简单的启动一个协程, 几乎没有什么学习成本.
当然并发编程中固有的业务上的困难依然存在(比如并发时的同步, 超时等), 但是 golang 在语言级别给我们提供了优雅简洁的解决这些问题的途径.
理解了 golang 中协程的使用, 会给我们写并发程序时带来极大的便利.
首先以一个简单的例子开始 golang 的并发编程.
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 10; i++ {
go sum(i, i+10)
}
time.Sleep(time.Second * 5)
}
func sum(start, end int) int {
var sum int = 0
for i := start; i < end; i++ {
sum += i
}
fmt.Printf("Sum from %d to %d is %d\n", start, end, sum)
return sum
}
执行结果如下: (同时启动10个协程做累加运算, 10个协程的执行顺序可能会不一样)
$ go run main.go
Sum from 0 to 10 is 45
Sum from 6 to 16 is 105
Sum from 7 to 17 is 115
Sum from 2 to 12 is 65
Sum from 8 to 18 is 125
Sum from 1 to 11 is 55
Sum from 9 to 19 is 135
Sum from 3 to 13 is 75
Sum from 4 to 14 is 85
Sum from 5 to 15 is 95
通过 go 关键字启动协程之后, 主进程并不会等待协程的执行, 而是继续执行直至结束.
本例中, 如果没有 time.Sleep(time.Second * 5) 等待5秒的话, 那么主进程不会等待那10个协程的运行结果, 直接就结束了.
主进程结束也会导致那10个协程的执行中断, 所以, 如果去掉 time.Sleep 这行代码, 可能屏幕上什么显示也没有.
简单示例
实际使用协程时, 我们一般会等待所有协程执行完成(或者超时)后, 才会结束主进程, 但是不会用 time.Sleep 这种方式,
因为主进程并不知道协程什么时候会结束, 没法设置等待时间.
这时, 就看出 golang 中的 channel 机制所带来的好处了. 下面用 channel 来改造上面的 time.Sleep
package main
import "fmt"
func main() {
var ch = make(chan string)
for i := 0; i < 10; i++ {
go sum(i, i+10, ch)
}
for i := 0; i < 10; i++ {
fmt.Print(<-ch)
}
}
func sum(start, end int, ch chan string) {
var sum int = 0
for i := start; i < end; i++ {
sum += i
}
ch <- fmt.Sprintf("Sum from %d to %d is %d\n", start, end, sum)
}
程序执行结果和上面一样, 因为是并发的缘故, 可能输出的 sum 顺序可能会不一样.
$ go run main.go
Sum from 9 to 19 is 135
Sum from 0 to 10 is 45
Sum from 5 to 15 is 95
Sum from 6 to 16 is 105
Sum from 7 to 17 is 115
Sum from 2 to 12 is 65
Sum from 8 to 18 is 125
Sum from 3 to 13 is 75
Sum from 1 to 11 is 55
Sum from 4 to 14 is 85
golang 的 chan 可以是任意类型的, 上面的例子中定义的是 string 型.
从上面的程序可以看出, 往 chan 中写入数据之后, 协程会阻塞在那里, 直到在某个地方将 chan 中的值读取出来, 协程才会继续运行下去.
上面的例子中, 我们启动了10个协程, 每个协程都往 chan 中写入了一个字符串, 然后在 main 函数中, 依次读取 chan 中的字符串, 并在屏幕上打印出来.
通过 golang 中的 chan, 不仅实现了主进程 和 协程之间的通信, 而且不用像 time.Sleep 那样不可控(因为你不知道要 Sleep 多长时间).
并发时的缓冲
上面的例子中, 所有协程使用的是同一个 chan, chan 的容量默认只有 1, 当某个协程向 chan 中写入数据时, 其他协程再次向 chan 中写入数据时, 其实是阻塞的.
等到 chan 中的数据被读出之后, 才会再次让某个其他协程写入, 因为每个协程都执行的非常快, 所以看不出来.
改造下上面的例子, 加入些 Sleep 代码, 延长每个协程的执行时间, 我们就可以看出问题, 代码如下:
package main
import (
"fmt"
"time"
)
func main() {
var ch = make(chan string)
for i := 0; i < 5; i++ {
go sum(i, i+10, ch)
}
for i := 0; i < 10; i++ {
time.Sleep(time.Second * 1)
fmt.Print(<-ch)
}
}
func sum(start, end int, ch chan string) int {
ch <- fmt.Sprintf("Sum from %d to %d is starting at %s\n", start, end, time.Now().String())
var sum int = 0
for i := start; i < end; i++ {
sum += i
}
time.Sleep(time.Second * 10)
ch <- fmt.Sprintf("Sum from %d to %d is %d at %s\n", start, end, sum, time.Now().String())
return sum
}
执行结果如下:
$ go run main.go
Sum from 4 to 14 is starting at 2015-10-13 13:59:56.025633342 +0800 CST
Sum from 3 to 13 is starting at 2015-10-13 13:59:56.025608644 +0800 CST
Sum from 0 to 10 is starting at 2015-10-13 13:59:56.025508327 +0800 CST
Sum from 2 to 12 is starting at 2015-10-13 13:59:56.025574486 +0800 CST
Sum from 1 to 11 is starting at 2015-10-13 13:59:56.025593711 +0800 CST
Sum from 4 to 14 is 85 at 2015-10-13 14:00:07.030611465 +0800 CST
Sum from 3 to 13 is 75 at 2015-10-13 14:00:08.031926629 +0800 CST
Sum from 0 to 10 is 45 at 2015-10-13 14:00:09.036724803 +0800 CST
Sum from 2 to 12 is 65 at 2015-10-13 14:00:10.038125044 +0800 CST
Sum from 1 to 11 is 55 at 2015-10-13 14:00:11.040366206 +0800 CST
为了演示 chan 的阻塞情况, 上面的代码中特意加了一些 time.Sleep 函数.
- 每个执行 Sum 函数的协程都会运行 10 秒
- main函数中每隔 1 秒读一次 chan 中的数据
从打印结果我们可以看出, 所有协程几乎是同一时间开始的, 说明了协程确实是并发的.
其中, 最快的协程(Sum from 4 to 14…)执行了 11 秒左右, 为什么是 11 秒左右呢?
说明它阻塞在了 Sum 函数中的第一行上, 等了 1 秒之后, main 函数开始读出 chan 中数据后才继续运行.
它自身运行需要 10 秒, 加上等待的 1 秒, 正好 11 秒左右.
最慢的协程执行了 15 秒左右, 这个也很好理解, 总共启动了 5 个协程, main 函数每隔 1 秒 读出一次 chan, 最慢的协程等待了 5 秒,
再加上自身执行了 10 秒, 所以一共 15 秒左右.
到这里, 我们很自然会想到能否增加 chan 的容量, 从而使得每个协程尽快执行, 完成自己的操作, 而不用等待, 消除由于 main 函数的处理所带来的瓶颈呢?
答案是当然可以, 而且在 golang 中实现还很简单, 只要在创建 chan 时, 指定 chan 的容量就行.
package main
import (
"fmt"
"time"
)
func main() {
var ch = make(chan string, 10)
for i := 0; i < 5; i++ {
go sum(i, i+10, ch)
}
for i := 0; i < 10; i++ {
time.Sleep(time.Second * 1)
fmt.Print(<-ch)
}
}
func sum(start, end int, ch chan string) int {
ch <- fmt.Sprintf("Sum from %d to %d is starting at %s\n", start, end, time.Now().String())
var sum int = 0
for i := start; i < end; i++ {
sum += i
}
time.Sleep(time.Second * 10)
ch <- fmt.Sprintf("Sum from %d to %d is %d at %s\n", start, end, sum, time.Now().String())
return sum
}
执行结果如下:
$ go run main.go
Sum from 0 to 10 is starting at 2015-10-13 14:22:14.64534265 +0800 CST
Sum from 2 to 12 is starting at 2015-10-13 14:22:14.645382961 +0800 CST
Sum from 3 to 13 is starting at 2015-10-13 14:22:14.645408947 +0800 CST
Sum from 4 to 14 is starting at 2015-10-13 14:22:14.645417257 +0800 CST
Sum from 1 to 11 is starting at 2015-10-13 14:22:14.645427028 +0800 CST
Sum from 1 to 11 is 55 at 2015-10-13 14:22:24.6461138 +0800 CST
Sum from 3 to 13 is 75 at 2015-10-13 14:22:24.646330223 +0800 CST
Sum from 2 to 12 is 65 at 2015-10-13 14:22:24.646325521 +0800 CST
Sum from 4 to 14 is 85 at 2015-10-13 14:22:24.646343061 +0800 CST
Sum from 0 to 10 is 45 at 2015-10-13 14:22:24.64634674 +0800 CST
从执行结果可以看出, 所有协程几乎都是 10秒完成的. 所以在使用协程时, 记住可以通过使用缓存来进一步提高并发性.
并发时的超时
并发编程, 由于不能确保每个协程都能及时响应, 有时候协程长时间没有响应, 主进程不可能一直等待, 这时候就需要超时机制.
在 golang 中, 实现超时机制也很简单.
package main
import (
"fmt"
"time"
)
func main() {
var ch = make(chan string, 1)
var timeout = make(chan bool, 1)
go sum(1, 10, ch)
go func() {
time.Sleep(time.Second * 5) // 5 秒超时
timeout <- true
}()
select {
case sum := <-ch:
fmt.Print(sum)
case <-timeout:
fmt.Println("Sorry, TIMEOUT!")
}
}
func sum(start, end int, ch chan string) int {
var sum int = 0
for i := start; i < end; i++ {
sum += i
}
time.Sleep(time.Second * 10)
ch <- fmt.Sprintf("Sum from %d to %d is %d\n", start, end, sum)
return sum
}
通过一个匿名函数来控制超时, 然后同时启动 计算 sum 的协程和timeout协程, 在 select 中看谁先结束,
如果 timeout 结束后, 计算 sum 的协程还没有结束的话, 就会进入超时处理.
上例中, timeout 只有5秒, sum协程会执行10秒, 所以执行结果如下:
$ go run main.go
Sorry, TIMEOUT!
修改 time.Sleep(time.Second * 5) 为 time.Sleep(time.Second * 15) 的话, 就会看到 sum 协程的执行结果
Golang 并发简介的更多相关文章
- golang并发编程
golang并发编程 引子 golang提供了goroutine快速实现并发编程,在实际环境中,如果goroutine中的代码要消耗大量资源时(CPU.内存.带宽等),我们就需要对程序限速,以防止go ...
- 马蜂窝搜索基于 Golang 并发代理的一次架构升级
搜索业务是马蜂窝流量分发的重要入口.很多用户在使用马蜂窝时,都会有目的性地主动搜索与自己旅行需求相关的各种信息,衣食住行,事无巨细,从而做出最符合需求的旅行决策. 因此在马蜂窝,搜索业务交互的下游模块 ...
- golang 并发顺序输出数字
参考 package main import ( "fmt" "sync/atomic" "time" ) func main() { va ...
- Golang并发原理及GPM调度策略(一)
其实从一开始了解到go的goroutine概念就应该想到,其实go应该就是在内核级线程的基础上做了一层逻辑上的虚拟线程(用户级线程)+ 线程调度系统,如此分析以后,goroutine也就不再那么神秘了 ...
- Golang - 并发编程
目录 Golang - 并发编程 1. 并行和并发 2. go语言并发优势 3. goroutine是什么 4. 创建goroutine 5. runtime包 6. channel是什么 7. ch ...
- golang 并发demo 写入 redis
原文链接:golang 并发demo 写入 redis 源代码: package main import ( "fmt" "runtime" "str ...
- 4种Golang并发操作中常见的死锁情形
摘要:什么是死锁,在Go的协程里面死锁通常就是永久阻塞了,你拿着我的东西,要我先给你然后再给我,我拿着你的东西又让你先给我,不然就不给你.我俩都这么想,这事就解决不了了. 本文分享自华为云社区< ...
- <转>golang 并发性能数据
1.管道chan吞吐极限10,000,000,单次Put,Get耗时大约100ns/op,无论是采用单Go程,还是多Go程并发(并发数:100, 10000, 100000),耗时均没有变化,Go内核 ...
- go/wiki/MutexOrChannel Golang并发:选channel还是选锁?
https://mp.weixin.qq.com/s/JcED2qgJEj8LaBckVZBhDA https://github.com/golang/go/wiki/MutexOrChannel M ...
随机推荐
- 数据可视化 seaborn绘图(2)
统计关系可视化 最常用的关系可视化的函数是relplot seaborn.relplot(x=None, y=None, hue=None, size=None, style=None, data=N ...
- C#通过虚方法实现方法重写—多态。
class Program { //希望person存的是哪个类的对象就调用哪个类的方法 //第一步 将父类中对应方法家virtual关键字 变为虚方法(子类可重写) //子类中方法用override ...
- ORACLE 配置连接远程数据库
ORACLE配置tnsnames.ora文件实例 客户机为了和服务器连接,必须先和服务器上的监听进程联络.ORACLE通过tnsnames.ora文件中的连接描述符来说明连接信息.一般tnsnames ...
- collect_list/collect_set(列转行)
Hive中collect相关的函数有collect_list和collect_set. 它们都是将分组中的某列转为一个数组返回,不同的是collect_list不去重而collect_set去重. 做 ...
- Laravel篇之Laravel的安装及使用
想搭建一个基于Lavarel和vuejs的个人网站,使用git推送到github存储,千里之行,始于足下,首先要做的用composer来下载laravel框架,并成功运行. 一.使用compose ...
- Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/hadoop/fs/CanUnbuffer
在执行spark on hive 的时候在 sql.show()处报错 : Exception in thread "main" java.lang.NoClassDefFoun ...
- H5新增特性、方法
1.FileReader和progress实现实时监控文件上传进度 2.HTML5新增的客户端校验 1.调用checkValidity方法进行校验 2.setCustomValidity自定义错误
- ATOM中MARKDOWN的使用小结
1.ctrl+shift+p ; 输入ssvvp ; 回车 2.输入markdown-preview进行安装 3.打开任意.md 文件 ; 按 ctrl-shift-m 进行预览
- cf1132G. Greedy Subsequences(线段树)
题意 题目链接 Sol 昨天没想到真是有点可惜了. 我们考虑每个点作为最大值的贡献,首先预处理出每个位置\(i\)左边第一个比他大的数\(l\),显然\([l + 1, i]\)内的数的后继要么是\( ...
- 前端整理——css部分
(1)盒模型(普通盒模型.怪异盒模型) 1.元素的content(内容).padding(内边距).border(边框).margin(外边距)构成了CSS盒模型 2.IE盒模型和W3C盒模型 1)I ...