goroutine 和 channel

goroutine-看一个需求

需求:要求统计 1-9000000000 的数字中,哪些是素数?

分析思路:

  1. 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。[很慢]
  2. 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到goroutine.【速度提高 4 倍】

goroutine-基本介绍

进程和线程介绍

程序、进程和线程的关系

并发和并行

并发和并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行

Go 协程和 Go 主线程

Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个 Go 线程上,可以起多个协程,你可以这样理解,协程是轻量级的线程[编译器做优化]。

Go 协程的特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

goroutine-快速入门

案例说明

请编写一个程序,完成如下功能:

  1. 在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 1 秒输出 "hello,world"
  2. 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
  3. 要求主线程和 goroutine 同时执行.
package main
import (
"fmt"
"strconv"
"time"
) // 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world"
// 在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
// 要求主线程和goroutine同时执行 //编写一个函数,每隔1秒输出 "hello,world"
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("tesst () hello,world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
} func main() { go test() // 开启了一个协程 for i := 1; i <= 10; i++ {
fmt.Println(" main() hello,golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}

快速入门小结

  1. 主线程是一个物理线程,直接作用在 cpu 上的。是重量级的,非常耗费 cpu 资源。
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
  3. Golang 的协程机制是重要的特点,可以轻松的 开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了

goroutine 的调度模型

MPG 模式基本介绍

解释一下MPG含义:

M(Machine):操作系统的主线程

P(Processor):协程执行需要的资源(上下文context),可以看作一个局部的调度器,使go代码在一个线程上跑,他是实现从N:1到N:M映射的关键

G(Gorountine):协程,有自己的栈。包含指令指针(instruction pointer)和其它信息(正在等待的channel等等),用于调度。一个P下面可以有多个G

MPG 模式运行的状态一

  • P的数量可以通过GOMAXPROCS()来设置,他其实代表了真正的并发度,即有多少个goroutine可以同时运行。P同时也维护着G(协程)的队列(称之为runqueue进程队列)。Go代码中的M每有一个语句被执行,P就在末尾加入一个G(从runqueue队列中取出来的),在下一个调度点(P),就从runqueue队列中取出G。

  • P可以在OS线程(主线程,或者是M)被阻塞时,转到另一个OS线程(M)!Go中的调度器保证有足够的线程来运行所有的P。当启用一个M0中的G0被sysCall(系统调用)的时候,M0下面的P转给另一个线程M1(可以是创建的,也可以是原本就存在的)。M1接受了P(包括P所带的runqueue的队列里面所有状态的G,但不包括已经被syscall的G0),继续运行。而M0会等待执行syscall的G0的返回值。当G0的syscall结束后,他的主线程M0会尝试取得一个P来运行G0,一般情况下,他会从其他的M里面偷一个P过来,如果没有偷到的话就会把G0放到一个Global runqueue(全局进程队列)中,然后把自己(M0)放进线程池或者转为休眠状态。

设置 Golang 运行的 cpu 数

介绍:为了充分了利用多 cpu 的优势,在 Golang 程序中,设置运行的 cpu 数目

package main
import (
"runtime"
"fmt"
) func main() {
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=", cpuNum) //可以自己设置使用多个cpu
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Println("ok")
}

channel(管道)-看个需求

需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。

要求使用 goroutine 完成

分析思路:

  1. 使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题.
  2. 这里就提出了不同 goroutine 如何通信的问题

代码实现

  1. 使用 goroutine 来完成(看看使用 gorotine 并发完成会出现什么问题? 然后去解决)
  2. 在运行某个程序时,如何知道是否存在资源竞争问题。 方法很简单,在编译该程序时,增加一个参数 -race 即可

不同 goroutine 之间如何通讯

  1. 全局变量的互斥锁
  2. 使用管道 channel 来解决

使用全局变量加锁同步改进程序

因为没有对全局变量 m 加锁,因此会出现资源争夺问题,代码会出现错误,提示 concurrent map

writes

解决方案:加入互斥锁

我们的数的阶乘很大,结果会越界,可以将求阶乘改成 sum += uint64(i)

package main
import (
"fmt"
_ "time"
"sync"
) // 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成 // 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的. var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁,
//sync 是包: synchornized 同步
//Mutex : 是互斥
lock sync.Mutex
) // test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) { res := 1
for i := 1; i <= n; i++ {
res *= i
} //这里我们将 res 放入到myMap
//加锁
lock.Lock()
myMap[n] = res //concurrent map writes?
//解锁
lock.Unlock()
} func main() { // 我们这里开启多个协程完成这个任务[200个]
for i := 1; i <= 20; i++ {
go test(i)
} //休眠10秒钟【第二个问题 】
//time.Sleep(time.Second * 5) //这里我们输出结果,变量这个结果
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock() }

