目录
5.临界资源安全问题:多个线程操作同一个资源会发生临界资源安全问题
1.多线程相关概念
进程:一个程序执行起来会开辟一个或多个进程,至少有一个进程
线程:一个进程可以开辟一个或多个线程,至少有一个线程
并发:同一时间多个线程抢夺cpu资源,交替执行
并行:同一时间多个线程同时执行
2.Go语言中的协程(Goroutine)
协程概念:Go语言使用协程(Goroutine)实现并发,是一个轻量级的线程,一个协程在堆上初始分配的大小只有4K
开启协程:开启协程只需要在调用函数或方法时,在前面加上go关键字即可
注意事项:
1.当开启一个新的协程,程序会立刻回到当前协程继续执行,不会等待协程执行结束后再执行
2.如果main协程先执行完,程序会终止,不会等待其他协程执行结束
func main() {
//开启一个新的协程
go func() {
for i := 0; i < 100; i++ {
fmt.Println("goroutine-->", i)
}
}()
//main协程
for i := 0; i < 100; i++ {
fmt.Println("main-->", i)
}
}
3.主Goroutine
主Goroutine做了哪些操作
1.设定申请的栈空间的最大尺寸,32位计算机:250M,64位计算机:1G
2.初始化
2.1:创建一个特殊的defer语句,用于在主Goroutine退出时做一些善后处理,因为主Goroutine也可能报错
2.2:启动专用于后台清理内存垃圾的Goroutine,也叫Gc线程
2.3:执行main包下的init函数
2.4:执行main函数
2.5:执行完后检查主goroutine是否引发了运行时恐慌,并进行必要的处理
2.6:程序运行完后主goroutine会结束自己的运行
4.runtime包
1.获取系统信息
1.获取GoRoot目录:runtime.GOROOT()
2.获取操作系统:runtime.GOOS
3.获取cpu核心数量:runtime.NumCPU()
2.让出时间片,让其他代码先执行,不一定能让成功:runtime.Gosched()
3.终止当前Goroutine的执行:runtime.Goexit()
//获取系统信息
func main() {
fmt.Println("GoRoot目录:", runtime.GOROOT())
fmt.Println("System:", runtime.GOOS)
fmt.Println("cpu核心数量:", runtime.NumCPU())
}
//让出时间片
func main() {
//开启goroutine
go func() {
for i := 0; i < 10; i++ {
fmt.Println("goroutine-->", i)
}
}()
//主goroutine中的代码
for i := 0; i < 10; i++ {
runtime.Gosched() //让出时间片,不一定能让成功
fmt.Println("main-->", i)
}
}
//终止当前Goroutine的执行
func main() {
go func() {
fmt.Println("start")
runtime.Goexit()
fmt.Println("end")
}()
time.Sleep(time.Second * 2) //休眠两秒,避免主goroutine提前结束,保证goroutine能正常执行完
}
5.临界资源安全问题:多个线程操作同一个资源会发生临界资源安全问题
卖票案例:出现了临界资源安全问题
// 总共十张票
var ticket int = 10
// 卖票
func saleTicket(name string) {
for {
if ticket > 0 {
fmt.Println(name, "正在卖第", ticket, "张票")
ticket--
} else {
fmt.Println("票已售完")
break
}
}
}
// 四个窗口卖票
func main() {
go saleTicket("窗口一")
go saleTicket("窗口二")
go saleTicket("窗口三")
go saleTicket("窗口四")
time.Sleep(time.Second * 2) //休眠2秒,防止主goroutine提前结束
}
6.互斥锁:解决临界资源安全问题"不推荐"
Go并发编程有这么一句话:"不要以共享内存的方式去通信,而要以通信的方式去共享内存",锁机制就是以共享内存的方式去通信,而通道是以通信的方式去共享内存;
案例:使用sync.Mutex锁解决多线程临界资源安全问题
// 总共十张票
var ticket int = 10
// 1.定义一个锁
var mutex sync.Mutex
// 卖票
func saleTicket(name string) {
for {
//2.操作共享资源之前先上锁
mutex.Lock()
if ticket > 0 {
fmt.Println(name, "正在卖第", ticket, "张票")
ticket--
//3.操作完共享资源后解锁
mutex.Unlock()
} else {
fmt.Println("票已售完")
//3.程序结束也要解锁
mutex.Unlock()
break
}
}
}
// 四个窗口卖票
func main() {
go saleTicket("窗口一")
go saleTicket("窗口二")
go saleTicket("窗口三")
go saleTicket("窗口四")
time.Sleep(time.Second * 2) //休眠2秒,防止主goroutine提前结束
}
7.同步等待组(计数器):优化上面主线程睡眠2秒的问题
上面代码主线程睡眠两秒是为了解决主线程提前结束问题,如果睡眠两秒后,协程还是没有处理完,这时候代码就会终止,怎么办?
可以使用同步等待组解决这个问题
1.定义一个同步等待组:var wg sync.WaitGroup
2.每执行完一个协程,协程数减一:defer wg.Done()
3.等待协程数归0自动往下执行:wg.Wait()
案例:使用同步等待组解决主线程何时结束问题
// 定义一个锁
var mutex sync.Mutex
// 1.定义一个同步等待组
var wg sync.WaitGroup
// 总共十张票
var ticket int = 10
// 卖票
func saleTicket(name string) {
//2.每执行完一个协程数减一
defer wg.Done()
for {
//操作共享资源之前先上锁
mutex.Lock()
if ticket > 0 {
fmt.Println(name, "正在卖第", ticket, "张票")
ticket--
//操作完共享资源后解锁
mutex.Unlock()
} else {
fmt.Println("票已售完")
//程序结束也要解锁
mutex.Unlock()
break
}
}
}
// 四个窗口卖票
func main() {
//设置协程数
wg.Add(4)
go saleTicket("窗口一")
go saleTicket("窗口二")
go saleTicket("窗口三")
go saleTicket("窗口四")
//3.等待协程数归0自动往下执行
wg.Wait()
}
8.通道
通道的使用
1.声明通道:只声明不创建是nil
var 通道名 chan 通道里传输的数据类型2.创建通道
通道名 =make(chan 通道里传输的数据类型)3.向通道中写入数据
通道名 <- 数据4.从通道中拿数据
变量名 := <- 通道名
func main() {
//声明通道
var ch chan bool
//创建通道
ch = make(chan bool)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
//协程运行完后往通道写入一个数据
ch <- true
}()
//从通道中取出数据,证明协程执行结束了
data := <-ch
fmt.Println("程序运行结束", data)
}
9.通道的注意事项
1.通道是阻塞的,只能存储一个数据,想再次存入,只能先取出通道中的数据,否则会产生死锁
2.如果创建一个通道后,直接读取数据,没有向通道中写入数据,程序会产生死锁
3.通道是同步的,同一时间只能有一个goroutine操作通道
4.通道是处理不同goroutine之间的通信,通道的发送和接收必须在不同goroutine,否则会产生死锁
10.关闭通道
//通道使用完后关闭通道
func main() {
// 创建一个通道
ch := make(chan int)
//协程向通道中写入数据:一次写入一条,主协程取出来后继续写入
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
//写入结束,关闭通道
close(ch)
}()
//从通道中取出数据
for v := range ch {
fmt.Println(v)
}
}
11.缓冲通道
/*
缓冲通道:带有缓冲区的通道,缓冲区满时才会阻塞等待goroutine取出通道的数据
通道名 := make(chan 通道中传输的数据类型, 通道大小)
*/
func main() {
//定义一个普通的通道
ch1 := make(chan int)
fmt.Println(len(ch1), cap(ch1)) //0 0
//定义一个缓冲通道
ch2 := make(chan int, 5)
fmt.Println(len(ch2), cap(ch2)) //0 5
//向通道中存入数据
go func() {
for i := 0; i < 10; i++ {
ch2 <- i
fmt.Println("写入的数据:", i)
}
//关闭通道
close(ch2)
}()
//停顿两秒观察效果:发现一次写入了5个数据到通道,才来读取的
time.Sleep(time.Second * 2)
//读取数据
for v := range ch2 {
fmt.Println("读取出的数据:", v)
}
}
12.定向通道
定向通道:只能读或只能写的通道,也叫单项通道;
一般作为函数的参数,控制这个函数只能向通道中写入数据或只能从通道中读取数据
1.只能向通道中写入数据; 通道名 := make(chan <- 通道中传输数据的类型)
2.只能从通道中读取数据; 通道名 := make(<- chan 通道中传输数据的类型)
func main() {
//定义一个可读可写的通道
ch := make(chan int)
go writeOnly(ch)
go readOnly(ch)
time.Sleep(time.Second) //睡眠一秒,防止main协程提前结束
}
// 只写
func writeOnly(ch chan<- int) {
ch <- 100
}
// 只读
func readOnly(ch <-chan int) {
data := <-ch
fmt.Println(data)
}
13.Select
/*
select:是一个控制结构,会在多个满足条件的条件中,随机选择一个执行;如果没有条件满足就会阻塞,知道有条件满足执行
注意:每个case都必须是一个通道的操作
*/
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
ch1 <- 100
}()
go func() {
ch2 <- 200
}()
select {
case data1 := <-ch1:
fmt.Println(data1)
case data2 := <-ch2:
fmt.Println(data2)
/*default: 写了default不会阻塞
fmt.Println("没匹配到")*/
}
}
14.Timer定时器
/*
1.创建一个定时器:在指定时间后触发;定时器名 := time.NewTimer(时间) 返回的是一个定时器
2.提前关闭定时器;定时器名.Stop()
3.创建一个定时器:在指定时间后触发;定时器名 := time.NewTimer(时间) 返回的是一个通道
4.定时执行一个函数:time.AfterFunc(时间,函数)
*/
func main() {
//1.创建一个定时器
fmt.Println("当前时间:", time.Now())
timer := time.NewTimer(time.Second * 3)
timerChan := timer.C
fmt.Println("定时器执行时间:", <-timerChan)
//---------------
//2.提前关闭定时器
timer2 := time.NewTimer(time.Second * 5)
go func() {
fmt.Println("goroutine取到了数据:", <-timer2.C)
}()
flag := timer2.Stop()
if flag {
fmt.Println("timer2停止了")
}
//3.创建一个定时器
fmt.Println("当前时间:", time.Now())
timeChan2 := time.After(time.Second * 3)
fmt.Println("定时器2执行时间:", <-timeChan2)
//4.定时执行一个函数
time.AfterFunc(time.Second*3, func() {
fmt.Println("函数执行了")
})
time.Sleep(time.Second * 5) //主程序睡眠5秒,防止提前结束
}