并发与并行的区别:
并发:逻辑上具备同时处理多个任务的能力
并行:物理上在同一时刻执行多个并发任务
通常都会说程序是并发设计的,也就是说它允许多个任务同时执行,但实际上并不一定真在同一时刻发生。
在单核处理器上,它们能以间隔方式切换执行
并行则依赖多核处理器等物理设备,让多个任务真正在同一时刻执行,它代表了当前程序运行状态。
简单点说,并行是并发设计的理想执行模式。
多线程或多进程是并行的基本条件,但单线程也可用协程(coroutine)做到并发
尽管协程在单个线程上通过主动切换来实现多任务并发,但它也有自己的优势。
除了将因阻塞而浪费的时间找回来外,还免去了线程切换开销,有着不错的执行效率。
协程上运行的多个任务本质上依旧串行,加上可控自主调度,所以并不需要做同步处理。
即使采用多线程也未必能并行。Python就因GIL限制,默认只能并发而不能并行,所以很多时候转而使用“多进程”+“协程”架构。
通常情况下,用多进程来实现分布式和负载均衡,减轻单进程垃圾回收压力;
用多线程(LWP)抢夺更多的处理器资源;用协程来提高处理器时间片利用率。

简单将goroutine归纳为协程并不合适。运行时会创建多个线程来执行并发任务,且任务单元可被调度到其它线程并行执行。
这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。
只须在函数调用前添加go关键字即可创建并发任务。

go fmt.PrintLn("hello")

关键字go并非执行并发操作,而是创建一个并发任务单元。
新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。
当前流程不会阻塞,不会等待该任务启动,且运行时也不保证并发任务的执行次序。

每个任务单元除保存函数指针、调用参数外,还会分配执行所需的栈内存空间。
相比系统默认MB级别的线程栈,goroutine自定义栈初始仅须2KB,所以才能创建成千上万的并发任务。
自定义栈采取按需分配策略,在需要时进行扩容,最大能到GB规模。

与defer一样,goroutine也会因“延迟执行”而立即计算并复制执行参数。

package main

import (
"fmt"
"time"
) var c int //初始为0 func counter() int {
c++
return c
} func main() {
a := 100 go func(x, y int) { //匿名函数直接执行
//hour min second 时分秒
time.Sleep(time.Second) //休息一秒钟
fmt.Println("go", x, y) //执行counter 100 1 //goroutine在main之后执行
}(a, counter()) //立即计算并复制参数 a += 100
fmt.Println("main:", a, counter()) //200 2
time.Sleep(time.Second * 3) //等待goroute结束 } /*
main: 200 2
go 100 1
为什么先打印main?
go创建一个并发单元,但是不会马上执行,而是会放置在队列中
*/

进程退出时不会等待并发任务结束,可用通道(channel)阻塞,然后发出退出信号。

package main

import (
"fmt"
"time"
) func main() {
// exit := make(chan struct{}) //创建通道。因为仅仅是通知,数据并没有实际意义。 go func() {
time.Sleep(time.Second)
fmt.Println("goroutine done.") // close(exit) //关闭通道发出信号
}() fmt.Println("main...")
// <-exit //如通道关闭,立即解除阻塞。
fmt.Println("main exit...")
}

<-exit接收操作符,如果exit代表了元素类型为byte的通道类型值,则此表达式就表示从exit中接收一个byte类型值的操作。
如果不创建通道,main直接结束之后进程就结束了,而不会等待并发任务的结束,这样就不会执行并发任务。
所以,我们可以创建一个通道,当通道关闭后才会解除阻塞,整个程序才会结束

除关闭通道外,写入数据也可解除阻塞。
如果等待多个任务结束,推荐使用sync.WaitGroup。
通过设定计数器,让每个goroutine在退出前递减,直至归零时解除阻塞。

package main

import (
"fmt"
"sync"
"time"
) func main() {
var wg sync.WaitGroup for i := 0; i < 10; i++ {
wg.Add(1) //累加计数,每循环以此加一 go func(id int) { //创建多个任务单元
defer wg.Done() //递减计数,这个任务计数之前减一 time.Sleep(time.Second)
fmt.Println("goroutine", id, "done")
}(i)
} fmt.Println("main...")
wg.Wait() //阻塞,直至wg归零
fmt.Println("main exit")
} /*
main...
goroutine 6 done
goroutine 1 done
goroutine 2 done
goroutine 4 done
goroutine 9 done
goroutine 8 done
goroutine 0 done
goroutine 7 done
goroutine 5 done
goroutine 3 done
main exit
*/

  

尽管WaitGroup.Add实现了原子操作,但建议在goroutine外累加计数器,以免Add尚未执行,wait已经退出。

