通常程序会被编写为一个顺序执行并完成一个独立任务的代码。

如果没有特别的需求,最好总是这样写代码,因为这种类型的程序通常很容易写,也容易维护。

不过也有一些情况下,并行执行多个任务会有更大的好处。

一个例子是,Web服务需要在各自独立的套接字上同时接收多个数据请求。

每个套接字请求都是独立的,可以完全独立于其它套接字进行处理。

具有并行执行多个请求的能力可以显著提高这类系统的性能。

考虑到这一点,Go语言的语法和运行时直接内置了对并发的支持。

Go语言里的并发指的是能让某个函数独立于其它函数运行的能力

当一个函数创建为goroutine时,Go会将其视为一个独立的工作单元

这个单元会被调度到可用的逻辑处理器上执行。

Go语言运行时的调度器是一个复杂的软件,能管理被创建的所有goroutine并为其分配执行时间

这个调度器在操作系统之上,将操作系统的线程与语言运行时的逻辑处理器绑定,并在逻辑处理器上运行goroutine

调度器在任何给定的时间,都会全面控制哪个goroutine要在哪个逻辑处理器上运行。

Go语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes,CSP)的范型(paradigm)。

CSP是一种消息传递模型,通过在goroutine之间传递数据来传递消息,而不是对数据进行加锁来同步访问

用于在goroutine同步和传递数据的关键数据类型叫做通道(channel)

使用通道可以时编写并发程序更容易,也能够让并发程序出错更少。

1.并发与并行

当运行一个应用程序(如一个IDE或者编辑器)的时候,操作系统会为这个应用程序启动一个进程。

可以将这个进程看作包含了一个应用程序在运行中需要用到和维护的各种资源的容器

上图展示了一个包含所有可能分配的常用资源的进程。

这些资源包括但不限于内层地址空间、文件和设备句柄以及线程。

一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码。

每个进程至少有一个线程,每个线程的初始线程被称作主线程

因为执行这个线程的空间是应用程序的本身空间,所以当主线程终止时,应用程序也会终止。

操作系统将线程调度到某个处理器上运行,这个处理器并不一定是进程所在的处理器。

不同操作系统使用的线程调度算法一般不一样,但这种不同会被操作系统屏蔽,并不会展示给程序员。

操作系统会在物理处理器上调度线程来运行,而Go语言在运行时会在逻辑处理器上调度goroutine来运行

每个逻辑处理器都分别绑定到单个操作系统线程

在1.5版本上,Go在运行默认会为每个可用的物理处理器分配一个逻辑处理器。

在1.5之前的版本中,默认给整个应用程序只分配一个逻辑处理器。

这些逻辑处理器会用于执行所有被创建的goroutine

即便只有一个逻辑处理器,Go也可以以神奇的效率和性能并发调度无数个goroutine。

在上图中展示了操作系统线程、逻辑处理器和本地运行队列之间的关系。

如果创建一个gotoutine并准备运行,这个gotoutine就会被放到调度器的全局运行队列中。

之后,调度器就会将这些队列中的goroutine分配给逻辑处理器,并放到这个逻辑处理器对应的本地运行队列中。

本地运行队列中的goroutine会一直等待直到自己被分配的逻辑处理器执行。

有时,正在运行的goroutine需要执行一个阻塞的系统调用,如打开一个文件。

当这类调用发生时,线程和goroutine会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用的返回

于此同时,这个逻辑处理器就失去了用来运行的线程。所以,调度器会创建一个新线程,并将其绑定到该逻辑处理器上。

之后,调度器会从本地运行队列里选择另一个goroutine来运行

一旦被阻塞的系统调用执行完成并返回,对应的goroutine会放回到本地运行队列,而之前的线程会保存好,以便之后可以继续使用。

如果一个goroutine需要做一个网络I/O调用,流程上会有些不一样。

在这种情况下,goroutine会和逻辑处理器分离,并移到继承了网络轮询器的运行时

一旦该轮询器指示了某个网络读或者写操作已经就绪,对应的goroutine就会重新分配到逻辑处理器上来完成操作。

调度器对可以创建的逻辑处理器的数量没有限制,但语言运行时默认限制每个程序最多创建10000个线程。

这个限制值可以通过调用runtime/debug包的SetMaxThreads方法来更改。如果程序试图使用更多的线程,就会崩溃。

并发不是并行。并行是让不同的代码片段同时在不同的物理处理器上执行

并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一般就被暂停去做别的事情去了。

在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。

这种“使用较少的资源做更多的事情”的哲学,也是Go语言设计的哲学。

如果希望让goroutine并行,必须使用多于一个逻辑处理器。

