【Go】十七、进程、线程、协程

文章目录

1、进程、线程

  • 进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域
  • 一个进程下面有多个线程,干着不同的活儿

进程与线程,好比打开360,同时进行木马查杀和电脑清理,360是一个进程,后面两个则是两个线程

补充,关于并行和并发:

  • 并发:多线程同时/交替操作同一资源类
  • 并行:多线程同时操作多个资源类

示意图:

2、协程

  • 协程是一种用户态的轻量级线程
  • 又称微线程、纤程
  • 是一种单线程下的并发
  • 协程中只有一个线程在执行(协程的本质是个单线程)

在一个单独的线程中,出现IO操作时,此时可控制单线程下的多个任务,在另一个任务IO阻塞时,将其寄存器上下文和栈保存到某个地方,去切到另一个任务继续计算。如此,就保证了线程最大程度的处于就绪状态,执行效率变高。

协程的引入,给CPU一种:该线程好像是一直在计算,io比较少的错觉,从而会更多的将cpu的执行权限分配给我们的线程
线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级

感觉在线程的基础上再细分,还是因为后面计算机在硬件上发展快了,如此再做切换,可以更加提升效率。

go 复制代码
package main
import(
        "fmt"
        "strconv"
        "time"
)
func test(){
        for i := 1;i <= 10;i++ {
                fmt.Println("hello golang + " + strconv.Itoa(i))
                //阻塞一秒:
                time.Sleep(time.Second)
        }
}
func main(){//主线程
        go test() //开启一个协程
        for i := 1;i <= 10;i++ {
                fmt.Println("hello 9527 + " + strconv.Itoa(i))
                //阻塞一秒:
                time.Sleep(time.Second)
        }
}

如上,主线程中,开启一个协程,协程每1秒输出hello golang,主线程每一秒输出一次hello 9527,主线程和协程在同时执行,且属于同一个线程(主线程)。运行:


3、主死从随

即:

  • 主线程执行结束退出了,则即使其下的协程没有执行完,也要跟着陪葬
  • 当然协程如果提前在主线程之前结束,那就正常自己结束就好
go 复制代码
package main
import(
        "fmt"
        "strconv"
        "time"
)
func test(){
        for i := 1;i <= 1000;i++ {
                fmt.Println("hello golang + " + strconv.Itoa(i))
                //阻塞一秒:
                time.Sleep(time.Second * 1)
        }
}
func main(){//主线程
        go test() //开启一个协程
        for i := 1;i <= 10;i++ {
                fmt.Println("hello msb + " + strconv.Itoa(i))
                //阻塞一秒:
                time.Sleep(time.Second * 1)
        }
}

4、启动多个协程

go 复制代码
package main
import(
        "fmt"
        "time"
)
func main(){
        //匿名函数+外部变量 = 闭包
        for i := 1;i <= 5;i++ {
                //启动一个协程
                //使用匿名函数,直接调用匿名函数
                go func(n int){
                        fmt.Println(n)
                }(i)
        }
        time.Sleep(time.Second * 2)
}

5、使用WaitGroup控制协程退出

思想类似Java的计数器那些JUC辅助类,用来解决主线程在子协程结束后自动结束,即阻塞线程,等等所有协程执行完。核心方法:

go 复制代码
//协程开始的时候加1操作
func (wg*WaitGroup) Add(delta int)

//协程执行完后减一
func(wg *WaitGroup) Done()

//WaitGroup为0前,阻塞线程
func(wg *WaitGroup) Wait()

示例:

go 复制代码
package main
import(
        "fmt"
        "sync"
)
var wg sync.WaitGroup //只定义无需赋值
func main(){
        //启动五个协程
        for i := 1 ;i <= 5;i++ {
                wg.Add(1) //协程开始的时候加1操作
                go func(n int){
                        fmt.Println(n)
                        wg.Done()  //协程执行完成减1
                }(i)
        }
        //主线程一直在阻塞,什么时候wg减为0了,就停止
        wg.Wait()
}

当然也可用defer关键字去减一

go 复制代码
package main
import(
        "fmt"
        "sync"
)
var wg sync.WaitGroup 
func main(){
        for i := 1 ;i <= 5;i++ {
                wg.Add(1) 
                go func(n int){
                        defer wg.Done()	//!!!这里
                        fmt.Println(n)		
                }(i)
        }
        wg.Wait()
}

可以最开始在知道协程次数的情况下先Add操作

go 复制代码
package main
import(
        "fmt"
        "sync"
)
var wg sync.WaitGroup 
func main(){
        wg.Add(5)		//这里!!!
        for i := 1 ;i <= 5;i++ {
                go func(n int){
                        defer wg.Done()
                        fmt.Println(n)		
                }(i)
        }
        wg.Wait()
}

