• golang中关于deadlock的思考与学习


    1、Golang中死锁的触发条件

    1.1 书上关于死锁的四个必要条件的讲解

    发生死锁时,线程永远不能完成,系统资源被阻碍使用,以致于阻止了其他作业开始执行。在讨论处理死锁问题的各种方法之前,我们首先深入讨论一下死锁特点。

    必要条件:

    如果在一个系统中以下四个条件同时成立,那么就能引起死锁:

    1. 互斥:至少有一个资源必须处于非共享模式,即一次只有一个线程可使用。如果另一线程申请该资源,那么申请线程应等到该资源释放为止。
    2. 占有并等待:—个线程应占有至少一个资源,并等待另一个资源,而该资源为其他线程所占有。
    3. 非抢占:资源不能被抢占,即资源只能被线程在完成任务后自愿释放。
    4. 循环等待:有一组等待线程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。

    我们强调所有四个条件必须同时成立才会出现死锁。循环等待条件意味着占有并等待条件,这样四个条件并不完全独立。

    图示例:

    线程1、线程2都尝试获取对方未释放的资源,从而会一直阻塞,导致死锁发生。

    1.2 Golang 死锁的触发条件

    看完了书上关于死锁的介绍,感觉挺清晰的,但是实际上到了使用或者看代码时,自己去判断是否会发生死锁却是模模糊糊的,难以准确判断出来。所以特意去网上找了些资料学习,特此记录。

    golang中死锁的触发条件:

    死锁是当 Goroutine被阻塞而无法解除阻塞时产生的一种状态。注意:for 死循环不能算在这里,虽然空for循环是实现了阻塞的效果,但是实际上goroutine是处于运行状态的。
    

    1.3 golang 中阻塞的场景

    1.3.1 sync.Mutex、sync.RWMutex

    golang中的锁是不可重入锁,对已经上了锁的写锁,再次申请锁是会报死锁。上了读锁的锁,再次申请写锁会报死锁,而申请读锁不会报错。

    写写冲突,读写冲突,读读不冲突。

    func main() {
    	var lock sync.Mutex
    	lock.Lock()
    	lock.Lock()
    }   
    //报死锁错误
    
    func main() {
    	var lock sync.RWMutex
    	lock.RLock()
    	lock.Lock()
    }
    //报死锁错误
    
    func main() {
    	var lock sync.RWMutex
    	lock.RLock()
    	lock.RLock()
    }
    //正常执行
    

    注意:这样就不会报 deadlock 错误,因为 for 死循环是处于运行状态,而不是阻塞状态

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    func main() {
    	var lock sync.Mutex
    	go func() {
    		for  {
    			time.Sleep(time.Second)
    			fmt.Println(2)
    		}
    	}()
    	fmt.Println(1)
    	lock.Lock()
    	fmt.Println(3)
    	lock.Lock()
    }
    
    1.3.2 sync.WaitGroup

    一个不会减少的 WaitGroup 会永久阻塞。

    func main() {
    	var wg sync.WaitGroup
    	wg.Add(1)
    	wg.Wait()
      //报死锁错误
    }
    

    正常运行,注意,启动的goroutine 必须是for 死循环,否则当goroutine 执行结束后,依旧会报 deadlock错误。其他的几个例子是一样的,就不一一举例了

    package main
    
    import (
    	"sync"
    	"time"
    )
    
    func main() {
    	var wg sync.WaitGroup
    	go func() {
    		for  {
    			time.Sleep(time.Second)
    		}
    	}()
    	wg.Add(1)
    	wg.Wait()
    	
    }
    
    1.3.3 空 select

    空 select 会一直阻塞。

    package main
    
    func main() {
    	select {
    	
    	}
    }
    //报死锁错误
    
    1.3.4 channel

    为 nil 的channel 发送、接受数据都会阻塞。

    func main() {
    	var ch chan struct{}
    	ch <- struct{}{}
    }
    //报死锁错误
    

    无缓冲的channel 发送、接受数据都会阻塞。

    func main() {
    	ch := make(chan struct{})
    	<- ch
    }
    //报死锁错误
    

    channel 缓冲区满了的,继续发送数据会阻塞。

    2、死锁案例讲解

    2.1 案例一:空 select{}

    package main
    
    func main() {
    	select {
    	
    	}
    }
    

    以上面为例子,select 语句会 造成 当前 goroutine 阻塞,但是却无法解除阻塞,所以会导致死锁。

    2.2 案例二:从无缓冲的channel接受、发送数据

    func main() {
    	ch := make(chan struct{})
    	//ch <- struct{}{} //发送
    	<- ch //接受
    	fmt.Println("main over!")
    }
    

    发生原因:

    上面创建了一个 名为:ch 的channel,没有缓冲空间。当向无缓存空间的channel 发送或者接受数据时,都会阻塞,但是却无法解除阻塞,所以会导致死锁。

    解决方案:边接受边读取

    package main
     
    // 方式1
    func recv(c chan int) {
    	ret := <-c
    	fmt.Println("接收成功", ret)
    }
    func main() {
    	ch := make(chan int)
    	go recv(ch) // 启用goroutine从通道接收值
    	ch <- 10
    	fmt.Println("发送成功")
    }
     
    // 方式2
    func main() {
       ch := make(chan int,1)
       ch<-1
       println(<-ch)
    }
    

    2.3 案例三:从空的channel中读取数据

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func request(index int,ch chan<- string)  {
    	time.Sleep(time.Duration(index)*time.Second)
    	s := fmt.Sprintf("编号%d完成",index)
    	ch <- s
    }
    
    func main() {
    	ch := make(chan string, 10)
    	fmt.Println(ch,len(ch))
    
    	for i := 0; i < 4; i++ {
    		go request(i, ch)
    	}
    
    	for ret := range ch{ //当 ch 中没有数据的时候,for range ch 会发生阻塞,但是无法解除阻塞,发生死锁
    		fmt.Println(len(ch))
    		fmt.Println(ret)
    	}
    }
    

    发生原因:

    当 ch 中没有数据的时候,就是从空的channel中接受数据,for range ch 会发生阻塞,但是无法解除阻塞,发生死锁。

    解决办法:当数据发送完了过后,close channel

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    var wg sync.WaitGroup
    
    func request(index int,ch chan<- string)  {
    	time.Sleep(time.Duration(index)*time.Second)
    	s := fmt.Sprintf("编号%d完成",index)
    	ch <- s
    
    	wg.Done()
    }
    
    func main() {
    	ch := make(chan string, 10)
    	for i := 0; i < 4; i++ {
    		wg.Add(1)
    		go request(i, ch)
    	}
    
    	go func() {
    		wg.Wait()
    		close(ch)
    	}()
    
    	LOOP:
    		for {
    			select {
    			case i,ok := <-ch: // select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句
            if !ok {
              break LOOP
            }
    				println(i)
    			default:
    				time.Sleep(time.Second)
    				fmt.Println("无数据")
    			}
    		}
    }
    

    2.4 案例四:给满了的channel发送数据

    func main() {
    	ch := make(chan struct{}, 3)
    
    	for i := 0; i < 4; i++ {
    		ch <- struct{}{}
    	}
    }
    

    发生原因:

    ch 是一个带缓冲的channel,但是只能缓冲三个struct,当channel满了过后,继续往channel发送数据会阻塞,但是无法解除阻塞,发生死锁。

    解决办法:读取channel中的数据

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    var wg sync.WaitGroup
    
    func main() {
    	ch := make(chan struct{}, 3)
    	
    	go func() {
    
    		for {
    			select {
    			case i, ok := <- ch:
    				wg.Done()
    				fmt.Println(i)
    				if !ok {
    					return
    				}
    			}
    		}
    	}()
    
    	for i := 0; i < 4; i++ {
    		wg.Add(1)
    		ch <- struct{}{}
    	}
    
    	wg.Wait()
    }
    

    3、总结

    最重要的是记住golang中死锁的触发条件:当 goroutine 发生阻塞,但是无法解除阻塞状态时,就会发生死锁。然后在使用或者阅读代码时,再根据具体情况进行分析。

    channel异常情况总结:

    注意:对已经关闭的channel再次关闭,也会发生panic。

    以上就是我对死锁的思考,有不对的地方恳请指出,谢谢。


    __EOF__

  • 本文作者: 画个一样的我
  • 本文链接: https://www.cnblogs.com/huageyiyangdewo/p/17231337.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    JVM(二十二)—— 垃圾回收器(二)CMS垃圾回收器
    热门高薪岗位大数据开发 自学转行如何就业(自学教程 学习路线 方法建议)
    1.3 vue ui框架-element-ui框架
    【软件工程师从0到1】- 继承 (知识汇总)
    提升用户体验的利器:揭秘Spring框架中国际化的奇妙魔力
    低代码和人工智能助力疫情期间抗原自测信息自动化收集和处理
    SpringBoot SpringBoot 开发实用篇 5 整合第三方技术 5.11 jetcache 方法缓存
    开源高性能 RISC-V 处理器“香山”国际亮相;Apache Log4j 远程代码执行漏洞;DeepMind 拥有 2800 亿参数的模型 | 开源日报
    HTML5期末考核大作业:基于Html+Css+javascript的网页制作(化妆品公司网站制作)
    初始化一个ArrayList的多种方式
  • 原文地址:https://www.cnblogs.com/huageyiyangdewo/p/17231337.html