为什么需要 channel

  1. 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
  2. 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
  3. 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作

    状态,这时也会随主线程的退出而销毁
  4. 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
  5. 上面种种分析都在呼唤一个新的通讯机制-channel

channel 的基本介绍

  1. channle 本质就是一个数据结构-队列
  2. 数据是先进先出【FIFO : first in first out】
  3. 线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
  4. channel 有类型的,一个 string 的 channel 只能存放 string 类型数据。

定义/声明 channel

var 变量名 chan 数据类型

举例:

var intChan chan int (intChan 用于存放 int 数据)
var mapChan chan map[int]string (mapChan 用于存放 map[int]string 类型)
var perChan chan Person
var perChan2 chan *Person
...

说明

channel 是引用类型

channel 必须初始化才能写入数据, 即 make 后才能使用

管道是有类型的,intChan 只能写入 整数 int

package main
import (
"fmt"
) func main() { //演示一下管道的使用
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3) //2. 看看intChan是什么
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan) //3. 向管道写入数据
intChan<- 10
num := 211
intChan<- num
intChan<- 50
// //如果从channel取出数据后,可以继续放入
<-intChan
intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量 //4. 看看管道的长度和cap(容量)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3 //5. 从管道中读取数据 var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3 //6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock num3 := <-intChan
num4 := <-intChan //num5 := <-intChan fmt.Println("num3=", num3, "num4=", num4/*, "num5=", num5*/) }

管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项

channel 使用的注意事项

  1. channel 中只能存放指定的数据类型
  2. channle 的数据放满后,就不能再放入了
  3. 如果从 channel 取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock

读写 channel 案例演示

package main
import (
"fmt"
) type Cat struct {
Name string
Age int
} func main() { //定义一个存放任意数据类型的管道 3个数据
//var allChan chan interface{}
allChan := make(chan interface{}, 3) allChan<- 10
allChan<- "tom jack"
cat := Cat{"小花猫", 4}
allChan<- cat //我们希望获得到管道中的第三个元素,则先将前2个推出
<-allChan
<-allChan newCat := <-allChan //从管道中取出的Cat是什么? fmt.Printf("newCat=%T , newCat=%v\n", newCat, newCat)
//下面的写法是错误的!编译不通过
//fmt.Printf("newCat.Name=%v", newCat.Name)
//使用类型断言
a := newCat.(Cat)
fmt.Printf("newCat.Name=%v", a.Name) }

channel 的遍历和关闭

channel 的关闭

使用内置函数 close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据

channel 的遍历

channel 支持 for--range 的方式进行遍历,请注意两个细节

  1. 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误
  2. 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。

channel 遍历和关闭的案例演示

package main
import (
"fmt"
) func main() { intChan := make(chan int, 3)
intChan<- 100
intChan<- 200
close(intChan) // close
//这是不能够再写入数到channel
//intChan<- 300
fmt.Println("okook~")
//当管道关闭后,读取数据是可以的
n1 := <-intChan
fmt.Println("n1=", n1) //遍历管道
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan2<- i * 2 //放入100个数据到管道
} //遍历管道不能使用普通的 for 循环
// for i := 0; i < len(intChan2); i++ { // }
//在遍历时,如果channel没有关闭,则会出现deadlock的错误
//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
close(intChan2)
for v := range intChan2 {
fmt.Println("v=", v)
} }

应用示例--channel与goroutine

package main
import (
"fmt"
"time"
) //write Data
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
//放入数据
intChan<- i //
fmt.Println("writeData ", i)
//time.Sleep(time.Second)
}
close(intChan) //关闭
} //read data
func readData(intChan chan int, exitChan chan bool) { for {
v, ok := <-intChan
if !ok {
break
}
time.Sleep(time.Second)
fmt.Printf("readData 读到数据=%v\n", v)
}
//readData 读取完数据后,即任务完成
exitChan<- true
close(exitChan) } func main() { //创建两个管道
intChan := make(chan int, 10)
exitChan := make(chan bool, 1) go writeData(intChan)
go readData(intChan, exitChan) time.Sleep(time.Second * 10)
for {
_, ok := <-exitChan
if !ok {
break
}
} }

应用实例 2-阻塞

若上面的代码,注释掉go readData(intChan, exitChan),会怎样,因为管道有长度,所以当编译器发现一个管道只有写而没有读,改管道会阻塞(读与写的频率不一致没关系)!

应用实例 3

需求:

要求统计 1-200000 的数字中,哪些是素数?这个问题在本章开篇就提出了,现在我们有 goroutine和 channel 的知识后,就可以完成了 [测试数据: 80000]

分析思路:

传统的方法,就是使用一个循环,循环的判断各个数是不是素数【ok】。

