go语言chan 和 routine活用

时间:2022-05-05
本文章向大家介绍go语言chan 和 routine活用,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

这里,我们以游戏中的一个情况为例。比如魔兽世界里的40人团队副本BOSS战,很多玩家同时攻击BOSS,BOSS的血量会进行频繁修改,我们要记录对BOSS的致命一击。常规 对BOSS血量的操作经行加锁,然后修改,每次攻击甚至还会判断怪物是否处于死亡状态以判断客户端是否能施放某个技能,但这样会让一个锁控制40个玩家的攻击操作,会导致攻击,技能等释放的不流畅。所以魔兽世界应该不是采用这种加锁的方式。为什么我这样说,因为做为了一个wower,我们经常会发现,实际BOSS,怪物已经死了,但我的寒冰剑还在半路,结果呢,寒冰剑击中了已经成为尸体的怪物并且显示伤害值。

使用Go语言来实现这样的功能:

/**  
 * Created by Administrator on 13-12-20.  
 */  
package monster  
 
// monster 以小写命名,标识该类无法直接创建,这里由 monster.MonsterNew 创建  
type monster struct {  
    Id    int  
    Hp    int  
}  
/**  
 * Created by Administrator on 13-12-20.  
 */  
package monster  
 
import (  
    "sync"  
    "fmt"  
    "os"  
    "time"  
    "math/rand"  
)  
 
// 命令  
type MonsterCommand interface {  
    doCommand()  
}  
 
// 改变血量的命令  
type MonsterHpChangeCommand struct {  
    Id        int  
    Hp        int  
    Attack    int // 攻击方  
}  
 
func (this *MonsterHpChangeCommand) doCommand() {  
    m := monsters[this.Id]  
    if m != nil {  
        time.Sleep(time.Millisecond * time.Duration(rand.Intn(10))) // 处理时间10毫秒  
        m.Hp += this.Hp  
        if m.Hp < 0 && m.Hp-this.Hp > 0 {  
            fmt.Fprintf(os.Stdout, "玩家[%d]对怪物[%d]造成[%d]点伤害,并给于致命一击!n", this.Attack, this.Id, this.Hp)  
        }  
    } else {  
        // panic(strconv.Itoa(this.Id) + " is nil")  
    }  
}  
 
// 怪物map  
var monsters map[int]*monster = make(map[int]*monster, 100)  
 
// 怪物map lock  
var monstersLock sync.Mutex = sync.Mutex{}  
 
// 怪物血量命令通道  
var monstersChan chan *MonsterHpChangeCommand = make(chan *MonsterHpChangeCommand, 100)  
 
// 创建一个怪物,并且将他添加到“怪物map”并且为其注册“怪物命令通道map”  
func MonsterNew(id int) *monster {  
    monstersLock.Lock()  
    defer monstersLock.Unlock()  
 
    m := &monster{id, 99999}  
 
    monsters[id] = m  
 
    fmt.Fprintf(os.Stdout, "len(monsters): %dn", len(monsters))  
 
    return m  
}  
 
// 删除怪物命令  
func MonsterRemove(id int) {  
    monstersLock.Lock()  
    defer monstersLock.Unlock()  
 
    delete(monsters, id)  
}  
 
// 为指定怪物添加处理命令,以这样的方式可以保证对该怪物的处理绝对的线程安全  
func MonsterAddCommand(id int, command *MonsterHpChangeCommand) {  
    monstersChan <- command  
}  
 
 
func PrintMonstersInfo() {  
    for id, m := range monsters {  
        fmt.Fprintf(os.Stdout, "MonstersInfo monsters[%d] = %vn", id, m)  
    }  
}  
 
func init() {  
    go func() {  
        fmt.Fprintf(os.Stdout, "怪物血量command处理routine启动")  
        for c := range monstersChan {  
            c.doCommand()  
        }  
    }()  
}  

使用一:

测试逻辑代码,线程安全性

package main  
 
import (  
    "monster"  
    "fmt"  
    "os"  
    "time"  
    "math/rand"  
)  
 