当有多个逻辑处理器时,调度器会将goroutine平等分配到每个逻辑处理器上。这会让goroutine在不同的线程上运行。

不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。

否则,哪怕Go语言运行时使用了多个线程,goroutine依然会在同一个物理处理器上并发执行,达不到并行的效果

上图展示了在一个逻辑处理器上并发运行goroutine和在两个逻辑处理器上并行运行两个并发的goroutine之间的区别。

2.goroutine

示例1:创建goroutine

//这个程序展示如何创建goroutine
//以及调度器的行为
package main import (
"fmt"
"runtime"
"sync"
) //main是程序的入口
func main() {
//分配一个逻辑处理器给调度器使用
runtime.GOMAXPROCS(1) //wg用来等待程序完成
//通过设定计数器,让每个goroutine在退出前递减,直至归零时解除阻塞。
var wg sync.WaitGroup
//计数加2,表示要等待两个goroutine
wg.Add(2) fmt.Println("Start Goroutines") //声明一个匿名函数,并创建一个goroutine
go func() {
//在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done() //显式字母表三次
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c ", char)
}
}
}() go func() {
defer wg.Done() for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c ", char)
}
}
}() fmt.Println("waiting To finish")
//等待goroutine结束
wg.Wait() fmt.Println("\nTerminating Program")
}

结果展示:

Start Goroutines
waiting To finish
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
a b c d e f g h i j k l m n o p q r s t u v w x y z
Terminating Program

第一个goroutine完成所有显式所花费的时间太短,以至于在调度器切换到第二个goroutine之前,就完成了所有任务。

GOMAXPROCS函数允许程序更改调度器可以使用的逻辑处理器的数量。

WaitGroup是一个计数信号量,可以用来记录并维护运行的goroutine。如果WaitGroup的值大于0,那么Wait方法就会阻塞。

defer保证每个goroutine一旦完成其工作就调用Done方法。

基于调度器的内部算法,一个正运行的goroutine在工作结束前,可以被停止并重新调度。

调度器这样做的目的是防止某个goroutine长时间占用逻辑处理器。

当goroutine占用时间过长时,调度器会停止当前正运行的goroutine,并给其它可运行的goroutine运行的机会。

上图从逻辑处理器的角度展示了这一场景。在第1步,调度器开始运行goroutine A,而goroutine B在运行队列里等待调度。

之后,在第2步,调度器交换了goroutine A和goroutine B,由于goroutine A并没有完成工作,因此被放回到运行队列。

之后,在第3步,goroutine B完成了它的工作并被销毁。这也让goroutine A继续之前的工作。

可以通过创建一个需要长时间才能完成的其工作的goroutine来看到这个行为。

package main

import (
"fmt"
"runtime"
"sync"
) var wg sync.WaitGroup func main() {
runtime.GOMAXPROCS(1) wg.Add(2) fmt.Println("create goroutine")
go printPrime("A")
go printPrime("B") fmt.Println("waiting to finish")
wg.Wait()
fmt.Println("Terminating program") } func printPrime(prefix string) {
defer wg.Done() next:
for outer := 2; outer < 5000; outer++ {
for inner := 2; inner < outer; inner++ {
if outer%inner == 0 {
continue next
}
}
fmt.Printf("%s:%d\n", prefix, outer)
}
fmt.Println("completed", prefix)
} /*
create goroutine
waiting to finish
B:2
B:3
...
B:4787
B:4789
A:2 //切换goroutine
A:3
...
A:3919
A:3923
B:4793 //切换goroutine
B:4799
...
B:4993
B:4999
completed B
A:3929 //切换goroutine
A:3931
...
A:4999
completed A
Terminating program
*/

  

3.竞争状态

如果两个或者多个goroutine在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争状态。

竞争状态的存在是让并发程序变得复杂的地方,十分容易引起潜在的问题。

对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个goroutine对共享资源进行读和写操作。

//如何在程序里造成竞争状态
//实际上不希望出现这种情况
package main import (
"fmt"
"runtime"
"sync"
) var (
//counter是所有goroutine都要增加其值的变量
counter int //wg用来等待程序结束
wg sync.WaitGroup
) func main() {
//计数加2,要等待两个goroutine
wg.Add(2) //创建两个goroutine
go inCounter(1)
go inCounter(2) //等待goroutine结束
wg.Wait()
fmt.Println("Final Counter:", counter)
} //inCounter增加包里counter变量的值
func inCounter(id int) {
//在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done() for count := 0; count < 2; count++ {
//捕获counter的值
value := counter //当前goroutine从线程退出,并放回队列
runtime.Gosched() //增加本地value变量的值
value++ //将值保存回counter
counter = value }
}  

