Go语言并发编程简介
并发的基础知识
进程与线程的回顾总结:
进程的定义:
进程比较通用的几个定义:
- 进程是程序的一次执行过程
- 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
- 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
从定义上可以看出,进程实际上是一个过程的描述,它强调运行的程序的这个过程,而不是像程序那样的物理实体。。
进程的基本特性:
- 动态性:正如上面描述的,进程是一个进程实体的执行过程
- 并发性:多个实体进程同时存在与内存中,可以在一个时间段内同时运行。并发不是并行,并行是指同一时刻同时发生,而并发只是在一个时间段内。
- 独立性:每个进程可以独立运行、独立获得资源和独立接受处理机的调度。每个进程的资源不允许其他进程访问。
- 异步性:每个进程是各自独立地,按照不可预知的速度进行推进
进程的基本状态
- 就绪状态:进程已经拥有除了CPU之外的所有执行所需的资源,只要获得CPU就能执行
- 执行状态:正在CPU上执行
- 阻塞状态:执行的进程由于发生某事件(I/O处理、申请缓存失败等),暂时无法继续执行的状态。
线程的定义
线程可以视为进程的一个任务,相当于一个更加轻量级的进程。在引入线程的OS中,线程是运行的基本单位。一个进程可以有多个线程,线程之间可以并发或者并行执行;进程之间可以共享线程的资源;每个进程可以访问所属线程的所有空间;系统开销远远小于进程。
进程线程之间的关联
主要参考了这篇博客
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元.
进程与线程的关系:
进程和线程的几个转换状态:
并发主流的实现模型
- 多进程,并发的基本模式
- 多线程,大部分操作系统属于系统层面的并发模式,使我们使用的最多也是最有效的模式
- 基于回调的异步I/O操作,
Nodejs
采用的这种方式,事件循环,异步I/O模式。但是编程的时候需要大量的回调操作。 - 协程,一种更加轻量级的线程。一个线程可以有多个协程,系统开销极小,不需要操作系统进行抢占式调度。
多线程的实现借助于“共享内存系统”,而Golang的协程借助于消息传递系统,发送消息的时候对状态进行复制,并在消息传递的边界上交出这个状态的所有权。
协程和goroutine
协程与进程想、线程的关系:
进程和线程都是由操作系统控制,进程、线程的切换需要操作系统的内核来管理;而协程是由程序本身控制的,所以使用的代价极低。
goroutine
是Go语言中的协程,需要使用关键字go
来实现。使用了go
的函数,在调用时就会在goroutine
中执行,函数返回则goroutine
结束,如果函数有返回值,则丢弃返回值。当main
函数返回时,程序退出,而且程序不等待其他的goroutine
(非主goroutine
)结束。
代码:
package mainimport ("fmt"
)func Add(x, y int) {z := x + yfmt.Println(z)
}func main() {for i := 0; i < 10; i++ {go Add(i, i)}
}
代码中的函数不会有任何输出,因为主函数启动了10个goroutine
后,就退出了,那10个goroutine
还没来得及执行。。
并发通信
- 共享数据模型:可以理解成加锁的那种模式,用户的线程共享内存单元
- 共享消息模型:每个并发的单元是独立的个体,他们之间不共享变量等的数据,每个并发单元之间唯一的输入和输出是“消息”,这也是他们唯一的通信方式,不共享内存。
channel
Golang使用channel
提供goroutine
之间的通信,是类型相关的,一个channel
只能传递一种类型的值,需要提前声明。但是,涉及到跨进程通信时,最好采用分布式的方式解决,比如Socket
等的协议。
基本语法
声明:
var chaName chan ElementType
代码实例:
var ch chan int // 整型的
var m map[string] chan bool // 注意map型的
定义:
ch := make(chan int) // 定义int型的channel
写入数据:
ch <- value // value数据写入channel
注意:向channel
写入数据的操作必须在一个goroutine
中执行(即使用关键字go
),否则程序报错!
写入数据通常会阻塞程序,知道有其他的goroutine
从channel
中读取数据。
读取数据:
value := <-ch
如果channel
中没有数据,则goroutine
也会阻塞,直到channel
中被写入数据为止
select
操作
基本语法示例:
select {case <-chan1:// 如果chan1读到数据,则进行该case处理case <-chan2:// 如果chan2读到数据,则进行该case处理case chan3 <- value:// 如果写入数据到chan3,则进行该case处理default:// 都没成功,则在这里处理
}
每个case
后面必须是面向channel
的操作。
缓冲机制
用于创建channel
缓冲队列,适合大规模的数据传输场景:
c := make(chan int, 1024) // 创建了含有1024个channel的队列
这样,即使没有读取方,写入方也可以一直往channel
里面写入数据,缓冲区填满之前不会发生阻塞。
读取方式:
for i := range c {fmt.Println("Received: ", i)
}
超时机制
适用于向channel
写数据时,channel
已满;或者从channel
读数据,channel
已空。防止产生死锁。借助于select
函数实现:
timeout := make(chan bool, 1)
go func() {time.Sleep(1e9) // 等待一秒钟timeout <-true
}()select {case <-ch:// 从ch中读取数据case <-timeout:// 一直没有从ch中读到数据,但是从timeout中读到了数据
}
channel
的传递
Go语言的channel
是一个基本类型,地位等同于map
之类的。channel
本身定义后也可以通过channel
来传递,可以用这个事项管道pipe
的特性。管道的知识。但是,GO语言的管道是类型相关的,只能传递一种类型的数据。
type PipeData struct {value int/** 通用的模式是在这里添加自己需要的数据结构*/handler func(int) intnext chan int
}func handle(queue chan *PipeData) {for data := range queue {data.next <- data.handler(data.value)}
}
代码的解读:
PipeData
结构体中封装了数据value
;handler
是一个函数指针,用来处理数据,传输和返回值都是int
型的,因为Go语言的管道是传递同一种类型的数据;next
是一个channel
类型,用于存储整型的数据,把每个程序的next
相互链接,就组成了管道。
单向channel
单向的channel
只能用于发送数据或者只能接收数据。用channel
的类型转换机制,实现专门的读或者写的channel
。
var ch1 chan int // 普通的channel
var ch2 chan<- float64 // 只能写数据的单向channel
var ch3 <-chan int // 只能读数据的单向channel
ch4 := make(chan, int)
ch5 := <-chan int(ch4) // 转换成只读的channel
ch6 := chan<- int(ch4) // 转换成只写的channel
这是为了遵循最小权限的准则:
func Parse(ch <-chan int) {for value := range ch {fmt.Println("Parsing value", value)}
}
方式误写入数据,因为可以理解为channel
是传递的引用。。。
关闭channel
使用close()
函数关闭channel
。
close(ch1)
x, ok := close(ch2) // 只需要看第二个ok,如果是false,那么表示成功关闭。
关闭channel
后,不能再向channel
中写入数据了,但是可以从中读取残余的数据!
关于channel
常用的操作
简单的同步操作
等待同步,等待list
排序完成后,再去做其他事情,代码示例:
c := make(chan int) // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {list.Sort()c <- 1 // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c // Wait for sort to finish; discard sent value.
并发任务模式,作为信号量集
channel
作为一个信号量使用,实现生产者和消费者模式,代码实例:
var sem = make(chan int, MaxOutstanding)func handle(r *Request) {sem <- 1 // Wait for active queue to drain.process(r) // May take a long time.<-sem // Done; enable next request to run.
}func Serve(queue chan *Request) {for {req := <-queuego handle(req) // Don't wait for handle to finish.}
}
上述代码中,handle
函数用于处理请求;有一个信号量集sem
,容量是MaxOutstanding
,表示最大的吞吐量;利用channel
自身的读写阻塞机制,可以实现最多MaxOutstanding
的并发操作。
但是,上述代码的Server
存在一些不足:虽然最多同时运行MaxOutstanding
个goroutine
,但是如果请求来的过快,会导致程序创建出过多的goroutine
。即使它们等待运行,但是它们会占用系统的其他资源的。下面给出改进的方案:
func Serve(queue chan *Request) {for req := range queue {sem <- 1go func(req *Request) {process(req)<-sem}(req) // 在这里的这一句,可以看成一个原子操作,防止req被多个goroutine共享}
}// 或者使用下面的等效操作
func Server(queue chan *Request) {for req := range queue {req := req // 在闭包内使用局部的,防止共享sem <- 1gon func() {process(req)<-sem}()}
}
另外一种方式,让handle
一次性处理一个批次的请求,然后在Server
函数中确定goroutine
的个数。这样,goroutine
的数量就限制了一次性调用process
函数的次数。quit
变量是用于控制退出的。
func handle(queue chan *Request) {for r := range queue {process(r)}
}func Serve(clientRequests chan *Request, quit chan bool) {// Start handlersfor i := 0; i < MaxOutstanding; i++ {go handle(clientRequests)}<-quit // Wait to be told to exit.
}
Channels
of channels
客户端代码:
type Request struct {args []intf func([]int) intresultChan chan int
}func sum(a []int) (s int) {for _, v := range a {s += v}return
}request:= &Request{[]int{3, 4, 5}, sum, make(chan int)}// 发送请求
clientRequests <-request// 等待服务器回应
fmt.Println("answer: %d\n", <-request.resultChan)
服务端代码:
func handle(queue chan *Request) {for req := range queue {req.resultChan <- req.f(req.args)}
}
多核并行化和出让时间片
计算N个整数的和,把所有的整数分解成M份,M是CPU的数量。让每个CPU计算分给它的那份计算任务,最后把结果做一个累加。
多核并行化代码实例:
type Vector []float64// // Apply the operation to v[i], v[i+1] ... up to v[n-1]
func (v Vector) DoSome(i, n int, u Vector, c chan int) {for ; i < n; i++ {v[i] += u.Op(v[i])}c <- 1 // 完成的信号
}const NCPU = 16func (v Vector) DoAll(u Vector) {c := make(chan int, NCPU) // 用于接收每个CPU完成的信号for i := 0; i < NCPU; i++ {go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)}for i := 0; i < NCPU; i++ {<-c // 取到一个数据,表示计算完成}
}
同步
这里主要是处理多个goroutine
之间共享数据的问题。
同步锁
sync
包中提供了两种类型锁:
sync.Mutex
:当一个goroutine
获得该锁后,其它的goroutine
只能等它释放这个锁sync.RWMutex
:这是单写多读模式,读锁占用的情况下,会阻止,但不会阻止读,多个goroutine
可以同时获得读锁(调用RLock()
方法);写锁(调用Lock()
方法)阻止其他任何goroutine
读写。
任何一个Lock()
或者RLock()
均需要有对应的UnLock()
和RUnLock()
方法与之对应,否则可能导致死锁。一般使用defer
关键字解决这个问题:
var l sync.Mutex
func foo() {l.Lock()defer l.UnLock()
}
全局唯一性操作
用于处理全局的一次性操作,使用Once
类型解决:
var a string
var once sync.Oncefunc setup() {a = "hello world !"
}func doprint() {once.Do(setup)print(a)
}func twoprint() {go doprint()go doprint()
}
once
的Do()
方法可以保证在全局值执行一次,其他的goroutine
执行到这一句时,会先被阻塞,知道全局唯一的once.Do()
调用结束后才返回。
Go语言并发编程简介相关推荐
- Akka框架——第一节:并发编程简介
本节主要内容: 1. 重要概念 2. Actor模型 3. Akka架构简介 多核处理器的出现使并发编程(Concurrent Programming)成为开发人员必备的一项技能,许多现代编程语言都致 ...
- Go语言开发(九)、Go语言并发编程
Go语言开发(九).Go语言并发编程 一.goroutine简介 1.并发与并行简介 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行. 并发(concurrency):指在同 ...
- Java并发编程简介
并发编程简介 1. 什么是并发编程 所谓并发编程是指在一台处理器上"同时"处理多个任务.并发是在在同一实体上的多个事件.多个事件在同一时间间隔发生. 并发编程 ①从程序设计的角度来 ...
- 融云开发漫谈:你是否了解Go语言并发编程的第一要义?
2007年诞生的Go语言,凭借其近C的执行性能和近解析型语言的开发效率,以及近乎完美的编译速度,席卷全球.Go语言相关书籍也如雨后春笋般涌现,前不久,一本名为<Go语言并发之道>的书籍被翻 ...
- java并发编程笔记_java并发编程笔记(一)——并发编程简介
java并发编程笔记(一)--简介 线程不安全的类示例 public class CountExample1 { // 请求总数 public static int clientTotal = 500 ...
- golang sqlx scan 到结构体中_Golang语言并发编程之定时器
上一章中对于golang的常用关键字说明如下: 1 for 和 range 2 select 3 defer 4 panic 和 recover 5 make 和 new 接下来我们来对golang的 ...
- Python并行和并发编程简介
通常,Python是用于数据处理和数据科学的最受欢迎的语言之一. 该生态系统提供了许多促进高性能计算的库和框架. 不过,在Python中进行并行编程可能会非常棘手. 在本教程中,我们将研究为什么并行性 ...
- 唤醒手腕 Go 语言 并发编程 (goroutine、channel)详细教程(更新中)
线程.协程基本概念 协程是单线程下的并发,又称微线程,纤程.它是实现多任务的另一种方式,只不过是比线程更小的执行单元.因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程 ...
- (2021-02-04)并发编程简介-并发编程(1)
关于并发编程,不光是面试经常问到,在实际的操作过程中,也会经常用到.所以一来是为了加深自己的印象,二来也希望能和大家公共学习.不对的地方请斧正,谢谢! 1.程序.进程.线程的关联和区别? 这是个老生常 ...
最新文章
- CTFshow php特性 web146
- Servlet-Access denied for user 'root'@'localhost' (using password: YES
- linux smplayer 快捷键,SMPlayer:让 MPlayer 的使用更简单
- 目标文件里面到底有什么(2)?
- 钉钉功能介绍_平棉集团组织召开阿里钉钉办公系统基础功能培训会
- 步步为营 SharePoint 开发学习笔记系列 七、SharePoint Timer Job 开发
- 全局变量局部变量ScriptCase中的全局变量、局部变量
- Ubuntu16.04 下 tensorRT安装
- python怎么打包_如何打包python程序
- 删除 setup.py 安装的 Python 软件包
- arcgis导出shp文件_RegionManager GIS导出shp文件编码说明
- android书籍和教程推荐--不断更新
- Android NDK学习记录
- 网工必考的8个dos命令
- 你不知道的JavaScript(上卷)- - 书本知识点记录
- 看山不是山看水不是水
- 领带的10种打法图解
- 传说之下怎么设置按键_《传说之下手机版》按键设置教程
- Bot Chat(聊天机器人) HeroCard的简单用法
- 在vue 中 ,dom操作滚动条 scrollTop无效