百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

golang deadlock详解

toyiye 2024-06-21 11:59 12 浏览 0 评论

fatal error: all goroutines are asleep - deadlock

近两天遇到此类错误, 发现goroutine以及channel的基础仍需巩固。由该错误牵引出go相关并发操作的问题, 下面做一些简单的tips操作和记录。

package main
import (
    "fmt"
)
func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

1、在程序启动时, Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了, 所有在main()函数中启动的goroutine会一同结束!

所以引出sync.WaitGroup的使用。通过它, 可以实现goroutine的同步。

package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("Hello Goroutine!", i)
}
func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1) // 启动一个goroutine就登记+1
        go hello(i)
    }
    wg.Wait() // 等待所有登记的goroutine都结束
}

2、单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。如果说goroutine是Go程序并发的执行体, channel就是它们之间的连接。

channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。Go 语言中的通道(channel)是一种特殊的类型。

通道像一个传送带或者队列, 总是遵循先入先出(First In First Out)的规则, 保证收发数据的顺序。每一个通道都是一个具体类型的导管, 也就是声明channel的时候需要为其指定元素类型。

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。我们通过调用内置的close函数来关闭通道。

关闭后的通道有以下特点:

(1) 对一个关闭的通道再发送值就会导致panic。

(2) 对一个关闭的通道进行接收会一直获取值直到通道为空。

(3) 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。

(4) 关闭一个已经关闭的通道会导致panic。

无缓冲的通道又称为阻塞的通道:

package main
import (
    "fmt"
)
func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}

上面这段代码能够通过编译, 但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!


goroutine 1 [chan send]:

main.main()

我们使用ch := make(chan int)创建的是无缓冲的通道, 无缓冲的通道只有在有人接收值的时候才能发送值。

上面的代码会阻塞在ch <- 10这一行代码形成死锁, 那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值, 并一种方式是使用带缓冲的通道, 例如:

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

但是注意:channel 通道增加缓存区后, 可将数据暂存到缓冲区, 而不需要接收端同时接收(缓冲区如果超出大小同样会造成死锁)

package main
import (
    "fmt"
)
func main() {
    ch := make(chan int, 1) // 定义缓冲区的大小为 1
    ch <- 1 // 发送通道
    ch <- 1 // 发送通道
    // 接收通道
    fmt.Println(<-ch) // 1
    /*

    fatal error: all goroutines are asleep - deadlock!
    goroutine 1 [chan send]:

    */
}

解决方法: 重新定义缓冲区大小, ch := make(chan int, 2)


channel异常情况总结

channel nil 非空 空的 满了 没满

接收 阻塞 接收值 阻塞 接收值 接收值

发送 阻塞 发送值 发送值 阻塞 发送值

关闭 panic 关闭成功 关闭成功 关闭成功 关闭成功,

读完数据后返回零值 返回零值 读完数据后返回零值 读完数据后返回零值

总结, 可以看出, 产生阻塞的方式, 主要容易踩坑的有两种:空的通道一直接收会阻塞; 满的通道一直发送也会阻塞!

3、那么, 如何解决阻塞死锁问题呢?

(1) 如果是上面的无缓冲通道, 使用再起一个协程的方式, 可使得接收端和发送端并行执行。

(2) 可以初始化时就给channel增加缓冲区, 也就是使用有缓冲的通道

(3) 易踩坑点, 针对有缓冲的通道, 产生阻塞, 如何解决?

如下面例子, 开启多个goroutine并发执行任务, 并将数据存入管道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 {
    fmt.Println(len(ch))
    fmt.Println(ret)
    }
}

错误如下:

0xc000056060 0

0

编号0完成

0

编号1完成

0

编号2完成

0

编号3完成

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:

main.main()

F:/project/test/suoding/deadlock/demo7.go:22 +0x1ad

exit status 2


不可靠的解决方式如下:

for {
    i, ok := <-ch // 通道关闭后再取值ok=false;通道为空去接收,会发生阻塞死锁
    if !ok {
        break
    }
    println(i)
}
for ret := range ch {
    fmt.Println(len(ch))
    fmt.Println(ret)
}

以上两种从通道获取方式, 都有小坑! 一旦获取的通道没有主动close(ch)关闭, 而且通道为空时, 无论通过for还是foreach方式去取值获取, 都会产生阻塞死锁deadlock chan receive错误!

可靠的解决方式1 如下:

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
    defer wg.Done()
}
func main() {
    ch := make(chan string, 10)
    go func() {
        wg.Wait()
        close(ch)
    }()
    for i := 0; i < 4; i++ {
        wg.Add(1)
        go request(i, ch)
    }
    for ret := range ch {
        fmt.Println(len(ch))
        fmt.Println(ret)
    }
}

解决方式: 即我们在生成完4个goroutine后对data channel进行关闭, 这样通过for range从通道循环取出全部值, 通道关闭就会退出for range循环。

具体实现:可以利用sync.WaitGroup解决, 在所有的 data channel 的输入处理之前, wg.Wait()这个goroutine会处于等待状态(wg.Wait()源码就是for循环)。

当执行方法处理完后(wg.Done), wg.Wait()就会放开执行, 执行后面的close(ch)。

可靠的解决方式2 如下:

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)
    for i := 0; i < 4; i++ {
    go request(i, ch)
}
for {
    select {
    case i := <-ch: // select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句
    println(i)
    default:
    time.Sleep(time.Second)
    fmt.Println("无数据")
    }
    }
}

上面这种方式获取, 通过select case + default的方式也可以完美避免阻塞死锁报错! 但是适用于通道不关闭, 需要时刻循环执行数据并且处理的情境下。

一定留意, default的作用很大! 是避免阻塞的核心。

使用select语句能提高代码的可读性。

可处理一个或多个channel的发送/接收操作。

如果多个case同时满足, select会随机选择一个。

对于没有case的select{}会一直等待, 可用于阻塞main函数。

5、实际项目中goroutine+channel+select的使用

如下, 使用于项目监听终端中断信号操作:



package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
    go SignalProc()
    done := make(chan bool, 1)
    for {
    select {
        case <-done:
        break
        }
    }
    fmt.Println("exit")
}
func SignalProc() {
    sigs := make(chan os.Signal)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGHUP, os.Interrupt)
    for {
    msg := <-sigs
    fmt.Println("Recevied signal:", msg)
    switch msg {
    default:
    fmt.Println("get sig=%v\n", msg)
    case syscall.SIGHUP:
    fmt.Println("get sighup\n")
    case syscall.SIGUSR1:
    fmt.Println("SIGUSR1 test")
    case syscall.SIGUSR2:
    fmt.Println("SIGUSR2 test")
    }
    }
}


// kill -USR1 10323
kill -USR2 10323
kill -n 2 10323
可以 SIGUSR1 做一些配置的重新加载
SIGUSR2 可以做一些游戏base的重新加载

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码