结果:

Final Counter: 2

每个goroutine都会覆盖另一个goroutine的工作。这种覆盖发生在goroutine切换的时候。

一种修正代码、消除竞争状态的办法是,使用Go语言提供的锁机制,来锁住共享资源,从而保证goroutine的同步状态。

4.锁住共享资源

Go语言提供了传统的同步goroutine的机制,就是对共享资源加锁。

如果需要顺序访问一个整型变量或者一段代码,atomic和sync包里的函数提供了很好的解决方法。

(1)原子函数

//展示如何使用atomic包来提供对数值类型的安全访问
package main import (
"fmt"
"runtime"
"sync"
"sync/atomic"
) var (
//counter是所有goroutine都要增加其值的变量
counter int64 //wg用来等待程序结束
wg sync.WaitGroup
) func main() {
//计数加2,要等待两个goroutine
wg.Add(2) //创建两个goroutine
go inCounter(1)
go inCounter(2) //等待goroutine结束
wg.Wait()
fmt.Println("Final Counter:", counter)
} //inCounter增加包里counter变量的值
func inCounter(id int) {
//在函数退出时调用Done来通知main函数工作已经完成
defer wg.Done() for count := 0; count < 2; count++ {
atomic.AddInt64(&counter, 1) runtime.Gosched()
}
} /*
结果:
Final Counter: 4
*/

  

AddInt64这个函数会同步整型值得加法,,方法是强制同一时刻只能有一个goroutine运行并完成这个加法操作。

当goroutine试图去调用任何原子函数时,这些goroutine都会自动根据所引用得变量做同步处理。

另外两个有用得原子函数是LoadInt64和StoreInt64。这两个函数提供了一种安全地读和写一个整型值的方式。

//这个示例程序展示如何使用atomic包里的Store和Load类型函数来提供对数值类型的安全访问。
package main import (
"fmt"
"sync"
"sync/atomic"
"time"
) var (
//shutdown是通知正在执行的goroutine停止工作的标志
shutdown int64 //wg用来等待程序结束
wg sync.WaitGroup
) func main() { wg.Add(2) //创建两个goroutine
go doWork("A")
go doWork("B") //给定goroutine执行时间
time.Sleep(1 * time.Second) //该停止工作了,安全设置shutdown标志
fmt.Println("Shutdown Now")
atomic.StoreInt64(&shutdown, 1) wg.Wait()
} //doWork用来模拟执行工作的goroutine
//检测之前的shutdown标志来决定是否提前终止
func doWork(name string) {
defer wg.Done() for {
fmt.Printf("Doing %s Work\n", name)
time.Sleep(250 * time.Millisecond) //要停止工作了吗?
if atomic.LoadInt64(&shutdown) == 1 {
fmt.Printf("Shutting %s Down\n", name)
break
}
}
}

  

在上面这个例子中,启动了两个goroutine,并完成了一些工作。在各自循环的每次迭代之后,goroutine会使用LoadInt64来检查shutdown变量的值。

这个函数会安全地返回shutdown变量地一个副本。如果这个副本地值为1,goroutine就会跳出循环并终止。

main函数使用StoreInt64函数来安全地修改shutdown变量地值。如果哪个doWork goroutine试图在main函数调用StoreInt64的同时调用LoadInt64函数,

那么原子函数会将这些调用互相同步,保证这些操作都是安全的,不会进入竞争状态。

(2)互斥锁

另一种同步访问共享资源的方式是使用互斥锁(mutex)。

互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码。

//展示互斥锁,定义临界区
package main import (
"fmt"
"runtime"
"sync"
) var (
counter int
wg sync.WaitGroup
//mutex用来定义一段代码临界区
mutex sync.Mutex
) func main() {
wg.Add(2) go inCounter(1)
go inCounter(2) wg.Wait()
fmt.Println("Final Counter:", counter)
} func inCounter(id int) { defer wg.Done() for count := 0; count < 2; count++ {
//同一时刻只允许一个goroutine进入
//定义临界区
mutex.Lock()
{
value := counter runtime.Gosched() value++ counter = value
}
mutex.Unlock()
//释放锁,允许其它正在等待的goroutine进入临界区
}
} /*
结果:
Final Counter: 4
*/

函数Lock()和Unlock()定义临界区,会将其中的代码保护起来。

