返回

多房间聊天室,为什么非加锁不可?

后端

在这场关于多房间聊天室的讨论中,有人质疑为何必须给代码逻辑加锁。本文将通过编写测试用例,论证加锁的必要性和充分性。

上篇文章中,我们构建了一个多房间的聊天室,它允许用户加入和离开房间,并在这些房间内发送消息。我们还讨论了需要给共享数据结构加锁,以确保并发访问的正确性。

但有人可能会问:为什么一定要加锁?不加锁行不行啊?

为了回答这个问题,让我们编写一些测试用例。

第一个测试用例不使用任何锁。

func TestUnlockChatRoom(t *testing.T) {
    room := &ChatRoom{
        clients: make(map[string]*Client),
    }

    // 模拟两个客户端同时加入同一个房间
    go func() {
        room.Join("client1")
    }()

    go func() {
        room.Join("client2")
    }()

    // 模拟其中一个客户端离开房间
    room.Leave("client1")

    // 检查房间中是否还有另一个客户端
    if len(room.clients) != 1 {
        t.Errorf("Expected 1 client in the room, got %d", len(room.clients))
    }
}

运行这个测试用例,我们可能会得到以下错误:

--- FAIL: TestUnlockChatRoom (0.00s)
    multiroom_test.go:22: Expected 1 client in the room, got 0

这个错误告诉我们,当我们不给代码逻辑加锁时,两个客户端可能会同时修改房间的客户端列表,导致数据不一致。

为了解决这个问题,我们可以给代码逻辑加锁。

func TestLockChatRoom(t *testing.T) {
    room := &ChatRoom{
        clients: make(map[string]*Client),
        lock:    &sync.Mutex{},
    }

    // 模拟两个客户端同时加入同一个房间
    go func() {
        room.Lock()
        defer room.Unlock()
        room.Join("client1")
    }()

    go func() {
        room.Lock()
        defer room.Unlock()
        room.Join("client2")
    }()

    // 模拟其中一个客户端离开房间
    room.Lock()
    defer room.Unlock()
    room.Leave("client1")

    // 检查房间中是否还有另一个客户端
    room.Lock()
    defer room.Unlock()
    if len(room.clients) != 1 {
        t.Errorf("Expected 1 client in the room, got %d", len(room.clients))
    }
}

运行这个测试用例,我们将不会得到任何错误。这意味着当我们给代码逻辑加锁时,两个客户端就不会同时修改房间的客户端列表,从而保证了数据的一致性。

通过这两个测试用例,我们可以看到,给多房间聊天室的代码逻辑加锁是必要的。不加锁可能会导致数据不一致,而加锁可以保证数据的一致性。