前面已经讲过很多Golang系列知识,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.html

接下来要说的是golang的并发,其实之前简单介绍过协程(goroutine)和管道(channel) 等基础内容,只是比较简单,只讲了基本的语法。今天就详细说说golang的并发编程。

一、并发和并行

Go是并发语言,而不是并行语言。所以我们在讨论,我们首先必须了解什么是并发,以及它与并行性有什么不同。

什么是并发

并发就是一段时间内处理许多事情。

比如,一个人在晨跑。在晨跑时,他的鞋带松了。现在这个人停止跑步,系鞋带,然后又开始跑步。这是一个典型的并发。这个人能够同时处理跑步和系鞋带,这是一个人能够同时处理很多事情。

什么是并行

并行就是同一时刻做很多事情。这听起来可能与并发类似,但实际上是不同的。

再比如,这个人正在慢跑,并且使用他的手机听音乐。在这种情况下,一个人一边慢跑一边听音乐,那就是他同时在做很多事情。这就是所谓的并行。

并发不是并行。并发更关注的是程序的设计层面,并发的程序完全是可以顺序执行的,只有在真正的多核CPU上才可能真正地同时运行。并行更关注的是程序的运行层面,并行一般是简单的大量重复,例如GPU中对图像处理都会有大量的并行运算。为更好的编写并发程序,从设计之初Go语言就注重如何在编程语言层级上设计一个简洁安全高效的抽象模型,让程序员专注于分解问题和组合方案,而且不用被线程管理和信号互斥这些繁琐的操作分散精力。
 
上图能清楚的说明了并发和并行的区别。
 

二、协程(Goroutines)

go中使用Goroutines来实现并发。Goroutines是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小。因此,Go应用程序可以并发运行数千个Goroutines。

Goroutines在线程上的优势。

  1. 与线程相比,Goroutines非常便宜。它们只是堆栈大小的几个kb,堆栈可以根据应用程序的需要增长和收缩,而在线程的情况下,堆栈大小必须指定并且是固定的

  2. Goroutines被多路复用到较少的OS线程。在一个程序中可能只有一个线程与数千个Goroutines。如果线程中的任何Goroutine都表示等待用户输入,则会创建另一个OS线程,剩下的Goroutines被转移到新的OS线程。所有这些都由运行时进行处理,我们作为程序员从这些复杂的细节中抽象出来,并得到了一个与并发工作相关的干净的API。

  3. 当使用Goroutines访问共享内存时,通过设计的通道可以防止竞态条件发生。通道可以被认为是Goroutines通信的管道。

如何使用Goroutines

在函数或方法调用前面加上关键字go,您将会同时运行一个新的Goroutine。

实例代码:

package main

import (
"fmt"
"time"
) func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
time.Sleep( * time.Second)
fmt.Println("main function")
}
运行结果:
Hello world goroutine
main function

如何启动多个Goroutines

示例代码:

package main

import (
"fmt"
"time"
) func numbers() {
for i := ; i <= ; i++ {
time.Sleep( * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func alphabets() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep( * time.Millisecond)
fmt.Printf("%c ", i)
}
}
func main() {
go numbers()
go alphabets()
time.Sleep( * time.Millisecond)
fmt.Println("main terminated")
}
运行结果: a b c d e main terminated

Goroutine切换

下面通过素数计算的例子来说明goland是如何通过切换不同的goroutine实现并发的。

package main

import (
"fmt"
"runtime"
"sync"
) var wg sync.WaitGroup func main() { runtime.GOMAXPROCS(1) wg.Add(2)
go printPrime("A")
go printPrime("B") fmt.Println("Wait for finish")
wg.Wait()
fmt.Println("Program End")
} func printPrime(prefix string) {
defer wg.Done()   nextNum:
for i := 2; i < 6000; i++ {
for j := 2; j < i; j++ {
if i%j == 0 {
continue nextNum
}
}
fmt.Printf("%s:%d\n", prefix, i)
}
fmt.Printf("complete %s\n", prefix)
}
运行结果:
Wait for finish
B:2
B:3
B:5
B:7
B:11
...
B:457
B:461
B:463
B:467
A:2
A:3
A:5
A:7
...
A:5981
A:5987
complete A
B:5939
B:5953
B:5981
B:5987
complete B
Program End

通过以上的输出结果,可以看出两个Goroutine是在一个处理器上通过切换goroutine实现并发执行。

三、通道(channels)

通道可以被认为是Goroutines通信的管道。类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收。

声明通道

每个通道都有与其相关的类型。该类型是通道允许传输的数据类型。(通道的零值为nil。nil通道没有任何用处,因此通道必须使用类似于地图和切片的方法来定义。)

示例代码:

package main

import "fmt"

func main() {
var a chan int
if a == nil {
fmt.Println("channel a is nil, going to define it")
a = make(chan int)
fmt.Printf("Type of a is %T", a)
}
}
运行结果: channel a is nil, going to define it
Type of a is chan int

也可以简短的声明:

a := make(chan int)

发送和接收

发送和接收的语法:

data := <- a   // read from channel a
a <- data // write to channel a

在通道上箭头的方向指定数据是发送还是接收。

一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。类似地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。

这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。

示例代码:

package main

import (
"fmt"
"time"
) func hello(done chan bool) {
fmt.Println("hello go routine is going to sleep")
time.Sleep( * time.Second)
fmt.Println("hello go routine awake and going to write to done")
done <- true
}
func main() {
done := make(chan bool)
fmt.Println("Main going to call hello go goroutine")
go hello(done)
<-done
fmt.Println("Main received data")
}
运行结果:

 Main going to call hello go goroutine
 hello go routine is going to sleep
 hello go routine awake and going to write to done
 Main received data

定向通道

之前我们学习的通道都是双向通道,我们可以通过这些通道接收或者发送数据。我们也可以创建单向通道,这些通道只能发送或者接收数据。

创建仅能发送数据的通道,示例代码:

package main

import "fmt"

func sendData(sendch chan<- int) {
sendch <-
} func main() {
sendch := make(chan<- int)
go sendData(sendch)
fmt.Println(<-sendch)
}
报错:

 # command-line-arguments
 .\main.go:12:14: invalid operation: <-sendch (receive from send-only type chan<- int)

示例代码:

package main

import "fmt"

func sendData(sendch chan<- int) {
sendch <-
} func main() {
chnl := make(chan int)
go sendData(chnl)
fmt.Println(<-chnl)
}
运行结果:
10

死锁

为什么会死锁?非缓冲信道上如果发生了流入无流出,或者流出无流入,也就导致了死锁。或者这样理解 Go启动的所有goroutine里的非缓冲信道一定要一个线里存数据,一个线里取数据,要成对才行 。

示例代码:

package main

func main() {
c, quit := make(chan int), make(chan int) go func() {
c <- 1 // c通道的数据没有被其他goroutine读取走,堵塞当前goroutine
quit <- 0 // quit始终没有办法写入数据
}() <-quit // quit 等待数据的写
}
报错:
fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan send]:
main.main()
/tmp/sandbox249677995/main.go:11 +0x80

关闭通道

关闭通道只是关闭了向通道写入数据,但可以从通道读取。

package main

import (
"fmt"
) var ch chan int = make(chan int, ) func main() {
ch <-
ch <-
ch <- close(ch) for v := range ch {
fmt.Println(v)
}
}

四、缓冲通道

之前学习的所有通道基本上都没有缓冲。发送和接收到一个未缓冲的通道是阻塞的。

可以用缓冲区创建一个通道。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。

可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小。

语法:

ch := make(chan type, capacity)

上述语法的容量应该大于0,以便通道具有缓冲区。默认情况下,无缓冲通道的容量为0,因此在之前创建通道时省略了容量参数。

示例代码:

func main() {
done := make(chan int, 1) // 带缓存的管道 go func(){
fmt.Println("你好, 世界")
done <- 1
}() <-done
}

五、最后

以上,就把golang并发编程相关的内容介绍完了,希望能对大家有所帮助。