func main() {  
 
    time.Sleep(time.Second * 2)  
    m1 := monster.MonsterNew(1)  
    time.Sleep(time.Second * 2)  
    m2 := monster.MonsterNew(2)  
    m3 := monster.MonsterNew(3)  
    m4 := monster.MonsterNew(4)  
    m5 := monster.MonsterNew(5)  
 
    fmt.Fprintf(os.Stdout, "m1 = %vn", m1)  
    fmt.Fprintf(os.Stdout, "m2 = %vn", m2)  
    fmt.Fprintf(os.Stdout, "m3 = %vn", m3)  
    fmt.Fprintf(os.Stdout, "m4 = %vn", m4)  
    fmt.Fprintf(os.Stdout, "m5 = %vn", m5)  
 
    rand.Seed(time.Now().Unix()) // 设置随机种子  
 
    for i := 1; i < 6; i++ {  
        id := i  
        go func() {  
            for j := 0; j < 100; j++ { // 100个线程修改同一个怪物血量  
                time.Sleep(time.Millisecond * time.Duration(rand.Intn(5)))  
                go func() {  
                    monster.MonsterAddCommand(id, &monster.MonsterHpChangeCommand{id, -100})  
                }()  
            }  
        }()  
    }  
 
    testRemove := true  
    if testRemove {  
        for i := 1; i < 6; i++ {  
            id := i  
            if rand.Intn(2) < 1 {  
                time.Sleep(time.Millisecond * time.Duration(rand.Intn(200)))  
                fmt.Fprintf(os.Stdout, "删除怪物:%dn", id)  
                monster.MonsterRemove(id)  
            }  
        }  
    }  
    time.Sleep(time.Second * 5)  
 
    monster.PrintMonstersInfo()  
 
    fmt.Fprintf(os.Stdout, "m1 = %vn", m1)  
    fmt.Fprintf(os.Stdout, "m2 = %vn", m2)  
    fmt.Fprintf(os.Stdout, "m3 = %vn", m3)  
    fmt.Fprintf(os.Stdout, "m4 = %vn", m4)  
    fmt.Fprintf(os.Stdout, "m5 = %vn", m5)  
 
    monster.MonsterRemove(1)  
    monster.MonsterRemove(2)  
    monster.MonsterRemove(3)  
    monster.MonsterRemove(4)  
    monster.MonsterRemove(5)  
}  

测试二:

40人集火BOSS

package main  
 
import (  
    "monster"  
    "fmt"  
    "os"  
    "time"  
    "math/rand"  
)  
 
type Player struct {  
    Id    int  
    Dc    int // 攻击力  
}  
 
func (this *Player) AttackMonster(id int) {  
    monster.MonsterAddCommand(id, &monster.MonsterHpChangeCommand{id, this.Dc, this.Id})  
}  
 
func main() {  
 
    m1 := monster.MonsterNew(1)  
    fmt.Fprintf(os.Stdout, "m1 = %vn", m1)  
 
    // 创建40人的团队副本  
    m := make(map[int]*Player)  
    for i := 0; i < 40; i++ {  
        id := i  
        m[id] = &Player{id, ^rand.Intn(300)}  
    }  
 
    // 40个玩家不停的给怪物造成伤害  
    for _, p := range m {  
        go func() {  
            for {  
                time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))  
                p.AttackMonster(m1.Id)  
            }  
        }()  
    }  
 
    for i := 0; i < 30;i ++ {  
        time.Sleep(time.Second)  
        fmt.Fprintf(os.Stdout, "m1 = %vn", m1)  
    }  
    monster.MonsterRemove(1)  
}  

如何理解这种设计方案?

换个例子来说。玩家的金钱。在一个多线程的服务器中,可能玩家发起的购买操作都是一个单独的线程处理。如果对金币的修改不经行线程安全处理,将导致严重的bug。

玩家有100金币,结果由于网络等特殊原因,玩家在购买一个物品时,结果点了2次,甚至更多次。导致服务器在接收处理时,几乎同一时间开辟了2个线程A,B来处理

线程A:

读取玩家当前金币:100 判断购买道具价格20是否处于可以购买状态,为玩家添加物品 修改玩家金币为80

线程B:

读取玩家当前金币:100 判断购买道具价格20是否处于可以购买状态,为玩家添加物品 修改玩家金币为80

因为 “为玩家添加物品”这样的操作,可能涉及数据库操作等原因导致执行时间较长。这将导致A 在修改玩家金币为80前,B线程已经在处理中了,结果B线程读取到玩家的当前金币为100,导致的结果就是,玩家获得了2件物品,结果却只扣了一件物品的金币。

OK,为了杜绝上面这种情况,我们需要对金币的操作经行加锁处理。

也就是在A线程未处理完的时候,B线程处于等待状态。

很明显,这种设计方案结果了上面的问题,但却带来了另外一个问题,那就是B请求处于等待状态!

也许上面的例子有点模糊。我再举个例子。

大学时,我们去食堂吃饭。卖饭的窗口只有1个。然后我们就排着长长的队伍,一步一步向前进。

然而另外一种设计方案呢?

我们并没有自己去排队,我们买了票,递给了收银员,然后我们就可以去选座,或者去买点其他东西,玩玩手机什么的。我们只需要的是,收银员处理我们递过去的票,然后将我们点的饭菜给我们送过来。