使用并发/并行的方式,将统计素数的任务分配给多个(4 个)goroutine 去完成,完成任务时间短。

传统方法,一个协程

package main
import (
"time"
"fmt"
) func main() { start := time.Now().Unix()
for num := 1; num <= 80000; num++ { flag := true //假设是素数
//判断num是不是素数
for i := 2; i < num; i++ {
if num % i == 0 {//说明该num不是素数
flag = false
break
}
} if flag {
//将这个数就放入到primeChan
//primeChan<- num
} }
end := time.Now().Unix()
fmt.Println("普通的方法耗时=", end - start) }

开了四个协程

package main
import (
"fmt"
"time"
) //向 intChan放入 1-8000个数
func putNum(intChan chan int) { for i := 1; i <= 80000; i++ {
intChan<- i
} //关闭intChan
close(intChan)
} // 从 intChan取出数据,并判断是否为素数,如果是,就
// //放入到primeChan
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { //使用for 循环
// var num int
var flag bool //
for {
//time.Sleep(time.Millisecond * 10)
num, ok := <-intChan //intChan 取不到.. if !ok {
break
}
flag = true //假设是素数
//判断num是不是素数
for i := 2; i < num; i++ {
if num % i == 0 {//说明该num不是素数
flag = false
break
}
} if flag {
//将这个数就放入到primeChan
primeChan<- num
}
} fmt.Println("有一个primeNum 协程因为取不到数据,退出")
//这里我们还不能关闭 primeChan
//向 exitChan 写入true
exitChan<- true } func main() { intChan := make(chan int , 1000)
primeChan := make(chan int, 20000)//放入结果
//标识退出的管道
exitChan := make(chan bool, 8) // 4个 start := time.Now().Unix() //开启一个协程,向 intChan放入 1-8000个数
go putNum(intChan)
//开启4个协程,从 intChan取出数据,并判断是否为素数,如果是,就
//放入到primeChan
for i := 0; i < 8; i++ {
go primeNum(intChan, primeChan, exitChan)
} //这里我们主线程,进行处理
//直接
go func(){
for i := 0; i < 8; i++ {
<-exitChan
} end := time.Now().Unix()
fmt.Println("使用协程耗时=", end - start) //当我们从exitChan 取出了4个结果,就可以放心的关闭 prprimeChan
close(primeChan)
}() //遍历我们的 primeChan ,把结果取出
for {
_, ok := <-primeChan
if !ok{
break
}
//将结果输出
//fmt.Printf("素数=%d\n", res)
} fmt.Println("main线程退出") }

结论:使用 go 协程后,执行的速度,理论上比普通方法提高至少 4 倍(我这是两倍)

channel 使用细节和注意事项

  1. channel 可以声明为只读,或者只写性质 【案例演示】
package main
import (
"fmt"
) func main() {
//管道可以声明为只读或者只写 //1. 在默认情况下下,管道是双向
//var chan1 chan int //可读可写 //2 声明为只写
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2<- 20
//num := <-chan2 //error fmt.Println("chan2=", chan2) //3. 声明为只读
var chan3 <-chan int
num2 := <-chan3
//chan3<- 30 //err
fmt.Println("num2", num2) }
  1. 使用 select 可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
"time"
) func main() { //使用select可以解决从管道取数据的阻塞问题 //1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}
//2.定义一个管道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
} //传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock //问题,在实际开发中,可能我们不好确定什么关闭该管道.
//可以使用select 方式可以解决
//label:
for {
select {
//注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
//,会自动到下一个case匹配
case v := <-intChan :
fmt.Printf("从intChan读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan :
fmt.Printf("从stringChan读取的数据%s\n", v)
time.Sleep(time.Second)
default :
fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
//break label
}
}
}
  1. goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题

如果我们开了一个协程,但这个协程出现panic,就会导致整个程序崩溃,这时我们可以在goroutine中使用recover来捕获panic,这样及时协程发生问题,主线程依然不受影响

package main
import (
"fmt"
"time"
) //函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
//函数
func test() {
//这里我们可以使用defer + recover
defer func() {
//捕获test抛出的panic
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
//定义了一个map
var myMap map[int]string
myMap[0] = "golang" //error
} func main() { go sayHello()
go test() for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
} }