go——并发(二)的更多相关文章

  1. Java并发(二):基础概念

    并发编程的第二部分,先来谈谈发布(Publish)与逸出(Escape); 发布是指:对象能够在当前作用域之外的代码中使用,例如:将对象的引用传递到其他类的方法中,对象的引用保存在其他类可以访问的地方 ...

  2. java多线程与线程并发二:线程互斥

    本文章内容整理自:张孝祥_Java多线程与并发库高级应用视频教程 当两条线程访问同一个资源时,可能会出现安全隐患.以打印字符串为例,先看下面的代码: // public class Test2 { p ...

  3. 并发(二)CyclicBarrier

    CyclicBarrier 循环屏障,用于一组固定数目的线程互相等待.使用场景如下: 主任务有一组串行的执行节点,每个节点之间有一批任务,固定数量的线程执行这些任务,执行完成后,在节点完成集合后,再继 ...

  4. 流畅python学习笔记第十八章:使用asyncio包处理并发(二)

    前面介绍了asyncio的用法.下面我们来看下如何用协程的方式来实现之前的旋转指针的方法 @asyncio.coroutine def spin(msg): write,flush=sys.stdou ...

  5. java并发(二):初探syncronized

    参考博客 Java多线程系列--"基础篇"04之 synchronized关键字 synchronized基本规则 第一条 当线程访问A对象的synchronized方法和同步块的 ...

  6. java 关于多线程高并发方面

    转有关的文章链接: Java 高并发一:前言: http://www.jb51.net/article/92358.htm Java 高并发二:多线程基础详细介绍 http://www.jb51.ne ...

  7. Innodb 实现高并发、redo/undo MVCC原理

    一.并发控制   因为并发情况下有可能出现不同线程对同一资源进行变动,所以必须要对并发进行控制以保证数据的同一与安全.   可以参考CPython解释器中的GIL全局解释器锁,所以说python中没有 ...

  8. TCP与UDP比较 以及并发编程基础知识

    一.tcp比udp真正可靠地原因 1.为什么tcp比udp传输可靠地原因: 我们知道在传输数据的时候,数据是先存在操作系统的缓存中,然后发送给客户端,在客户端也是要经过客户端的操作系统的,因为这个过程 ...

  9. nginx 多进程 + io多路复用 实现高并发

    一.nginx 高并发原理 简单介绍:nginx 采用的是多进程(单线程) + io多路复用(epoll)模型 实现高并发 二.nginx 多进程 启动nginx 解析初始化配置文件后会 创建(for ...

  10. Python并发编程内容回顾

    Python并发编程内容回顾 并发编程小结 目录 • 一.到底什么是线程?什么是进程? • 二.Python多线程情况下: • 三.Python多进程的情况下: • 四.为什么有这把GIL锁? • 五 ...

随机推荐

  1. a5调试

    1 generating rsa key...[    4.452000] mmc0: error -110 whilst initialising SD card[    5.602000] mmc ...

  2. Make Docker Image On Ubuntu17.10

    1.拉取基础镜像 docker pull ubuntu 2.查看镜像 docker images 3.启动一个容器 docker run -it ubuntu 4.查找运行的容器ID docker p ...

  3. 同学帮帮 h5 刮刮卡组件:Txbb.Scratch

    同学帮帮 h5 刮刮卡组件,简洁.无依赖,支持 globals 和 amd 两种调用方式. 暂时只能用在移动端 使用方法 <div id="J-Scratch">< ...

  4. YARN源码分析(一)-----ApplicationMaster

    转自:http://blog.csdn.net/androidlushangderen/article/details/48128955 YARN学习系列:http://blog.csdn.net/A ...

  5. <!>表格语法

    <table aling=left>...</table>表格位置,置左 <table aling=center>...</table>表格位置,置中 ...

  6. Hive数据类型与文件存储格式

    Hive数据类型 基础数据类型: TINYINT,SMALLINT,INT,BIGINT,BOOLEAN,FLOAT,DOUBLE,STRING,BINARY,TIMESTAMP,DECIMAL,CH ...

  7. WPF之路——用户控件对比自定义控件UserControl VS CustomControl)

    将多个现有的控件组合成一个可重用的“组”. 由一个XAML文件和一个后台代码文件. 不能使用样式和模板. 继承自UserControl类. 自定义控件(扩展) 在现有的控件上进行扩展,增加一些新的属性 ...

  8. Android中的Manifest.permission(应用权限)整理

    ACCESS_CHECKIN_PROPERTIES 允许读/写登记数据库(checkin database),中的“properties”表,用来改变他的值来上传东西. 这个权限第三方应用无法使用. ...

  9. jQuery的validate验证插件使用方法

    (1)默认校验规则(1)required:true 必输字段(2)remote:"check.php" 使用ajax方法调用check.php验证输入值(3)email:true ...

  10. jpa双向一对一关联外键映射

    项目结构: Wife package auth.model; import javax.persistence.CascadeType; import javax.persistence.Column ...