GO语言面试系列:(八)golang 并发安全性案例分析

golang 在1.5版本之前默认只使用一个核心来跑所有的goroutines,即GOMAXPROCS默认设置为1, ,即是串行执行goroutines,在1.5版本后,GOMAXPROCS默认设置为当前计算机真实的核心线程数,即是在并行执行goroutines

并行执行安全性案例分析

利用计算机多核处理的特性,并行执行能成倍的提高程序的性能,但同时也带入了数据安全性问题,下面看一个在线银行转账的案例:

type User struct {
        Cash int
}

func (u *User) sendCash(to *User, amount int) bool {
    if u.Cash < amount {
        return false
    }
    /* 设置延迟Sleep,当多个goroutines并行执行时,便于进行数据安全分析 */
    time.Sleep(500 * time.Millisecond)
    u.Cash = u.Cash - amount
    to.Cash = to.Cash + amount
    return true
}

func main() {
    me := User{Cash: 500}
    you := User{Cash: 500}
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        me.sendCash(&you, 50) //转账
        fmt.Fprintf(w, "I have $%d\n", me.Cash)
        fmt.Fprintf(w, "You have $%d\n", you.Cash)
        fmt.Fprintf(w, "Total transferred: $%d\n", (you.Cash - 500))
    })
    http.ListenAndServe(":8080", nil)
}

这是一个通用的Go Web应用,定义User数据结构,sendCash是在两个User之间转账的服务,这里使用的是net/http 包,我们创建了一个简单的Http服务器,然后将请求路由到转账50元的sendCash方法,在正常操作下,代码会如我们预料一样运行,每次转移50美金,一旦一个用户的账户余额达到0美金,就不能再进行转出钞票了,因为没有钱了,但是,如果我们很快地发送很多请求,这个程序会继续转出很多钱,导致账户余额为负数。
这是课本上经常谈到的竞争情况race condition,在这个代码中,账户余额的检查是与从账户中取钱操作分离的,我们假想一下,如果一个请求刚刚完成账户余额检查,但是还没有取钱,也就是没有减少账户余额数值;而另外一个请求线程同时也检查账户余额,发现账户余额还没有剩为零(结果两个请求都一起取钱,导致账户余额为负数),这是典型的”check-then-act”竞争情况。这是很普遍存在的 并发 bug。

用锁解决竟态数据安全问题

那么我们如何解决呢?我们肯定不能移除检查操作,而是确保检查和取钱两个动作之间没有任何其他操作发生,其他语言是使用锁,当账户进行更新时,锁住禁止同时有其他线程操作,确保一次只有一个进程操作,也就是排斥锁Mutex。,下面用golang自带的sync包实现对转账判断及数据操作过程的加锁:

type User struct {
        Cash int
}

var transferLock *sync.Mutex

func (u *User) sendCash(to *User, amount int) bool {

    transferLock.Lock() //对转账操作进行加锁
    defer transferLock.Unlock() //转账结束后解锁释放资源
    if u.Cash < amount {
        return false
    }
    /* 设置延迟Sleep,当多个goroutines并行执行时,便于进行数据安全分析 */
    time.Sleep(500 * time.Millisecond)
    u.Cash = u.Cash - amount
    to.Cash = to.Cash + amount
    return true
}
func main() {
    me := User{Cash: 500}
    you := User{Cash: 500}
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        me.sendCash(&you, 50) //转账
        fmt.Fprintf(w, "I have $%d\n", me.Cash)
        fmt.Fprintf(w, "You have $%d\n", you.Cash)
        fmt.Fprintf(w, "Total transferred: $%d\n", (you.Cash - 500))
    })
    http.ListenAndServe(":8080", nil)
}

利用Channel,更好的实现并发

但是锁的问题很显然降低了程序并发的性能,锁是并发设计的最大敌人,在Go中推荐使用通道Channel,能够使用事件循环event loop机制更灵活地实现并发;通过委托一个后台协程监听通道,当通道中有数据时,立即进行转账操作,因为协程是顺序地读取通道中的数据,也就是巧妙地回避了竞争情况,没有必要使用任何状态变量防止并发竞争了。 具体示例:

type User struct {
    Cash int
}
type Transfer struct {
    Sender    *User
    Recipient *User
    Amount    int
}

func sendCashHandler(transferchan chan Transfer) {
    var val Transfer
    for {
        val = <-transferchan
        val.Sender.sendCash(val.Recipient, val.Amount)
    }
}

func (u *User) sendCash(to *User, amount int) bool {
    if u.Cash < amount {
        return false
    }
    /* 设置延迟Sleep,当多个goroutines并行执行时,便于进行数据安全分析 */
    time.Sleep(500 * time.Millisecond)
    u.Cash = u.Cash - amount
    to.Cash = to.Cash + amount
    return true
}

func main() {
    me := User{Cash: 500}
    you := User{Cash: 500}
    transferchan := make(chan Transfer)
    go sendCashHandler(transferchan)
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        transfer := Transfer{Sender: &me, Recipient: &you, Amount: 50}
        transferchan <- transfer
        fmt.Fprintf(w, "I have $%d\n", me.Cash)
        fmt.Fprintf(w, "You have $%d\n", you.Cash)
        fmt.Fprintf(w, "Total transferred: $%d\n", (you.Cash - 500))
    })
    http.ListenAndServe(":8080", nil)

上面这段代码创建了比较可靠的系统从而避免了并发竞争,但是我们会带来另外一个安全问题:DoS(Denial of Service服务拒绝),如果我们的转账操作慢下来,那么不断进来的请求需要等待进行转账操作的那个协程从通道中读取新数据,但是这个线程忙于照顾转账操作,没有闲功夫读取通道中新数据,这个情况会导致系统容易遭受DoS攻击,外界只要发送大量请求就能让系统停止响应。

祭出select 进一步提升性能

一些基础机制比如buffered channel可以处理这种情况,但是buffered channel是有内存上限的,不足够保存所有请求数据,优化解决方案是使用Go杰出的select语句:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    transfer := Transfer{Sender: &me, Recipient: &you, Amount: 50}
    /*转账*/
    result := make(chan int)
    go func(transferchan chan<- Transfer, transfer Transfer, result chan<- int) {
        transferchan <- transfer
        result <- 1
    }(transferchan, transfer, result)

    /*用select来处理超时机制*/
    select {
    case <-result:
        fmt.Fprintf(w, "I have $%d\n", me.Cash)
        fmt.Fprintf(w, "You have $%d\n", you.Cash)
        fmt.Fprintf(w, "Total transferred: $%d\n", (you.Cash - 500))
    case <-time.After(time.Second * 10): //超时处理
        fmt.Fprintf(w, "Your request has been received, but is processing slowly")
    }
})

这里提升了事件循环,等待不能超过10秒,等待超过timeout时间,会返回一个消息给User告诉它们请求已经接受,可能会花点时间处理,请耐心等候即可,使用这种方法我们降低了DoS攻击可能,一个真正健壮的能够并发处理转账且没有使用任何锁的系统诞生了。

小编微信:grey0805
DApp开源社区,共享创意

文章出处:朴实的一线攻城狮

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容