go-goroutine 和 channel的更多相关文章

  1. TODO:Go语言goroutine和channel使用

    TODO:Go语言goroutine和channel使用 goroutine是Go语言中的轻量级线程实现,由Go语言运行时(runtime)管理.使用的时候在函数前面加"go"这个 ...

  2. goroutine 加 channel 代替递归调用,突破递归调用的层级限制

    package main import ( "fmt" "github.com/davecgh/go-spew/spew" "github.com/B ...

  3. Go基础--goroutine和channel

    goroutine 在go语言中,每一个并发的执行单元叫做一个goroutine 这里说到并发,所以先解释一下并发和并行的概念: 并发:逻辑上具备同时处理多个任务的能力 并行:物理上在同一时刻执行多个 ...

  4. goroutine和channel

    近期在学习golang的goroutine和channel时候有一些疑惑: 带缓冲的channel和不带缓冲的channel有什么区别? goroutine和主进程的有哪些影响和关系? 多个gorou ...

  5. [转帖]go 的goroutine 以及 channel 的简介.

    进程,线程的概念在操作系统的书上已经有详细的介绍.进程是内存资源管理和cpu调度的执行单元.为了有效利用多核处理器的优势,将进程进一步细分,允许一个进程里存在多个线程,这多个线程还是共享同一片内存空间 ...

  6. Go part 8 并发编程,goroutine, channel

    并发 并发是指的多任务,并发编程含义比较广泛,包含多线程.多进程及分布式程序,这里记录的并发是属于多线程编程 Go 从语言层面上支持了并发的特性,通过 goroutine 来完成,goroutine ...

  7. Go开发[八]goroutine和channel

    进程和线程 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位. 一个进程可以创 ...

  8. Go--关于 goroutine、channel

    Go--关于 goroutine.channel goroutine 协程是一种轻量化的线程,由Go编译器进行优化. Go协程具有以下特点: 有独立的栈空间 共享程序堆中的空间 调度由用户控制 如果主 ...

  9. go并发之goroutine和channel,并发控制入门篇

    并发的概念及其重要性 这段是简单科普,大佬可以跳过 并发:并发程序指同时进行多个任务的程序.在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行 ...

  10. golang的goroutine与channel

    Golang的goroutine是非抢占式的, 令人相当蛋疼! 有痛不能呻吟...只能配合channel在各goroutine之间传递信号来实现抢占式, 而这形成了golang最灵活与最具性能的核心. ...

随机推荐

  1. Zuul 修改 请求头、响应头 (死磕)

    疯狂创客圈 Java 高并发[ 亿级流量聊天室实战]实战系列 [博客园总入口 ] 架构师成长+面试必备之 高并发基础书籍 [Netty Zookeeper Redis 高并发实战 ] 前言 Crazy ...

  2. 【linux命令 】文件特殊权限(SUID、SGID、SBIT)

    chmod 2770 /home/admins,刚看到这个命令,有点不解,后边770分别表示用户,组,其他人,前面的2不知道代表的是什么意思.百度之后发现2是代表八进制数,也是一种权限,它的三个bit ...

  3. Exe4j 打包: this executable was created with an evaluation version of exe4j

    异常 this executable was created with an evaluation version of exe4j   异常.png 问题原因 当前打包使用exe4j未授权 解决方法 ...

  4. koa2 从入门到进阶之路 (七)

    之前的文章我们介绍了一下 koa koa-static静态资源中间件,本篇文章我们来看一下 koa 中 cookie 和 session 的使用. cookie 是存储于访问者的计算机中的变量.可以让 ...

  5. Java 添加Word目录的2种方法

    目录是一种能够快速.有效地帮助读者了解文档或书籍主要内容的方式.在Word中,插入目录首先需要设置相应段落的大纲级别,根据大纲级别来生成目录表.本文中生成目录分2种情况来进行: 1.文档没有设置大纲级 ...

  6. C#中获取指定目录下所有目录的名称、全路径和创建日期

    场景 指定一个路径,根据这个父级路径获取此目录下所有目录的名称.全路径.创建日期等信息. 注: 博客主页: https://blog.csdn.net/badao_liumang_qizhi 关注公众 ...

  7. ASP.NET MVC Action向视图传值之匿名类型

    在使用ASP.NET MVC过程中想必大家都有遇到过一个问题就是我们的Action如何向视图传递匿名类型的值呢,如果不做特殊处理则无法实现. 接下来我们来看一个示例: 在我们的控制中: using S ...

  8. swift多线程定时器

    swift多线程定时器的使用 func countDown(_ timeOut:Int,view: UIView){ var timeout = timeOut let queue:DispatchQ ...

  9. JavaWeb开发——软件国际化(文本元素国际化)

    前几天围绕着JDBC编程进行了系统的学习.现在我们对Java程序数据库操作已经是轻车熟路了.也学会了使用各种框架来帮助我们简化编程. 今天是学习计划的第七天,虽然学习热情没有前几天高涨了.但是,写博客 ...

  10. 2019 DevOps 必备面试题——持续集成篇

    原文地址:https://medium.com/edureka/devops-interview-questions-e91a4e6ecbf3 原文作者:Saurabh Kulshrestha 翻译君 ...