Skip to content
This repository has been archived by the owner on Apr 17, 2024. It is now read-only.

关于 C++ 接口设计 #107

Closed
Timothy-Liuxf opened this issue Jan 13, 2022 · 1 comment · Fixed by #106
Closed

关于 C++ 接口设计 #107

Timothy-Liuxf opened this issue Jan 13, 2022 · 1 comment · Fixed by #106
Assignees
Labels
design Idea for design good first issue Good for newcomers

Comments

@Timothy-Liuxf
Copy link
Member

Timothy-Liuxf commented Jan 13, 2022

根据最新的一次 PR #106 ,目前存在一些问题。问题参见该 PR 的评论 #106 (comment)#106 (comment)#106 (comment)

先假设仍然使用 mtx_statemtx_buffer(后面会看到 mtx_state 时没有必要使用的)。其中,mtx_state 用于锁 pStatemtx_buffer 用于锁 pBuffer

设计思路分两种讨论:

在一次 play 调用期间不改变

如果要在一次 play 调用期间选手获取到的信息不改变,则采用如下设计:

LoadBuffer 负责当信息到来时更新 pBuffer,使用 bool bufferDirty 标记其更新过 buffer,并唤醒条件变量 bufferCond

LoadBuffer()
{
    lock(mtx_buffer)
    {
        // 更新 pBuffer 内容,不需要调用 `Update`
        bufferDirty = true;    // 标记缓冲区已脏
        notify_all(bufferCond);
    }
}

Update 用于交换 pBufferpState

Update()
{
    lock (mtx_buffer)        // 锁住 pBuffer
    {
        // 缓冲区脏了才需要更新,否则等待下一帧
        while (!bufferDirty)
        {
            wait(bufferCond);
        }
        lock (mtx_state)  // 锁住 pState
        {
            swap(pBuffer, pState);
        }
        bufferDirty = false;    // 已更新完毕,buffer 不再是脏的。
                                // 注意对 bufferDirty 的修改一定要在 mtx_buffer 锁住的情况下
    }
}

要使一次循环中信息不改变,则交替执行 Update 和 AI 代码即可:

loop
{
    Update();
    AI();
}

注意到,如果在一次调用期间获取的信息不改变,那么,pState 只会有一个线程在使用(即 AI 代码的线程),因此在一般情况下可以不使用 mtx_state 锁(如果选手自己开多线程,并且在 play 返回之后子线程没有退出,则可能会出现并发问题,不过我们不妨将此视为选手的非法操作 x)。

在一次 play 调用期间改变

这种情况真的想了很久,非常诡异,怎么写都觉得有些不舒服,姑且贴几种可选的做法吧:

这种情况就必须要使用两个锁了,因为将看到,对 pState 的读和写使用不同的线程;此外这种情况必须为选手提供 Wait 接口用于等待下一帧的到来;取消 Update 函数

方法一

使用条件变量 frameCond 实现 Wait

  • 优点:写法简洁
  • 缺点:没有考虑条件变量存在的虚假唤醒现象,造成 Wait 并没有等到下一帧到来便被唤醒。目前我不知道虚假唤醒的影响有多大。
LoadBuffer()
{
    lock (mtx_buffer)
    {
        // 更新 pBuffer 内容

        // Update
        // 没有使用 Update 的初衷是
        // 如果像上一个问题一样加锁的话,不可避免地对 buffer 多一个解锁再加锁的过程造成额外开销
        // 如果 Update 不加锁,由外部加锁,则 Update 成为一个线程不安全的函数,有在后期被误调用的风险
        lock (mtx_state)
        {
            swap(pBuffer, pState);
        }
        notify_all(frameCond);
    }
}

Wait()
{
    lock (mtx_buffer)
    {
        wait(frameCond);
    }
}

loop
{
    Wait();
    AI::play();
}

方法二

使用一个标志变量 std::atomic_bool freshed 防范虚假唤醒。

  • 优点:在上一个方法基础上考虑了虚假唤醒
  • 缺点:不适用于多线程调用 Wait 的情况。如果选手开了多个线程 Wait,则可能导致一些线程将 freshed 置为 false 而导致其他的线程唤醒失败
LoadBuffer()
{
    lock (mtx_buffer)
    {
        // 更新 pBuffer 内容

        // Update
        // 没有使用 Update 的初衷是
        // 如果像上一个问题一样加锁的话,不可避免地对 buffer 多一个解锁再加锁的过程造成额外开销
        // 如果 Update 不加锁,由外部加锁,则 Update 成为一个线程不安全的函数,有在后期被误调用的风险
        lock (mtx_state)
        {
            swap(pBuffer, pState);
        }
        freshed = true;
        notify_all(frameCond);
    }
}

Wait()
{
    freshed = false;   // 刚进入函数时将其标识为 false。由于其不在 lock 内,为防止数据竞争因此需要定义为原子变量(如果在 lock
内,第一次进入时会发生两次访问之间没有内存屏障而导致指令乱序,也需要定义为原子变量,所以说这么设计非常怪)
    lock (mtx_buffer)
    {
        while (!freshed)
            wait(frameCond);
    }
}

loop
{
    Wait();
    AI::play();
}

方法三

每一帧赋予一个 freshed 变量,即可解决上述问题

  • 优点:以上一切问题均得到解决
  • 缺点:频繁申请动态内存,开销较大;涉及到的原子操作 std::atomic<std::shared_ptr> 可能不是 lock free 的,也有较大开销
// freshed 定义:std::atomic<std::shared_ptr<bool>> freshed

LoadBuffer()
{
    lock (mtx_buffer)
    {
        // 更新 pBuffer 内容

        // Update
        // 没有使用 Update 的初衷是
        // 如果像上一个问题一样加锁的话,不可避免地对 buffer 多一个解锁再加锁的过程造成额外开销
        // 如果 Update 不加锁,由外部加锁,则 Update 成为一个线程不安全的函数,有在后期被误调用的风险
        lock (mtx_state)
        {
            swap(pBuffer, pState);
        }
        *freshed = true;
        freshed.store(std::make_shared(false));
        notify_all(frameCond);
    }
}

Wait()
{
    // 下面的操作为获取当前帧的标志。由于该操作在 lock 外,而 shared_ptr 本身并不保证原子,只有引用计数保证原子
    // 因此 freshed 需要 std::atomic<std::shared_ptr<bool>>,使用 load() 可以使 frameFreshed 不为原子以节省开销;
    // 也可以将该操作放在 lock 内,freshed 不再需要是原子的,但是 LoadBuffer 越耗时,则 Wait 会多等一帧的概率越大

    auto frameFreshed = freshed.load();

    lock (mtx_buffer)
    {
        while (!*frameFreshed)
            wait(frameCond);
    }
}

loop
{
    Wait();
    AI::play();
}
@Timothy-Liuxf Timothy-Liuxf added good first issue Good for newcomers design Idea for design labels Jan 13, 2022
@Timothy-Liuxf Timothy-Liuxf linked a pull request Jan 13, 2022 that will close this issue
4 tasks
@Timothy-Liuxf Timothy-Liuxf reopened this Jan 17, 2022
@TCL606
Copy link
Member

TCL606 commented May 19, 2022

我觉得这个问题已经解决了,所以先关闭这个开放了几个月的issue了。。

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
design Idea for design good first issue Good for newcomers
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants