前面已经讲过很多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. 世纪龙校招java开发一、二面 面经

    头一天做的笔试,涉及到计组基本知识,还有几道智力题.java部分很简单(真的很简单有点基础就划过了) 第二天收简历 在隔壁教室等 叫到你 你就去面试 一面:先自我介绍 1 == 和 equals区别( ...

  2. Selenium+java - Page Object设计模式

    前言 Page Object(页面对象)模式,是Selenium实战中最为流行,并且被自动化测试同学所熟悉和推崇的一种设计模式之一.在设计测试时,把页面元素定位和元素操作方法按照页面抽象出来,分离成一 ...

  3. Linux的crond和crontab

    一.crond cron是一个linux下的定时执行工具(相当于windows下的scheduled task),可以在无需人工干预的情况下定时地运行任务task. 由于cron 是Linux的ser ...

  4. Spring Cloud Stream 核心概念

    Spring Cloud Stream简介 Spring cloud stream是一个构建与Spring Boot和Spring Integration之上的框架,方便开发人员快速构建基于Messa ...

  5. 聊聊我在这家公司设计的SSO

    最近小明遇到一个需求:需要将几个独立的系统(子系统)汇总到一个集中的系统(父系统)当中,当用户在父系统登录过后,再点击这几个子系统,就可以免登录跳转到任意一个系统.当时一听,duang~duang~就 ...

  6. RSA加密的java实现2(交互IOS)

    这里的base64的依赖不一样,一个是apache,一个是sun的  ,由于base64的依赖不同,导致在IOS中解析不了! package com.handsight.platform.cipher ...

  7. Linux环境搭建 | 手把手教你安装Linux虚拟机

    前言 作为一名Linux工程师,不管是运维.应用.驱动方向,在工作中肯定会需要Linux环境.想要获得Linux环境,一个办法就是将电脑系统直接换成Linux系统,但我们平常用惯了Windows系统, ...

  8. 集合系列 List(三):Vector

    Vector 的底层实现以及结构与 ArrayList 完全相同,只是在某一些细节上会有所不同.这些细节主要有: 线程安全 扩容大小 线程安全 我们知道 ArrayList 是线程不安全的,只能在单线 ...

  9. CNN中1x1 卷积的处理过程及作用

    参看:https://blog.csdn.net/ybdesire/article/details/80314925

  10. centos 7 修改sshd | 禁止 root登录及脚本定义

    1.新建用户wwweee000 [root@localhost ~]# useradd wwweee000 [root@localhost ~]# passwd wwweee000 Changing ...