Golang 入门系列(十五)如何理解go的并发?的更多相关文章

  1. Golang 入门系列(十) mysql数据库的使用

    之前,已经讲过一些Golang的基础的东西,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.html, 今天简 ...

  2. Golang 入门系列(五)GO语言中的面向对象

    前面讲了很多Go 语言的基础知识,包括go环境的安装,go语言的语法等,感兴趣的朋友可以先看看之前的文章.https://www.cnblogs.com/zhangweizhong/category/ ...

  3. Golang 入门系列(六)理解Go中的协程(Goroutine)

    前面讲的都是一些Go 语言的基础知识,感兴趣的朋友可以先看看之前的文章.https://www.cnblogs.com/zhangweizhong/category/1275863.html. 今天就 ...

  4. 无废话ExtJs 入门教程十五[员工信息表Demo:AddUser]

    无废话ExtJs 入门教程十五[员工信息表Demo:AddUser] extjs技术交流,欢迎加群(201926085) 前面我们共介绍过10种表单组件,这些组件是我们在开发过程中最经常用到的,所以一 ...

  5. Bootstrap入门(十五)组件9:面板组件

    Bootstrap入门(十五)组件9:面板组件 虽然不总是必须,但是某些时候你可能需要将某些 DOM 内容放到一个盒子里.对于这种情况,可以试试面板组件. 1.基本实例 2.带标题的面板 3.情景效果 ...

  6. MyBatis基础入门《十五》ResultMap子元素(collection)

    MyBatis基础入门<十五>ResultMap子元素(collection) 描述: 见<MyBatis基础入门<十四>ResultMap子元素(association ...

  7. RabbitMQ入门教程(十五):普通集群和镜像集群

    原文:RabbitMQ入门教程(十五):普通集群和镜像集群 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.c ...

  8. Angular2入门系列(五)———— 路由参数设置

    Angular2入门系列(五)---- 路由参数设置路由配置: { path: '', component: CarProFile, children: [ { path: 'add', compon ...

  9. 聊聊MySQL的加锁规则《死磕MySQL系列 十五》

    大家好,我是咔咔 不期速成,日拱一卒 本期来聊聊MySQL的加锁规则,知道这些规则后可以判断SQL语句的加锁范围,同时也可以写出更好的SQL语句,防止幻读问题的产生,在能力范围内最大程度的提升MySQ ...

  10. 学习ASP.NET Core Razor 编程系列十五——文件上传功能(三)

    学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...

随机推荐

  1. 002——Netty之Netty介绍

    Netty出现背景 Java NIO难用 据说存在bug 业界其他NIO框架不成熟 Netty主要解决两个相应关注领域 (1)异步和事件驱动的实现. (2)一组设计模式,将应用逻辑与网络层解耦. 特性 ...

  2. nginx 使用HTTPS协议-SSL证书模块报错解决-附nginx安装 : [emerg] the "ssl" parameter requires ngx_http_ssl_module in nginx.c

    Linux系统下ngnix使用HTTPS协议启动报错: nginx: [emerg] the "ssl" parameter requires ngx_http_ssl_modul ...

  3. JS之null与undefined的区别

    null表示尚未存在的对象 js 代码: alert(null == document.getElementById('notExistElement'));  //output "true ...

  4. Windows Server 2008在网络环境配置打印机

    下面学习在Windows Server2008在网络环境搭建打印机服务器,打印机服务器也是很常用的,特别是在中大型企业里面,打印机数量比较多为方便管理,可以搭建一个打印机服务,这里介绍一下,本地打印机 ...

  5. 定时清理docker私服镜像

    定时清理docker私服镜像 使用CI构建docker镜像进行发布极大促进了大家的版本发布效率,于是镜像仓库也就急速膨胀.为了缓解磁盘压力,我们需要设置一些清理策略. 对于不同docker镜像的清理策 ...

  6. Jetbrains好用的插件(经验总结)

    # Jetbrain好用的插件(经验总结) 笔者使用过idea,phpstorm,webstorm,pycharm和goland开发过项目,不得不说,Jetbrains的编辑器每一款都挺好用的,而且快 ...

  7. js的兼容问题以及解决方式(持续更新)

    我们在使用js操作页面的时候兼容问题是很常见的,下面将常见的兼容问题及其对应的解决方法分享给大家,并持续更新: 1.获取事件对象的兼容写法: IE中:window.event     正常浏览器中:对 ...

  8. Leetcode之二分法专题-278. 第一个错误的版本(First Bad Version)

    Leetcode之二分法专题-278. 第一个错误的版本(First Bad Version) 你是产品经理,目前正在带领一个团队开发新的产品.不幸的是,你的产品的最新版本没有通过质量检测.由于每个版 ...

  9. Delphi - 本地路径的创建、清空本地指定文件夹下的文件

    本地路径的创建 在做下载操作时,我们一般先把文件下载到本地指定的路径下,然后再做其他使用. 为了防止程序出现异常,我们通常需要先判断本地是否存在指定的路径. 以C盘Tmp文件夹为例,我们可以这样做,代 ...

  10. JDK中的Proxy技术实现AOP功能

    1.需求,用户在执行某个业务方法时我们需要对这个用户进行判断是否具有权限(或者说用户是否登录了)? 例如修改用户信息,我们在update()方法前判断当前用户user是否为null(表示没有权限或者没 ...