● 进程是程序在 *** 作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
● 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
● 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
● 一个程序至少有一个进程,一个进程至少有一个线程
● 多线程程序在单核上运行,就是并发
● 多线程程序在多核上运行,就是并行
● 并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询 *** 作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
● 并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
● Go主线程(有程序员直接称为线程/也可理解为进程):一个Go线程上,可以起多个协程,可以这样理解,协程是轻量级的线程【编译器做优化】
● Go协程的特点
○ 有独立的栈空间
○ 共享程序堆空间
○ 调度由用户控制
○ 协程是轻量级的线程
1.4 goroutine快速入门
(1)案例说明
编写一个程序,完成如下功能:
在主线程(可以理解为进程)中,开启一个goroutine,该协程每隔1秒输出“hello,world”
在主线程也每隔一秒输出“hello,golang”,输出10次后,退出程序
要求主线程和goroutine同时执行
package main
import (
"fmt"
"strconv"
"time"
)
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() 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)
}
}
(3)主线程和协程执行流程图
(4)快速入门小结
● 主线程是以一个物理线程,直接作用在cpu上。是重量级的,非常消耗cpu资源
● 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对较小
● golang的协程机制是个重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就凸显出golang在并发上的优势了
M: *** 作系统的主线程(是物理线程)
P:协程执行需要的上下文
G:协程
● 当前程序有三个M,如果三个M都在一个cpu运行,就是并发,如果在不同的cpu运行就是并行
● M1,M2,M3正在执行一个G,M1的协程队列有三个,M2的协程队列有3个,M3协程队列有2个
● 从上图可以看到: Go的协程是轻量级的线程,是逻辑态的,Go可以容易的起上万个协程。
● 其它程序c/java的多线程,往往是内核态的,比较重量级,几千个线程可能耗光cpu
● 分成两个部分来看
● 原来的情况是MO主线程正在执行G0协程,另外有三个协程在队列等待
● 如果GO协程阻塞,比如读取文件或者数据库等,这时就会创建M1主线程(也可能是从已有的线程池中取出M1),并且将等待的3个协程挂到M1下开始执行, MO的主线程下的GO仍然执行文件io的读写。
● 这样的MPG调度模式,可以既让GO执行,同时也不会让队列的其它协程一直阻塞,仍然可以并发/并行执行。
● 等到G0不阻塞了,MO会被放到空闲的主线程继续执行(从已有的线程池中取),同时G0又会被唤醒。
package main
import (
"fmt"
"runtime"
)
func main() {
//获取当前cpu数 返回的是本地机器的逻辑cpu数
num := runtime.NumCPU()
//设置运行go程序cpu数
runtime.GOMAXPROCS(num - 1)
fmt.Println("num=", num)
}
● go1.8后,默认让程序运行在多个核上,可以不用设置了
● go1.8前,需要设置
需求:计算1-200的各个数的阶乘,并把各个数的阶乘放入到map中,最后显示出来,需要使用goroutine完成
2.2初步实现,但有问题 package main
import "fmt"
var myMap = make(map[int]int, 10)
//test函数用来计算阶乘并将结果放入到map中
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
myMap[n] = res
}
func main() {
//开启多个协程完成这个任务【200个】
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10秒钟
time.Sleep(time.Second * 10)
//输出结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
直接运行报错,因为对map并发的写
如何判断程序是否存在资源竞争问题,在编译程序时,增加一个参数 -race 即可
go build -race main.go
如果存在资源竞争问题,会打印出来
如果不休眠,程序运行不完整
● 全局变量的互斥锁
● 使用管道channel来解决
● 因为对全局变量myMap没有加锁,因此会出现资源争夺问题,代码会出现错误,提示fatal error: concurrent map writes错误
● 解决方法:加入互斥锁
package main
import (
"fmt"
"sync"
)
var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁
//sync 是包
//Mutex 是互斥
lock sync.Mutex
)
//test函数用来计算阶乘并将结果放入到map中
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//加锁
lock.Lock()
myMap[n] = res
//解锁
lock.Unlock()
}
func main() {
//开启多个协程完成这个任务【200个】
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10秒钟
time.Sleep(time.Second * 10)
//输出结果
//加锁
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
//解锁
lock.Unlock()
}
没有报错
为什么输出结果也要加锁呢?
因为主线程不知道协程什么时候执行完,因此底层可能仍然出现资源争夺
map[n]的结果为0,是因为结果超出int范围了
● 前面使用全局变量加锁同步来解决goroutine的通讯,但不完美
● 主线程在等待所有goroutine全部加载完成的时间很难确定,我们这里设置10秒,仅仅是估算
● 如果主线程休眠时间长了,会加长等待时间,如果的等待时间短了,可能还有goroutine处于工作状态,这时也会随主线程的退出而销毁
● 通过全局变量加锁同步来实现通讯,也并不利于多个协程对全局变量的读写 *** 作
● channel本质就是一个数据结构-队列
● 数据是先进先出
● 线程安全,多个goroutine访问时,不需要加锁,channel本身是线程安全的
● channel有类型,一个string的channel只能存放string类型的数据
var 变量名 chan 数据结构
举例:
var intChan chan int
var mapChan chan map[int]string
var perChan chan Person
var perChan2 chan *Person
说明:
channel是引用类型
channel必须初始化才能写入数据,即make后才能使用
管道是有类型的,intChan只能写入整数int
2.8 管道初始化
package main
import "fmt"
func main() {
//1.创建一个可以存放三个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2.intChan是什么
fmt.Printf("intChan的值是=%v\n", intChan) //intChan的值是=0xc0000d4080
//3.向管道写入数据
intChan <- 19
intChan <- 12
intChan <- 23
//intChan<-50//报错,当我们给管道写入数据时不能超过其容量
//4.管道长度和容量
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan)) //channel len=3 cap=3
//5.从管道中读取数据
var num int
num = <-intChan
fmt.Println("num=", num) //num= 19
fmt.Printf("channel len=%v cap=%v\n", len(intChan), cap(intChan)) //channel len=2 cap=3
//6.在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取出就会报错deadlock
num2 := <-intChan
num3 := <-intChan
//num4 := <-intChan//报错
fmt.Println(num2, num3) //12 23
}
注意事项:
● channel中只能存放指定的数据类型
● channel数据放满后,就不能再放了
● 如果从channel取出数据后,可以继续放
● 在没有使用协程的情况下,如果channel数据取完了,再取,就会报dead lock
注意:
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)
cat1 := Cat{Name: "tom", Age: 10}
cat2 := Cat{Name: "tom~~", Age: 20}
allChan <- cat1
allChan <- cat2
allChan <- 121
allChan <- "abc"
//读取
cat111 := <-allChan
cat222 := <-allChan
v1 := <-allChan
v2 := <-allChan
fmt.Println(cat111, cat222, v1, v2) //{tom 10} {tom~~ 20} 121 abc 这样输出没有问题
//fmt.Println("Cat Name:",cat111.Name)//编译就报错cat111.Name undefined (type interface{} has no field or method
//如果要拿到name,必须使用断言
a := cat111.(Cat)
fmt.Println("Cat Name:", a.Name) //Cat Name: tom
}
2.10 channel的关闭
使用内置函数close就可以关闭channel,当channel关闭后,就不能再向channel写数据了,但是仍然可以从该channel读取数据
package main
import "fmt"
func main() {
intChan := make(chan int, 3)
intChan <- 23
intChan <- 34
close(intChan)
//关闭后如果写入会报错
//intChan <- 45 //panic: send on closed channel
// 关闭后可以正常读取数据
n1 := <-intChan
fmt.Println(n1) //23
}
2.11channel的遍历
channel支持for-range的方式进行遍历。需要注意两点
● 在遍历时,如果channel没有关闭,则会出现deadlock的错误
● 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历
package main
import "fmt"
func main() {
intChan1 := make(chan int, 100)
for i := 1; i <= 100; i++ {
intChan1 <- i * 2
}
//遍历管道不能用普通的for循环,因为长度一直在变化
//在遍历时,如果channel没有关闭,则会出现deadlock的错误
// 打印完数据并报fatal error: all goroutines are asleep - deadlock!
//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完成后,就会退出遍历
close(intChan1)
for v := range intChan1 {
fmt.Println("v=", v)
}
}
2.12 应用实例
使用goroutine和channel协同工作完成案例
要求:
● 开启一个writeData协程,向管道intChan中写入50个整数
● 开启一个readData协程,向管道intChan中读取writeData写入的数据
● 注意:writeData和readData *** 作的是同一个管道
● 主线程需要等待writeData和readData协程都完成后才能退出
思路分析:
package main
import "fmt"
//writeData
func writeData(intChan chan int) {
for i := 1; i <= 50; i++ {
intChan <- i
fmt.Println("writeData:", i)
}
close(intChan)
}
//readData
func readData(intChan chan int, exitChan chan bool) {
for {
v, ok := <-intChan
if !ok {
break
}
fmt.Println("readData 读到的数据为:", v)
}
//数据读取完成后,即任务完成
exitChan <- true
close(exitChan)
}
func main() {
//创建2个管道
intChan := make(chan int, 10)
exitChan := make(chan bool, 1)
go writeData(intChan)
go readData(intChan, exitChan)
for {
_, ok := <-exitChan
if !ok {
break
}
}
}
2.13阻塞
如果将上面代码的读数据注释go readData(intChan, exitChan),就会发生阻塞
运行结果:
writeData: 1
writeData: 2
writeData: 3
writeData: 4
writeData: 5
writeData: 6
writeData: 7
writeData: 8
writeData: 9
writeData: 10
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:37 +0x92
goroutine 19 [chan send]:
main.writeData(0x0)
C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:8 +0x3b
created by main.main
C:/Project/GolangProject/src/go_code/Project01/chapter16/demo6/main.go:33 +0x86
exit status 2
如果程序运行发现一个管道只有写,没有读,则该管道会发生阻塞。写管道和读管道的频率不一致无所谓
2.14 应用goroutine和channel需求:统计1-8000中,哪些是素数?
思路:
● 传统:使用一个循环,循环判断
● 使用并发/并行:将统计素数的任务分配给多个goroutine去完成,完成任务时间短
package main
import (
"fmt"
)
//将1-8000写入管道
func putNum(intChan chan int) {
for i := 1; i <= 8000; i++ {
intChan <- i
}
close(intChan)
}
//求素数,如果是就放到primeChan管道
func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
var flag bool
for {
num, ok := <-intChan
if !ok { //取不到了
break
}
flag = true
//判断num是不是素数
for i := 2; i < num; i++ {
if num%i == 0 { //说明概述不是素数
flag = false
break
}
}
if flag {
primeChan <- num
}
}
fmt.Println("有一个primeNu协程因为取不到数据退出!!!")
exitChan <- true
}
func main() {
intChan := make(chan int, 1000)
primeChan := make(chan int, 2000)
exitChan := make(chan bool, 4)
start := time.Now().Unix()
//开启一个协程,向intChan中放入1-80000
go putNum(intChan)
//开启四个协程,从intChan取出数据,并判断是否为素数,如果是就放到primeChan
for i := 0; i < 4; i++ {
go primeNum(intChan, primeChan, exitChan)
}
//主线程直接处理
go func() {
for i := 0; i < 4; i++ {
<-exitChan
}
end := time.Now().Unix()
fmt.Println("使用协程耗时=", end - start)
//当我们从exitChan取出4个结果,就可以放心的关闭primeChan
close(primeChan)
}()
//遍历primeChan,取出结果
for {
res, ok := <-primeChan
if !ok {
break
}
fmt.Println("素数=", res)
}
fmt.Println("main()线程退出")
}
使用4个协程后,执行速度比普通方法提高至少4倍
2.15 channel只读或只写 package main
import "fmt"
func main() {
//管道可以声明为只读或只写
//1.在默认情况下,管道是双向
//var channel chan int //可读可写
//声明为只写
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2 <- 20
//num:=<-chan2//报错invalid operation: cannot receive from send-only channel chan2 (variable of type chan<- int)
fmt.Println("chan2=", chan2)
//声明为只读
var chan3 <-chan int
num2 := <-chan3
// chan3<-10//报错
fmt.Println("num2=", num2)
}
2.16 使用select可以解决从管道取数据的阻塞问题
package main
import (
"fmt"
"strconv"
)
func main() {
//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" + strconv.Itoa(i)
}
//label
//传统的方法在遍历管道时,如果不关闭会阻塞而导致死锁deadlock
//在实际开发中,我们不好确定什么时候关闭管道,可以用select方式解决
for {
select {
//如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
case v := <-intChan:
fmt.Println("从intChan读取的数据:", v)
case v := <-stringChan:
fmt.Println("从stringChan读取的数据:", v)
default:
fmt.Println("都取不到数据:")
return
//或使用标签 break label
}
}
}
2.17 goroutine中使用recover解决出现panic问题
如果一个协程出现了panic,如果没有捕获这个panic就会造成整个程序崩溃
这时我们需要在协程中使用revover来捕获panic进行处理,这样即使这个协程发生问题,主线程不会受影响,可以继续执行
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 10; i++ {
fmt.Println("hello,world", i)
}
}
func test() {
//定义一个map,没有make就赋值
var myMap map[int]string
myMap[0] = "golang" //报错
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main()ok", i)
time.Sleep(time.Second)
}
}
不处理错误,程序报错并停止
解决
package main
import (
"fmt"
"time"
)
func sayHello() {
for i := 0; i < 10; i++ {
fmt.Println("hello,world", i)
}
}
func test() {
defer func() {
//捕获test抛出的panic
if err := recover(); err != nil {
fmt.Println("test()发生错误", err)
}
}()
//定义一个map,没有make就赋值
var myMap map[int]string
myMap[0] = "golang" //报错
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main()ok", i)
time.Sleep(time.Second)
}
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)