go func(id int) {
wg.Add(1)
defer wg.Done()
}

  

可在多处使用wait阻塞,它们都可以接收到通知。

package main

import (
"fmt"
"sync"
"time"
) func main() {
var wg sync.WaitGroup
wg.Add(1) go func() {
wg.Wait()
fmt.Println("wait exit")
}() go func() {
time.Sleep(time.Second * 3)
fmt.Println("done.")
wg.Done()
}() wg.Wait()
fmt.Println("main exit.")
} /*
done.
wait exit
main exit.
*/

  

GOMAXPROCS 线程数量
运行时可能会创建很多线程,但任何时候仅有限的几个线程参与并发任务执行。
该数量默认与处理器核数相等,可用runtime.GOMAXPROCS函数(或环境变量)修改。

package main

import (
"fmt"
"math"
"runtime"
"sync"
"time"
) //测试目标函数
func count() {
x := 0
for i := 0; i < math.MaxUint32; i++ {
x += 1
} fmt.Println(x)
} //循环执行
func test(n int) {
for i := 0; i < n; i++ {
count()
}
} //并发执行
func test2(n int) {
var wg sync.WaitGroup
wg.Add(n) //计数为4 for i := 0; i < n; i++ {
go func() {
count()
wg.Done()
}()
} wg.Wait()
} func main() {
n := runtime.GOMAXPROCS(0) //4
fmt.Println(time.Now())
// test(n) //6s
test2(n) //3s
fmt.Println(time.Now())
}

  

Local Storage 局部存储器
与线程不同,goroutine任务无法设置优先级,无法获取编号,没有局部存储(TLS),甚至连返回值都会被抛弃。

package main

import (
"fmt"
"sync"
) func main() {
var wg sync.WaitGroup
var gs [5]struct { //使用数组来进行局部存储
id int //编号
result int //返回值
}
fmt.Println(gs)
for i := 0; i < len(gs); i++ {
wg.Add(1) go func(id int) {
defer wg.Done() gs[id].id = id
gs[id].result = (id + 1) * 100
}(i)
} wg.Wait()
fmt.Printf("%+v\n", gs)
} /*
[{0 0} {0 0} {0 0} {0 0} {0 0}]
[{id:0 result:100} {id:1 result:200} {id:2 result:300} {id:3 result:400} {id:4 result:500}]
*/

  

如果使用map作为局部存储容器,建议做同步处理,因为运行时会对其做并发读写检查。

Gosched
暂停,释放线程去执行其它任务。当前任务被放回队列,等待下次调度时恢复执行

package main

import (
"fmt"
"runtime"
) func main() {
runtime.GOMAXPROCS(1)
exit := make(chan struct{}) go func() { /任务a
defer close(exit) go func() { //任务b
fmt.Println("b")
}() for i := 0; i < 4; i++ {
fmt.Println("a:", i) if i == 1 {
runtime.Gosched() //让出当前线程,调度执行b
}
}
}() <-exit
} /*
a: 0
a: 1
b //这个b有可能打印不出来
a: 2
a: 3
*/

  

该函数很少被使用,因为运行时会主动向长时间运行的任务发出抢占调度。

Goexit

立即终止当前任务,运行时确保所有已注册延迟调用被执行。
该函数不会影响其它并发任务,不会引发panic,自然也就无法捕获。

package main

import (
"fmt"
"runtime"
) func main() {
exit := make(chan struct{}) go func() {
defer close(exit)
defer fmt.Println("a") //执行3 func() {
defer func() {
fmt.Println("b", recover() == nil) //defer总会执行 2
}() func() {
fmt.Println("c") //执行1
runtime.Goexit() //立即终止当前任务
fmt.Println("c done.") //不会执行
}() fmt.Println("b done.") //不会执行
}() fmt.Println("a done.") //不会执行
}() <-exit
fmt.Println("main exit") //主程序
} /*
c
b true
a
main exit
*/

  

如果在main里调用Goexit,它会等其它任务结束,然后让进程直接崩溃。

package main

import (
"fmt"
"runtime"
"time"
) func main() {
for i := 0; i < 2; i++ {
go func(x int) {
for n := 0; n < 2; n++ {
fmt.Printf("%c: %d\n", 'a'+x, n)
time.Sleep(time.Millisecond)
}
}(i)
} runtime.Goexit() //等待所有任务结束
fmt.Println("main exit.")
} /*
b: 0
a: 0
a: 1
b: 1
fatal error: no goroutines (main called runtime.Goexit) - deadlock!
*/

  

无论身处那一层,Goexit都能立即终止整个调用堆栈,这与return仅退出当前函数不同。
标准库函数os.Exit可终止进程,但不会执行延迟调用。

