目录导航
-
撤销(Ctrl+Z)
-
重做(Ctrl+Y)
-
清空
-
H
标题(Ctrl+1~6)
- 一级标题
- 二级标题
- 三级标题
- 四级标题
- 五级标题
- 六级标题
-
粗体(Ctrl+B)
-
斜体(Ctrl+I)
-
删除线
-
插入引用(Ctrl+Q)
-
无序列表(Ctrl+U)
-
有序列表(Ctrl+O)
-
表格
-
插入分割线
-
插入链接(Ctrl+L)
-
插入图片
- 添加图片链接
-
插入代码块
-
保存(Ctrl+S)
-
开启预览
-
开启目录导航
-
关闭同步滚动
-
全屏(按ESC还原)
# 特点 * 简单,只有25个关键字,并发编程,内存管理处理方便。 * 高效,编译的静态语言,支持用指针直接访问内存。 * 生产力,有简洁清晰的依赖管理,独特的接口类型设计,以及对编程方式的约束。 * 复合大于继承,go不支持继承 # 背景 * 面临软件开发的新挑战: 1. 多核硬件架构 1. 超大规范分布式计算集群 1. Web模式导致的前所有为的开发模式和更新速度 # 工作区 * GOROOT:Go 语言安装根目录的路径,也就是 GO 语言的安装路径。 * GOPATH:若干工作区目录的路径。是我们自己定义的工作空间。 * GOBIN:GO 程序生成的可执行文件(executable file)的路径。 ## GOPATH * Go 语言的工作目录,可以是多个目录路径,每个目录都代表 Go 语言的一个工作区(workspace)。 * 工作区里面放置 Go 语言的源码文件(source file),以及安装(install)后的归档文件(archive file,也就是以“.a”为扩展名的文件)和可执行文件(executable file)。 # 源码文件 * 分为三种,即:命令源码文件、库源码文件和测试源码文件,它们都有着不同的用途和编写规则。 ## Go 语言源码的组织方式 * Go 语言的源码也是以代码包为基本组织单位的。代码包的名称一般会与源码文件所在的目录同名。如果不同名,那么在构建、安装的过程中会以代码包名称为准。 ## 源码安装结果 * 某个工作区的 src 子目录下的源码文件在安装后,如果产生了归档文件(以“.a”为扩展名的文件),就会放进该工作区的 pkg 子目录;如果产生了可执行文件,就可能会放进该工作区的 bin 子目录。 ## 源码安装和构建 * 安装操作会先执行构建,然后还会进行链接操作,并且把结果文件搬运到指定目录。 | | 构建 | 安装 | | ------------ | ------------ | ------------ | | 命令 | build | install | | 操作 | 编译+打包 | 编译+打包 | | 库源码文件 | 存放到临时目录 (检查和验证) | 存放到工作区的 pkg 目录下 | | 命令源码文件 | 源码文件所在的目录 | 工作区的 bin 目录或环境变量GOBIN目录 | * 命令go get会自动从一些主流公用代码仓库(比如 GitHub)下载目标代码包,并把它们安装到环境变量GOPATH包含的第 1 工作区的相应目录中。如果存在环境变量GOBIN,那么仅包含命令源码文件的代码包会被安装到GOBIN指向的那个目录。 # 语法 ## 应用程序入口 * 必须是main包,package main * 必须是main方法,func mian(){} * 文件名不一定是main.go ## 差异 * main函数不支持返回值,通过os.Exit()来返回状态,(而且只能返回数字或者是单个字符,单个字符会被直接转成ASCII码)。 * main函数不支持传入参数,可以通过os.Args获取参数 ## 测试 * 测试用例,文件名要以“_test.go”结尾, * 每个测试方法名都以大写的Test开始, * 参数为(t *testing.T) ``` func TestFriList(t *testing.T) { a, b := 1, 1 t.Log(a) for i:=0; i<5;i++ { t.Log(b) a, b = b, a+b } } ``` ## 变量赋值 * 可以使用 := 来让程序自动推断类型 * 在一个赋值语句可以赋值多个变量 * string类型默认值是空字符串而不是nil ``` const ( Monday = 1 + iota Tuesday Wedensday ) ``` ## 类型转化 * go语言不支持隐式类型转换;32位转64位也不行。 * 别名和原有类型也不支持隐式类型转换; ## 指针类型 * 不支持指针运算 ## 算数运算符 * 不支持前置的++,--,后置++,--是支持的,其他都 支持 * == 在比较两个数组时,只有相同维度相同元素个数时才可以。 ## 条件和循环 * go只有for没有while,可以直接用for代替while。 * if 语句condition表达式必须是布尔值,condition支持前面赋值 ``` if var declaration; condition{ } switch { case i % 2 == 0 : t.Log("Even") case i % 2 == 1: t.Log("Odd") default: t.Log("not zero") } switch i { case 0,2: t.Log("Even") case 1,3: t.Log("Odd") default: t.Log("not zero") } ``` ## 数组 * 定义:数组需要指定类型和元素个数,内存空间是固定。 ``` var arr [3]int arr := [...]int{1,2,3} //数组切片,前闭后开 arr[开始索引, 结束索引] ``` ### 数组遍历 ``` for i:=0; i<len(a);i++ { t.Log(i, a[i]) } //类似foreach写法 for key, value := range a{ t.Log(key, value) } //不想要下标,用_代替 for _, value := range a{ t.Log( value) } ``` ## 切片 * 切片是没有固定长度的,每一次长度变化都会将原来的数据进行拷贝,并分配新的内存空间, * 不能用 == 比较 ``` var s0 []int s0 = append(s0, 1) s := []int{} s1 := []int{1,2,3,4} s2 := make([]int, 3, 5) //3是指初始化的元素个数,(用len查看),初始化默认值为0,5是最大长度(用cap查看),违背初始化的元素无法访问,每一次cap增长都是前一次的2倍。 ``` ## Map * Map是没有固定长度的,以键值对的形式存在; * 访问不存在的key时,会返回value类型值的默认值,string是空字符串,int是0,不能通过nil来判断是否存在。 * value可以是一个函数,来构建工厂模式。 ``` m := map[string]int{"s":1,"d":1,"p":1} m := make(map[string]int, 9) //Map工厂模式 m := map[int]func(op int)int{} m[1] = func(op int) int { return op } m[2] = func(op int) int { return op*op } m[3] = func(op int) int { return op*op*op } t.Log(m[1](2), m[2](2), m[3](2)) ``` ## 字符串 * 与其他编程语言的差异 1. string是数据格式,不是引用或指针类型 1. string是只读的byte slice,len函数可以计算它包含的byte数,而不是字符数 1. string的byte数组可以存放任何数据 ## 函数 * 类似python的闭包 ``` func timeSpent(inner func(op int) int) func(op int) int { return func(n int) int { start := time.Now() ret := inner(n) fmt.Println("time spent: ", time.Since(start).Seconds()) return ret } } func slowFunc(op int) int { time.Sleep(time.Second *2) return op } ``` ### defer函数 * 在函数内容定义defer函数,会在函数执行成功之后再运行,一般可用来释放锁。 * 用panic抛出错误,defer函数仍会执行,但是panic后面的程序不再执行 * 多个defer的话,先入后出。 ## 面对对象 * 不支持继承,可以进行复合,但是复合也可以获得相关的方法,但是不能继承那样可以对父类方法进行重写来实现重载。 * 加上*表示指针引用,指针引用和值引用结果是一样的,但是指针引用不会占用新的内存。 ``` type Empleyee struct { Id int Name string Age int } func TestMyObject(t *testing.T) { e := Employee{0, "wc", 24} e1 := Employee{Id:2, Name:"wen"} e2 := new(Employee) e2.Id = 3 e2.Name = "cai" t.Log(e, e1, e2) } ``` ## 接口 * 接口是非入侵性的,实现是不依赖于接口定义的 * 接口的定义是可以包含在接口使用者包中 ``` type Program interface { Write() string } type GoProgram struct { Id string Name string } func (g *GoProgram) Write() string { return "hello world" } func TestInterface(t *testing.T) { var p Program p = new(GoProgram) t.Log(p, p.Write()) } ``` ## init 函数 * 包在被调用的时候,会先执行init函数 * 一个包中可以存着多个init函数 * 包中init函数的执行顺序与包被调用的依赖关系有关 ## 包 * 包必须放在GOPATH或者GOROOT的src或vender文件夹下面,才可以被调用 * 远程包可以通过go get获取到本地 ``` go get -u https://github.com/easierway/concurrent_map ``` * go不能同时使用同一个包的不同版本 * go的包管理工具:glide ## 协程Goroutine * 协程与线程thread区别: jdk中stack默认大小是1M,而go中stack是2k * KSE(kernel space entity)对应关系: Java Thread 是1对1,而go的Groutine是多对多 ### Goroutine的使用 * 在函数的调用者那里使用,而不是写在函数内部。 * 一定要能够在Goroutine外部能够控制Goroutine的生命周期。 * 要做好超时控制。 #### 如果在Processor调度器依次处理Goroutine协程时,有一个Goroutine协程运行时间很久,那么队列里的其他Goroutine协程会不会等很久? * 是这样的,go在运行的时候,会开启一个守护进程,它会记录每个Processor调度器完成的Goroutine协程的数量,当发现某个Processor完成的协程数量在一段时间内都没有发生变化时,它会往这个协程的任务栈中插入一个特殊的标记,当该协程执行到标记时就会中断下来,插入到等待协程队列的队尾,切换成别的协程,继续运行。 #### GPM并发机制 * 当某一个协程被系统中断了,例如:IO,需要等待的时候,为了提高整体的并发,Processor会把自己移动到另一个可使用的系统线程当中,继续执行它所挂的协程队列里的其他协程,当这个被中断的协程完成之后被唤醒,它会把自己加入到某一个协程等待队列里。 * 协程中断的时候,它在寄存器中的运行状态,也会保存到协程对象里,当协程再次运行时,这些会重新写入寄存器继续运行。 ### 共享内存并发机制 * go中的协程不是线程安全的,需要用锁来进行保护,使用锁的时候同时会用defer来最后释放锁,防止程序被整体挂起。 * WaitGroup线程同步方法,当wait中的程序执行完成之后才能继续往下执行。 ``` func TestGroutine(t *testing.T) { var mut sync.Mutex var wg sync.WaitGroup sum := 0 for i:=1; i<=10000; i++ { wg.Add(1) go func (i int) { defer func() { mut.Unlock() }() mut.Lock() sum ++ wg.Done() }(i) } wg.Wait() //time.Sleep(time.Microsecond * 1000 ) t.Logf("counter is %d", sum) } ``` ## Channel * 对于同一个Channel,发送操作和接受操作都是互斥的。 * 整个发送操作和整个接受操作都是原子性的不可分割。发送操作分为:复制元素值,和放置副本到队列中两个操作。接受操作分为:复制Channel内元素值,放置副本到接受方,删除Channel内元素值三个操作。 * 发送操作和接受操作在完成之前都是会被阻塞的。 * 往Channel中发送nil会阻塞,Channel是引用类型,所以它的零值就是nil。当我们只声明该类型的变量但没有用make函数对它进行初始化时,该变量的值就会是nil。 * 通道一旦关闭,再对它进行发送操作,就会引发 panic。 * 关闭一个已经关闭了的通道,也会引发 panic。 #### 非阻塞的Channel,长度为0,同步执行 * 发送阻塞直到数据被接收,接收阻塞直到读到数据。 * 发送方和接收方必须同时在线,发送方发送的过程中,接收方先一直等待。 * 数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。 #### 阻塞的Channel,长度大于0,异步执行 * 当缓冲满时发送阻塞,当缓冲空时接收阻塞。 * 设置一个Channel容量,在容量没有满时,发送方可以一直发新的消息,如果容量满了发送消息会进行阻塞,需要等待接收方处理才能发送。 * 通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发送操作。 ### channel的关闭 * 向关闭的channel发送数据,会导致panic * channel可以返回两个值,第一个是数据第二是channel的状态,状态为true时表示正常接收,状态为false时表示通道关闭。 * 所有channel的接受者都会在channel关闭时,立即从阻塞的等待中返回。 ``` func dataProducer(ch chan int, wg *sync.WaitGroup) { go func() { for i:=0; i<10; i++ { ch <- i } close(ch) wg.Done() }() } func dataReceiver(ch chan int, wg *sync.WaitGroup) { go func() { for { if data, ok := <-ch; ok { fmt.Println(data) } else { break } } //for i:=0; i<10; i++ { // data := <-ch // fmt.Println(data) //} wg.Done() }() } func TestCloseChannel(t *testing.T) { var wg sync.WaitGroup ch := make(chan int) wg.Add(1) dataProducer(ch, &wg) wg.Add(1) dataReceiver(ch, &wg) wg.Add(1) dataReceiver(ch, &wg) wg.Add(1) } ``` ## CSP * CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。 ## 多路选择和超时 * select实现多渠道的选择,不同渠道之间执行跟代码的顺序无关,一般用来处理超时问题,防止主线程一直阻塞。 ``` func service() string { time.Sleep(time.Millisecond * 50) return "Down" } func otherTask() { fmt.Println("working on something else") time.Sleep(time.Millisecond * 100) fmt.Println("work down") } func AsyncService() chan string { retCh := make(chan string, 1) go func() { ret := service() fmt.Println("return result") retCh <- ret fmt.Println("service exited") }() return retCh } func TestAsyncService(t *testing.T) { retCh := AsyncService() otherTask() fmt.Println(<-retCh) } func TestService(t *testing.T) { select { case ret := <-AsyncService(): t.Log(ret) case <-time.After(time.Millisecond*100): t.Error("time out!") } } ``` ## 从 panic 被引发到程序终止运行的大致过程是什么 * 我们先说一个大致的过程:某个函数中的某行代码有意或无意地引发了一个 panic。这时,初始的 panic 详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。 * 这也意味着,此行代码所属函数的执行随即终止。紧接着,控制权并不会在此有片刻的停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。 * 这里的最外层函数指的是go函数,对于主 goroutine 来说就是main函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。 * 随后,程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic 详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。 ## sync ### 竞态条件 * 目前大部分语言都是通过 **通过共享数据的方式来传递信息和协调线程运行**,而go强调 **用通讯的方式共享数据**。 * 一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况。这种情况也被称为竞态条件(race condition),这往往会破坏共享数据的一致性。 * 共享数据的一致性代表着某种约定,即:多个线程对共享数据的操作总是可以达到它们各自预期的效果。为了达到这个效果协调它们对缓冲区的修改,就需要进行同步。 * 同步的用途有两个,一个是避免多个线程在同一时刻操作同一个数据块,另一个是协调多个线程,以避免它们在同一时刻执行同一个代码块。 * 一个线程在想要访问某一个共享资源的时候,需要先申请对该资源的访问权限,并且只有在申请成功之后,访问才能真正开始。 * 而当线程对共享资源的访问结束时,它还必须归还对该资源的访问权限,若要再次访问仍需申请。 * 这时,我们可以说,多个并发运行的线程对这个共享资源的访问是完全串行的。只要一个代码片段需要实现对共享资源的串行化访问,就可以被视为一个临界区(critical section),也就是我刚刚说的,由于要访问到资源而必须进入的那个区域。 * 施加保护的重要手段之一,就是使用实现了某种同步机制的工具,也称为同步工具。 * 在 Go 语言中,可供我们选择的同步工具并不少。其中,最重要且最常用的同步工具当属互斥量(mutual exclusion,简称 mutex)。sync包中的Mutex就是与其对应的类型,该类型的值可以被称为互斥量或者互斥锁。 #### 使用互斥锁的注意事项如下: * 不要重复锁定互斥锁; * 不要忘记解锁互斥锁,必要时使用defer语句; * 不要对尚未锁定或者已解锁的互斥锁解锁; * 不要在多个函数之间直接传递互斥锁。 #### 当所有的goroutine都阻塞时,会造成deadlock,程序必然会发生panic,而且是无法recover的。而最简单、有效的方式就是让每一个互斥锁都只保护一个临界区或一组相关临界区。
特点
-
简单,只有25个关键字,并发编程,内存管理处理方便。
-
高效,编译的静态语言,支持用指针直接访问内存。
-
生产力,有简洁清晰的依赖管理,独特的接口类型设计,以及对编程方式的约束。
-
复合大于继承,go不支持继承
背景
- 面临软件开发的新挑战:
- 多核硬件架构
- 超大规范分布式计算集群
- Web模式导致的前所有为的开发模式和更新速度
工作区
- GOROOT:Go 语言安装根目录的路径,也就是 GO 语言的安装路径。
- GOPATH:若干工作区目录的路径。是我们自己定义的工作空间。
- GOBIN:GO 程序生成的可执行文件(executable file)的路径。
GOPATH
- Go 语言的工作目录,可以是多个目录路径,每个目录都代表 Go 语言的一个工作区(workspace)。
- 工作区里面放置 Go 语言的源码文件(source file),以及安装(install)后的归档文件(archive file,也就是以“.a”为扩展名的文件)和可执行文件(executable file)。
源码文件
- 分为三种,即:命令源码文件、库源码文件和测试源码文件,它们都有着不同的用途和编写规则。
Go 语言源码的组织方式
- Go 语言的源码也是以代码包为基本组织单位的。代码包的名称一般会与源码文件所在的目录同名。如果不同名,那么在构建、安装的过程中会以代码包名称为准。
源码安装结果
- 某个工作区的 src 子目录下的源码文件在安装后,如果产生了归档文件(以“.a”为扩展名的文件),就会放进该工作区的 pkg 子目录;如果产生了可执行文件,就可能会放进该工作区的 bin 子目录。
源码安装和构建
- 安装操作会先执行构建,然后还会进行链接操作,并且把结果文件搬运到指定目录。
构建 | 安装 | |
---|---|---|
命令 | build | install |
操作 | 编译+打包 | 编译+打包 |
库源码文件 | 存放到临时目录 (检查和验证) | 存放到工作区的 pkg 目录下 |
命令源码文件 | 源码文件所在的目录 | 工作区的 bin 目录或环境变量GOBIN目录 |
- 命令go get会自动从一些主流公用代码仓库(比如 GitHub)下载目标代码包,并把它们安装到环境变量GOPATH包含的第 1 工作区的相应目录中。如果存在环境变量GOBIN,那么仅包含命令源码文件的代码包会被安装到GOBIN指向的那个目录。
语法
应用程序入口
- 必须是main包,package main
- 必须是main方法,func mian(){}
- 文件名不一定是main.go
差异
- main函数不支持返回值,通过os.Exit()来返回状态,(而且只能返回数字或者是单个字符,单个字符会被直接转成ASCII码)。
- main函数不支持传入参数,可以通过os.Args获取参数
测试
- 测试用例,文件名要以“_test.go”结尾,
- 每个测试方法名都以大写的Test开始,
- 参数为(t *testing.T)
func TestFriList(t *testing.T) {
a, b := 1, 1
t.Log(a)
for i:=0; i<5;i++ {
t.Log(b)
a, b = b, a+b
}
}
变量赋值
- 可以使用 := 来让程序自动推断类型
- 在一个赋值语句可以赋值多个变量
- string类型默认值是空字符串而不是nil
const (
Monday = 1 + iota
Tuesday
Wedensday
)
类型转化
- go语言不支持隐式类型转换;32位转64位也不行。
- 别名和原有类型也不支持隐式类型转换;
指针类型
- 不支持指针运算
算数运算符
- 不支持前置的++,–,后置++,–是支持的,其他都 支持
- == 在比较两个数组时,只有相同维度相同元素个数时才可以。
条件和循环
- go只有for没有while,可以直接用for代替while。
- if 语句condition表达式必须是布尔值,condition支持前面赋值
if var declaration; condition{
}
switch {
case i % 2 == 0 :
t.Log("Even")
case i % 2 == 1:
t.Log("Odd")
default:
t.Log("not zero")
}
switch i {
case 0,2:
t.Log("Even")
case 1,3:
t.Log("Odd")
default:
t.Log("not zero")
}
数组
- 定义:数组需要指定类型和元素个数,内存空间是固定。
var arr [3]int
arr := [...]int{1,2,3}
//数组切片,前闭后开
arr[开始索引, 结束索引]
数组遍历
for i:=0; i<len(a);i++ {
t.Log(i, a[i])
}
//类似foreach写法
for key, value := range a{
t.Log(key, value)
}
//不想要下标,用_代替
for _, value := range a{
t.Log( value)
}
切片
- 切片是没有固定长度的,每一次长度变化都会将原来的数据进行拷贝,并分配新的内存空间,
- 不能用 == 比较
var s0 []int
s0 = append(s0, 1)
s := []int{}
s1 := []int{1,2,3,4}
s2 := make([]int, 3, 5) //3是指初始化的元素个数,(用len查看),初始化默认值为0,5是最大长度(用cap查看),违背初始化的元素无法访问,每一次cap增长都是前一次的2倍。
Map
- Map是没有固定长度的,以键值对的形式存在;
- 访问不存在的key时,会返回value类型值的默认值,string是空字符串,int是0,不能通过nil来判断是否存在。
- value可以是一个函数,来构建工厂模式。
m := map[string]int{"s":1,"d":1,"p":1}
m := make(map[string]int, 9)
//Map工厂模式
m := map[int]func(op int)int{}
m[1] = func(op int) int {
return op
}
m[2] = func(op int) int {
return op*op
}
m[3] = func(op int) int {
return op*op*op
}
t.Log(m[1](2), m[2](2), m[3](2))
字符串
- 与其他编程语言的差异
- string是数据格式,不是引用或指针类型
- string是只读的byte slice,len函数可以计算它包含的byte数,而不是字符数
- string的byte数组可以存放任何数据
函数
- 类似python的闭包
func timeSpent(inner func(op int) int) func(op int) int {
return func(n int) int {
start := time.Now()
ret := inner(n)
fmt.Println("time spent: ", time.Since(start).Seconds())
return ret
}
}
func slowFunc(op int) int {
time.Sleep(time.Second *2)
return op
}
defer函数
- 在函数内容定义defer函数,会在函数执行成功之后再运行,一般可用来释放锁。
- 用panic抛出错误,defer函数仍会执行,但是panic后面的程序不再执行
- 多个defer的话,先入后出。
面对对象
- 不支持继承,可以进行复合,但是复合也可以获得相关的方法,但是不能继承那样可以对父类方法进行重写来实现重载。
- 加上*表示指针引用,指针引用和值引用结果是一样的,但是指针引用不会占用新的内存。
type Empleyee struct {
Id int
Name string
Age int
}
func TestMyObject(t *testing.T) {
e := Employee{0, "wc", 24}
e1 := Employee{Id:2, Name:"wen"}
e2 := new(Employee)
e2.Id = 3
e2.Name = "cai"
t.Log(e, e1, e2)
}
接口
- 接口是非入侵性的,实现是不依赖于接口定义的
- 接口的定义是可以包含在接口使用者包中
type Program interface {
Write() string
}
type GoProgram struct {
Id string
Name string
}
func (g *GoProgram) Write() string {
return "hello world"
}
func TestInterface(t *testing.T) {
var p Program
p = new(GoProgram)
t.Log(p, p.Write())
}
init 函数
- 包在被调用的时候,会先执行init函数
- 一个包中可以存着多个init函数
- 包中init函数的执行顺序与包被调用的依赖关系有关
包
- 包必须放在GOPATH或者GOROOT的src或vender文件夹下面,才可以被调用
- 远程包可以通过go get获取到本地
go get -u https://github.com/easierway/concurrent_map
- go不能同时使用同一个包的不同版本
- go的包管理工具:glide
协程Goroutine
- 协程与线程thread区别:
jdk中stack默认大小是1M,而go中stack是2k - KSE(kernel space entity)对应关系:
Java Thread 是1对1,而go的Groutine是多对多
Goroutine的使用
- 在函数的调用者那里使用,而不是写在函数内部。
- 一定要能够在Goroutine外部能够控制Goroutine的生命周期。
- 要做好超时控制。
如果在Processor调度器依次处理Goroutine协程时,有一个Goroutine协程运行时间很久,那么队列里的其他Goroutine协程会不会等很久?
- 是这样的,go在运行的时候,会开启一个守护进程,它会记录每个Processor调度器完成的Goroutine协程的数量,当发现某个Processor完成的协程数量在一段时间内都没有发生变化时,它会往这个协程的任务栈中插入一个特殊的标记,当该协程执行到标记时就会中断下来,插入到等待协程队列的队尾,切换成别的协程,继续运行。
GPM并发机制
- 当某一个协程被系统中断了,例如:IO,需要等待的时候,为了提高整体的并发,Processor会把自己移动到另一个可使用的系统线程当中,继续执行它所挂的协程队列里的其他协程,当这个被中断的协程完成之后被唤醒,它会把自己加入到某一个协程等待队列里。
- 协程中断的时候,它在寄存器中的运行状态,也会保存到协程对象里,当协程再次运行时,这些会重新写入寄存器继续运行。
共享内存并发机制
- go中的协程不是线程安全的,需要用锁来进行保护,使用锁的时候同时会用defer来最后释放锁,防止程序被整体挂起。
- WaitGroup线程同步方法,当wait中的程序执行完成之后才能继续往下执行。
func TestGroutine(t *testing.T) {
var mut sync.Mutex
var wg sync.WaitGroup
sum := 0
for i:=1; i<=10000; i++ {
wg.Add(1)
go func (i int) {
defer func() {
mut.Unlock()
}()
mut.Lock()
sum ++
wg.Done()
}(i)
}
wg.Wait()
//time.Sleep(time.Microsecond * 1000 )
t.Logf("counter is %d", sum)
}
Channel
- 对于同一个Channel,发送操作和接受操作都是互斥的。
- 整个发送操作和整个接受操作都是原子性的不可分割。发送操作分为:复制元素值,和放置副本到队列中两个操作。接受操作分为:复制Channel内元素值,放置副本到接受方,删除Channel内元素值三个操作。
- 发送操作和接受操作在完成之前都是会被阻塞的。
- 往Channel中发送nil会阻塞,Channel是引用类型,所以它的零值就是nil。当我们只声明该类型的变量但没有用make函数对它进行初始化时,该变量的值就会是nil。
- 通道一旦关闭,再对它进行发送操作,就会引发 panic。
- 关闭一个已经关闭了的通道,也会引发 panic。
非阻塞的Channel,长度为0,同步执行
- 发送阻塞直到数据被接收,接收阻塞直到读到数据。
- 发送方和接收方必须同时在线,发送方发送的过程中,接收方先一直等待。
- 数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。
阻塞的Channel,长度大于0,异步执行
- 当缓冲满时发送阻塞,当缓冲空时接收阻塞。
- 设置一个Channel容量,在容量没有满时,发送方可以一直发新的消息,如果容量满了发送消息会进行阻塞,需要等待接收方处理才能发送。
- 通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发送操作。
channel的关闭
- 向关闭的channel发送数据,会导致panic
- channel可以返回两个值,第一个是数据第二是channel的状态,状态为true时表示正常接收,状态为false时表示通道关闭。
- 所有channel的接受者都会在channel关闭时,立即从阻塞的等待中返回。
func dataProducer(ch chan int, wg *sync.WaitGroup) {
go func() {
for i:=0; i<10; i++ {
ch <- i
}
close(ch)
wg.Done()
}()
}
func dataReceiver(ch chan int, wg *sync.WaitGroup) {
go func() {
for {
if data, ok := <-ch; ok {
fmt.Println(data)
} else {
break
}
}
//for i:=0; i<10; i++ {
// data := <-ch
// fmt.Println(data)
//}
wg.Done()
}()
}
func TestCloseChannel(t *testing.T) {
var wg sync.WaitGroup
ch := make(chan int)
wg.Add(1)
dataProducer(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
wg.Add(1)
dataReceiver(ch, &wg)
wg.Add(1)
}
CSP
- CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。
多路选择和超时
- select实现多渠道的选择,不同渠道之间执行跟代码的顺序无关,一般用来处理超时问题,防止主线程一直阻塞。
func service() string {
time.Sleep(time.Millisecond * 50)
return "Down"
}
func otherTask() {
fmt.Println("working on something else")
time.Sleep(time.Millisecond * 100)
fmt.Println("work down")
}
func AsyncService() chan string {
retCh := make(chan string, 1)
go func() {
ret := service()
fmt.Println("return result")
retCh <- ret
fmt.Println("service exited")
}()
return retCh
}
func TestAsyncService(t *testing.T) {
retCh := AsyncService()
otherTask()
fmt.Println(<-retCh)
}
func TestService(t *testing.T) {
select {
case ret := <-AsyncService():
t.Log(ret)
case <-time.After(time.Millisecond*100):
t.Error("time out!")
}
}
从 panic 被引发到程序终止运行的大致过程是什么
-
我们先说一个大致的过程:某个函数中的某行代码有意或无意地引发了一个 panic。这时,初始的 panic 详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。
-
这也意味着,此行代码所属函数的执行随即终止。紧接着,控制权并不会在此有片刻的停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。
-
这里的最外层函数指的是go函数,对于主 goroutine 来说就是main函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。
-
随后,程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic 详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。
sync
竞态条件
- 目前大部分语言都是通过 通过共享数据的方式来传递信息和协调线程运行,而go强调 用通讯的方式共享数据。
- 一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况。这种情况也被称为竞态条件(race condition),这往往会破坏共享数据的一致性。
- 共享数据的一致性代表着某种约定,即:多个线程对共享数据的操作总是可以达到它们各自预期的效果。为了达到这个效果协调它们对缓冲区的修改,就需要进行同步。
- 同步的用途有两个,一个是避免多个线程在同一时刻操作同一个数据块,另一个是协调多个线程,以避免它们在同一时刻执行同一个代码块。
- 一个线程在想要访问某一个共享资源的时候,需要先申请对该资源的访问权限,并且只有在申请成功之后,访问才能真正开始。
- 而当线程对共享资源的访问结束时,它还必须归还对该资源的访问权限,若要再次访问仍需申请。
- 这时,我们可以说,多个并发运行的线程对这个共享资源的访问是完全串行的。只要一个代码片段需要实现对共享资源的串行化访问,就可以被视为一个临界区(critical section),也就是我刚刚说的,由于要访问到资源而必须进入的那个区域。
- 施加保护的重要手段之一,就是使用实现了某种同步机制的工具,也称为同步工具。
- 在 Go 语言中,可供我们选择的同步工具并不少。其中,最重要且最常用的同步工具当属互斥量(mutual exclusion,简称 mutex)。sync包中的Mutex就是与其对应的类型,该类型的值可以被称为互斥量或者互斥锁。
使用互斥锁的注意事项如下:
- 不要重复锁定互斥锁;
- 不要忘记解锁互斥锁,必要时使用defer语句;
- 不要对尚未锁定或者已解锁的互斥锁解锁;
- 不要在多个函数之间直接传递互斥锁。
当所有的goroutine都阻塞时,会造成deadlock,程序必然会发生panic,而且是无法recover的。而最简单、有效的方式就是让每一个互斥锁都只保护一个临界区或一组相关临界区。
评论
请
登录后发表观点