注意Add的个数和协程的个数要一致。

6、多协程操作同一个数据

开一个协程去做一万次+1,再开一个协程去做一万次-1

go 复制代码
package main
import(
        "fmt"
        "sync"
)
//定义一个变量:
var totalNum int
var wg sync.WaitGroup //只定义无需赋值
func add(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                totalNum = totalNum + 1
        }
}

func sub(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                totalNum = totalNum - 1
        }
}

func main(){
        wg.Add(2)
        //启动协程
        go add()
        go sub()
        wg.Wait()
        fmt.Println(totalNum)
}

运行的结果始终不为0:

多协程操作同一个数据的问题:按以下1.2.3.4.5.6的步骤,就发现做了一次+1,一次-1,结果为-1

修复这个问题,让一个协程执行逻辑的时候,另一个协程不执行 ⇒ 互斥锁

7、互斥锁

引入sync包:

go 复制代码
//加入互斥锁:
var lock sync.Mutex
go 复制代码
package main
import(
        "fmt"
        "sync"
)
//定义一个变量:
var totalNum int
var wg sync.WaitGroup //只定义无需赋值
//加入互斥锁:
var lock sync.Mutex
func add(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                //加锁
                lock.Lock()
                totalNum = totalNum + 1
                //解锁:
                lock.Unlock()
        }
}
func sub(){
        defer wg.Done()
        for i := 0 ;i < 100000;i++{
                //加锁
                lock.Lock()
                totalNum = totalNum - 1
                //解锁:
                lock.Unlock()
        }
}
func main(){
        wg.Add(2)
        //启动协程
        go add()
        go sub()
        wg.Wait()
        fmt.Println(totalNum)
}

8、读写锁

互斥锁在读多写少的场景不适合,性能低下,采用读写互斥,但读读共享的读写锁。

go 复制代码
//加入读写锁:
var lock sync.RWMutex
go 复制代码
lock.RLock()//读锁
lock.RUnlock()

示例:

go 复制代码
package main
import(
        "fmt"
        "sync"
        "time"
)
var wg sync.WaitGroup //只定义无需赋值
//加入读写锁:
var lock sync.RWMutex
func read(){
        defer wg.Done()
        lock.RLock()//如果只是读数据,那么这个锁不产生影响,但是读写同时发生的时候,就会有影响
        fmt.Println("开始读取数据")
        time.Sleep(time.Second)
        fmt.Println("读取数据成功")
        lock.RUnlock()
}
func write(){
        defer wg.Done()
        lock.Lock()
        fmt.Println("开始修改数据")
        time.Sleep(time.Second * 10)
        fmt.Println("修改数据成功")
        lock.Unlock()
}
func main(){
        wg.Add(6)
        //启动协程 ---> 场合:读多写少
        for i := 0;i < 5;i++ {
                go read()
        }
        go write()
        wg.Wait()
}

运行发现:写的时候不能读,但读的时候可以共享读:

9、defer+recover优化多协程

多协程工作,一个协程出现panic,整个程序崩溃。引入defer+recover,让协程即使出现错误,也不影响主线程和其他协程的执行:

go 复制代码
ackage main
import(
        "fmt"
        "time"
)
//输出数字:
func printNum(){
        for i := 1;i <= 10;i++{
                fmt.Println(i)
        }
}
//做除法操作:
func devide(){
        defer func(){
                err := recover()
                if err != nil{
                        fmt.Println("devide()出现错误:",err)
                }
        }()
        num1 := 10
        num2 := 0
        result := num1 / num2
        fmt.Println(result)
}
func main(){
        //启动两个协程:
        go printNum()
        go devide()
        time.Sleep(time.Second * 5)
}

运行:

相关推荐
求知若饥9 分钟前
NestJS 项目实战-权限管理系统开发(六)
后端·node.js·nestjs
数据小爬虫@12 分钟前
如何高效利用Python爬虫按关键字搜索苏宁商品
开发语言·爬虫·python
ZJ_.14 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
Narutolxy19 分钟前
深入探讨 Go 中的高级表单验证与翻译:Gin 与 Validator 的实践之道20241223
开发语言·golang·gin
Hello.Reader26 分钟前
全面解析 Golang Gin 框架
开发语言·golang·gin
禁默37 分钟前
深入浅出:AWT的基本组件及其应用
java·开发语言·界面编程
Code哈哈笑1 小时前
【Java 学习】深度剖析Java多态:从向上转型到向下转型,解锁动态绑定的奥秘,让代码更优雅灵活
java·开发语言·学习
gb42152871 小时前
springboot中Jackson库和jsonpath库的区别和联系。
java·spring boot·后端
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
qq_433618441 小时前
shell 编程(二)
开发语言·bash·shell