golang 爬虫修炼02 ---协程、互斥锁、读写锁、waitgroup

协程

程序:为了完成特定任务,使用某种语言编写的一组指令的集合,是一段静态的代码

进程:是程序的一次执行过程。正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。进程是动态的,有产生、存在、消亡的过程

线程:进程可进一步细分为线程,是一个程序内部的一条执行路径,若一个进程同一时间并行执行多个线程,就是支持多线程的

协程:又称为微线程、纤程,协程是一种用户态的轻量级线程 作用:在执行A函数的时候可以随时中断去执行B函数,然后中断继续执行A函数(可以自由切换),注意这一切换过程并不是函数调用,过程很像多线程,但是实际只是一个线程,有一个特点就是主死从随(协程随着主线程死亡一起死亡)

类似如下图所示,将消耗时间和资源的io操作放在一边

在这里插入图片描述

对于单线程下,我们不可避免程序中出现i0操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在-个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算。在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的i0操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而会更多的将cpu的执行权限分配给我们的线程(注意:线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级)

一、协程

单个协程

go语言开启协程非常简单,只需在调用函数前加一个go

例: go test()

func test() {
    for i := 1; i <= 10; i++ {
       fmt.Println("hello golang", strconv.Itoa(i))
       //阻塞1s
       time.Sleep(time.Second * 1)
    }
}

func main() { //主线程
    go test() //开启协程
    for i := 1; i <= 10; i++ {
       fmt.Println("hello 秋刀鱼", strconv.Itoa(i))
       //阻塞1s
       time.Sleep(time.Second * 1)
    }
}

在这里加入

time.Sleep(time.Second * 1)

是为了防止主线程死掉导致没有时间给协程

运行结果:

在这里插入图片描述

查看结果就会发现在交替运行

在这里插入图片描述

多个协程
func main() { //主线程

    for i := 0; i <= 5; i++ {
       // 启动多个协程  (使用匿名函数)
       go func() {
          fmt.Println(i)
       }()
    }
    time.Sleep(1 * time.Second)
}

输出

在这里插入图片描述

为什么输出的不是1 2 3 4 5 6

这是因为for循环在这里是主线程,协程输出的是i变量是共享主线程的i,可能导致这种情况

想要输出无重复,直接将协程的匿名函数加一个变量,将i传入就去就行

二、WaitGroup

WaitGroup用于等待一组线程的结束,父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时主线程里可以调用Wait方法阻塞至所有线程结束。—》解决主线程在子协程结束后自动结束

阻塞主线程,等待协程结束一起结束
// 只定义无需赋值
var wg sync.WaitGroup

func main() { //主线程
    for i := 1; i <= 5; i++ {
       wg.Add(1) //协程开始的时候加1
       go func(n int) {
          fmt.Println("hello world", n)
          wg.Done() //协程结束的时候减1
       }(i)
    }
    // 阻塞主线程  当wg减为0的时候阻塞就停止
    wg.Wait()
}

var wg sync.WaitGroup 首先定义一个变量 数据类型为sync包内一个名为WaitGroup的结构体

wg.Add(1)然后在计数器内加入协程数量

wg.Done()每结束一次协程就减去1

wg.Wait()阻塞主线程 当wg减为0的时候阻塞就停止

多个协程操作同一个数据案例

多个协程操作同一个数据会导致结果不理想

请阅读下列代码

// 只定义无需赋值
var wg sync.WaitGroup
var totalNum int

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

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

func main() { //主线程
    wg.Add(2)
    go add()
    go sub()
    wg.Wait()
    time.Sleep(time.Second * 2)
    fmt.Println(totalNum)
}

在理想的情况下,不管两个协程的函数如何交替,最后输出的结果都应该为0,但是这里输出的是

在这里插入图片描述

每次执行结束结果都不一定,为什么呢

请看下图是两个函数交替执行一个轮回的图

在这里插入图片描述

解读:

假如第一步执行的是add函数首先获取原始totalNum的值,为0,然后第二部是sub函数获取原始totalNum的值,为0,第三、四步add函数对totalNum进行加一操作并赋值给totalNum,但是第五第六步却是将totalNum的原始数据0减1后赋值给totalNum,导致一轮下来不会为0

所以说直接使用多协程操作同一个数据会导致资源竞争问题

每次执行结束结果都不一定,为什么呢

请看下图是两个函数交替执行一个轮回的图

[外链图片转存中…(img-NkB0TRcZ-1718868327943)]

解读:

假如第一步执行的是add函数首先获取原始totalNum的值,为0,然后第二部是sub函数获取原始totalNum的值,为0,第三、四步add函数对totalNum进行加一操作并赋值给totalNum,但是第五第六步却是将totalNum的原始数据0减1后赋值给totalNum,导致一轮下来不会为0

所以说直接使用多协程操作同一个数据会导致资源竞争问题

互斥锁

由多个协程操作同一个数据案例所展现的问题来看,需要一个机制来确保一个协程在执行的时候其他协程不执行,这就得用上互斥锁了

互斥锁概念:其中Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁,适用于读写不确定场景,即读写次数没有明显的区别

创建互斥锁:

var lock sync.Mutex

加锁:

lock.Lock()

解锁:

lock.Unlock()

加锁后的完整代码

// 只定义无需赋值
var wg sync.WaitGroup
var totalNum int

// 加入互斥锁
var lock sync.Mutex

func add() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
       // 加锁
       lock.Lock()
       totalNum += 1
       // 解锁
       lock.Unlock()
    }
}

func sub() {
    defer wg.Done()
    for i := 0; i < 100000; i++ {
       // 加锁
       lock.Lock()
       totalNum -= 1
       // 解锁
       lock.Unlock()
    }
}

func main() { //主线程
    wg.Add(2)
    go add()
    go sub()
    wg.Wait()
    fmt.Println(totalNum)
}

执行后达成理想状态,结果为0

读写锁

RWMutex是一个读写锁,其经常用于读次数远远多于写次数的场景,和互斥锁的区别:读写锁只锁住写的操作

在读的时候,数据之间不产生影响,写和读之间才会产生影响

创建读写锁:

var lock sync.RWMutex

加锁:

lock.RLock()

闭锁:

lock.RUnlock()

上代码案例

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.RLock()
    fmt.Println("开始修改数据")
    time.Sleep(time.Second * 10) //模拟写的时间
    fmt.Println("修改数据成功")
    lock.RUnlock()
}

func main() { //主线程
    wg.Add(6)
    // 启动协程  --->  场合:读多写少
    for i := 0; i < 5; i++ {
       go read()
    }
    go write()
    wg.Wait()
}

执行后结果
在这里插入图片描述

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值