go——并发的更多相关文章

  1. .Net多线程编程—并发集合

    并发集合 1 为什么使用并发集合? 原因主要有以下几点: System.Collections和System.Collections.Generic名称空间中所提供的经典列表.集合和数组都不是线程安全 ...

  2. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

  3. [高并发]Java高并发编程系列开山篇--线程实现

    Java是最早开始有并发的语言之一,再过去传统多任务的模式下,人们发现很难解决一些更为复杂的问题,这个时候我们就有了并发. 引用 多线程比多任务更加有挑战.多线程是在同一个程序内部并行执行,因此会对相 ...

  4. 关于如何提高Web服务端并发效率的异步编程技术

    最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...

  5. 如何在高并发环境下设计出无锁的数据库操作(Java版本)

    一个在线2k的游戏,每秒钟并发都吓死人.传统的hibernate直接插库基本上是不可行的.我就一步步推导出一个无锁的数据库操作. 1. 并发中如何无锁. 一个很简单的思路,把并发转化成为单线程.Jav ...

  6. Java多线程基础——对象及变量并发访问

    在开发多线程程序时,如果每个多线程处理的事情都不一样,每个线程都互不相关,这样开发的过程就非常轻松.但是很多时候,多线程程序是需要同时访问同一个对象,或者变量的.这样,一个对象同时被多个线程访问,会出 ...

  7. 多线程的通信和同步(Java并发编程的艺术--笔记)

    1. 线程间的通信机制 线程之间通信机制有两种: 共享内存.消息传递.   2. Java并发 Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式执行,通信的过程对于程序员来说是完全透 ...

  8. 伪共享(false sharing),并发编程无声的性能杀手

    在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...

  9. 编写高质量代码:改善Java程序的151个建议(第8章:多线程和并发___建议126~128)

    建议126:适时选择不同的线程池来实现 Java的线程池实现从根本上来说只有两个:ThreadPoolExecutor类和ScheduledThreadPoolExecutor类,这两个类还是父子关系 ...

  10. 理解Storm并发

    作者:Jack47 PS:如果喜欢我写的文章,欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 注:本文主要内容翻译自understanding-the-parall ...

随机推荐

  1. Java实现ZIP解压功能

    1.引入依赖 <dependency> <groupId>org.apache.ant</groupId> <artifactId>ant</ar ...

  2. .net 编码常见问题

    问题一: windows service 启动时需要用死循环来控制程序,如果不开启异步,死循环会导致windows sevice 程序无法启动,代码情况如下 protected override vo ...

  3. mfc小工具开发之定时闹钟之---二十四小时时区和时间段

    1.凌晨0:00-6:00时显示凌晨,上午6:00-12:00显示上午,中午12:00-14:00显示中午,下午14:00-显示下午,晚上18:00-24:00显示晚上 2. 早上:6-8:上午8-1 ...

  4. linux 知识点拾遗

    文件名称 在 Linux 底下,每个档案或文件夹的文件名称最长能够到达 255 的字符,加上完整路径时,最长可达 4096 个字符; 因为 Linux 在文字接口下的一些指令操作关系,一般来说,您在设 ...

  5. php form 图片上传至服务器上

    本文章也是写给自己看的,因为写的很简洁,连判断都没有,只是直接实现了能上传的功能. 前台: <form action="upload.php" method="PO ...

  6. Linux文件的打包与压缩

    打包命令: tar tar 的选项与参数非常的多!我们只讲几个常用的选项,更多选项您可以自行 man tar 查询罗! [root@www ~]# tar [-j|-z] [cv] [-f 创建的档名 ...

  7. 用MathType编辑异或与非符号有什么方法

    在数学中我们会遇到各种数学符号,有运算符号,希腊符号,还有表示逻辑关系的逻辑符号等,这些大多都是比较常用的符号.其中逻辑符号中我们经常会用到异或与非等,这些符号的编辑我们常常会需要用MathType这 ...

  8. 修改了JS代码,刷新网页后,加载的JS还是原来旧的?

    本地修改JS脚本后,刷新网页看一下修改后的执行效果,结果调试显示加载的JS还是原来旧的,反复刷新均无效,郁闷! 解决办法:清理一下浏览器缓存(长经验了!)     Ctrl+Shift+Del 清除G ...

  9. Entity Framework基础

    http://blog.csdn.net/hurtlingsnail/article/details/53113934

  10. Python_selenium之处理Alert窗

    Python_selenium之处理Alert窗 一.介绍 1. 介绍如何通过switch_to方法处理网页Alert窗口 2. 然后我们自己创建一个alert弹窗进行操作 二.测试脚本 1. 测试脚 ...