简体   繁体   English

同步非常快的线程

[英]Synchronizing very fast threads

In the following example (an idealized "game") there are two threads. 在下面的例子中(理想化的“游戏”)有两个线程。 The main thread which updates data and RenderThread which "renders" it to the screen. 更新数据的主线程和RenderThread其“呈现”到屏幕的RenderThread What I need it those two to be synchronized. 我需要的是那两个要同步的东西。 I cannot afford to run several update iteration without running a render for every single one of them. 我没有能力运行几次更新迭代而不为它们中的每一个运行渲染。

I use a condition_variable to sync those two, so ideally the faster thread will spend some time waiting for the slower. 我使用condition_variable来同步这两个,所以理想情况下,更快的线程将花费一些时间等待较慢的。 However condition variables don't seem to do the job if one of the threads completes an iteration for a very small amount of time. 但是,如果其中一个线程在很短的时间内完成迭代,则条件变量似乎不起作用。 It seems to quickly reacquire the lock of the mutex before wait in the other thread is able to acquire it. 它似乎很快就会重新获取互斥锁的锁定,然后wait在另一个线程中获取它。 Even though notify_one is called 即使调用了notify_one

#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>
#include <functional>
#include <mutex>
#include <condition_variable>

using namespace std;

bool isMultiThreaded = true;

struct RenderThread
{
    RenderThread()
    {
        end = false;
        drawing = false;
        readyToDraw = false;
    }

    void Run()
    {
        while (!end)
        {
            DoJob();
        }
    }

    void DoJob()
    {
        unique_lock<mutex> lk(renderReadyMutex);
        renderReady.wait(lk, [this](){ return readyToDraw; });
        drawing = true;

        // RENDER DATA
        this_thread::sleep_for(chrono::milliseconds(15)); // simulated render time
        cout << "frame " << count << ": " << frame << endl;
        ++count;

        drawing = false;
        readyToDraw = false;

        lk.unlock();
        renderReady.notify_one();
    }

    atomic<bool> end;

    mutex renderReadyMutex;
    condition_variable renderReady;
    //mutex frame_mutex;
    int frame = -10;
    int count = 0;

    bool readyToDraw;
    bool drawing;
};

struct UpdateThread
{
    UpdateThread(RenderThread& rt)
        : m_rt(rt)
    {}

    void Run()
    {
        this_thread::sleep_for(chrono::milliseconds(500));

        for (int i = 0; i < 20; ++i)
        {
            // DO GAME UPDATE

            // when this is uncommented everything is fine
            // this_thread::sleep_for(chrono::milliseconds(10)); // simulated update time

            // PREPARE RENDER THREAD
            unique_lock<mutex> lk(m_rt.renderReadyMutex);
            m_rt.renderReady.wait(lk, [this](){ return !m_rt.drawing; });

            m_rt.readyToDraw = true;

            // SUPPLY RENDER THREAD WITH DATA TO RENDER
            m_rt.frame = i;

            lk.unlock();
            m_rt.renderReady.notify_one();

            if (!isMultiThreaded)
                m_rt.DoJob();
        }        

        m_rt.end = true;
    }

    RenderThread& m_rt;
};

int main()
{
    auto start = chrono::high_resolution_clock::now();

    RenderThread rt;
    UpdateThread u(rt);

    thread* rendering = nullptr;
    if (isMultiThreaded)
        rendering = new thread(bind(&RenderThread::Run, &rt));

    u.Run();

    if (rendering)
        rendering->join();

    auto duration = chrono::high_resolution_clock::now() - start;
    cout << "Duration: " << double(chrono::duration_cast<chrono::microseconds>(duration).count())/1000 << endl;


    return 0;
}

Here is the source of this small example code , and as you can see even on ideone's run the output is frame 0: 19 (this means that the render thread has completed a single iteration, while the update thread has completed all 20 of its). 下面是这个小示例代码的来源 ,正如您在ideone的运行中所看到的那样,输出是frame 0: 19 0:19(这意味着渲染线程已经完成了一次迭代,而更新线程已经完成了所有20个) 。

If we uncomment line 75 (ie simulate some time for the update loop) everything runs fine. 如果我们取消注释第75行(即模拟更新循环的一些时间),一切运行正常。 Every update iteration has an associated render iteration. 每次更新迭代都有一个关联的渲染迭代。

Is there a way to really truly sync those threads, even if one of them completes an iteration in mere nanoseconds, but also without having a performance penalty if they both take some reasonable amount of milliseconds to complete? 有没有办法真正真正同步​​这些线程,即使其中一个线程仅在几纳秒内完成迭代,但如果它们都花费一些合理的毫秒数来完成,也没有性能损失?

If I understand correctly, you want the 2 threads to work alternately: updater wait until the renderer finish before to iterate again, and the renderer wait until the updater finish before to iterate again. 如果我理解正确,您希望2个线程交替工作:更新程序等到渲染器完成再重复迭代,渲染器等到更新程序完成之后再次迭代。 Part of the computation could be parallel, but the number of iteration shall be similar between both. 部分计算可以是并行的,但两者之间的迭代次数应相似。

You need 2 locks: 你需要2个锁:

  • one for the updating 一个用于更新
  • one for the rendering 一个用于渲染

Updater: 更新:

wait (renderingLk)
update
signal(updaterLk)

Renderer: 渲染:

wait (updaterLk)
render
signal(renderingLk)

EDITED: 编辑:

Even if it look simple, there are several problems to solve: 即使它看起来很简单,也有几个问题需要解决:

Allowing part of the calculations to be made in parallel: As in the above snippet, update and render will not be parallel but sequential, so there is no benefit to have multi-thread. 允许部分计算并行进行:如上面的代码片段所示,update和render不是并行的,而是顺序的,因此拥有多线程没有任何好处。 To a real solution, some the calculation should be made before the wait, and only the copy of the new values need to be between the wait and the signal. 对于一个真正的解决方案,一些计算应该在等待之前进行,并且只有新值的副本需要在等待和信号之间。 Same for rendering: all the render need to be made after the signal, and only getting the value between the wait and the signal. 渲染相同:所有渲染都需要在信号之后进行,并且只获取等待和信号之间的值。

The implementation need to care also about the initial state: so no rendering is performed before the first update. 实现还需要关注初始状态:因此在第一次更新之前不执行渲染。

The termination of both thread: so no one will stay locked or loop infinitely after the other terminate. 两个线程的终止:所以没有人会在另一个终止后保持锁定或无限循环。

I think a mutex (alone) is not the right tool for the job. 我认为互斥(单独)不适合这项工作。 You might want to consider using a semaphore (or something similar) instead. 您可能想要考虑使用信号量(或类似的东西)。 What you describe sound a lot like a producer/consumer problem , ie, one process is allowed to run once everytime another process has finnished a task. 你所描述的听起来很像生产者/消费者问题 ,即,每当另一个进程完成任务时,允许一个进程运行一次。 Therefore you might also have a look at producer/consumer patterns. 因此,您可能还要了解生产者/消费者模式。 For example this series might get you some ideas: 例如,这个系列可能会给你一些想法:

There a std::mutex is combined with a std::condition_variable to mimic the behavior of a semaphore. 有一个std::mutex与一个std::condition_variable结合起来模仿一个信号量的行为。 An approach that appears quite reasonable. 一种看似合理的方法。 You would probably not count up and down but rather toggle true and false a variable with needs redraw semantics. 您可能不会计数和减少,而是切换true和false一个需要重绘语义的变量。

For reference: 以供参考:

This is because you use a separate drawing variable that is only set when the rendering thread reacquires the mutex after a wait , which may be too late. 这是因为您使用单独的drawing变量,该变量仅在渲染线程在wait后重新获取互斥锁时设置,这可能为时已晚。 The problem disappears when the drawing variable is removed and the check for wait in the update thread is replaced with ! m_rt.readyToDraw 删除drawing变量并替换更新线程中的wait检查时,问题消失! m_rt.readyToDraw ! m_rt.readyToDraw (which is already set by the update thread and hence not susceptible to the logical race. ! m_rt.readyToDraw (已由更新线程设置,因此不易受逻辑竞争的影响。

Modified code and results 修改了代码和结果

That said, since the threads do not work in parallel, I don't really get the point of having two threads. 也就是说,由于线程不能并行工作,我实际上并不认为有两个线程。 Unless you should choose to implement double (or even triple) buffering later. 除非您应该选择稍后实施双重(甚至三重)缓冲。

A technique often used in computer graphics is to use a double-buffer. 计算机图形中经常使用的技术是使用双缓冲区。 Instead of having the renderer and the producer operate on the same data in memory, each one has its own buffer. 不是让渲染器和生产者对内存中的相同数据进行操作,而是每个都有自己的缓冲区。 This is implemented by using two independent buffers, and switch them when needed. 这是通过使用两个独立的缓冲区实现的,并在需要时切换它们。 The producer updates one buffer, and when it is done, it switches the buffer and fills the second buffer with the next data. 生产者更新一个缓冲区,当它完成后,它会切换缓冲区并用下一个数据填充第二个缓冲区。 Now, while the producer is processing the second buffer, the renderer works with the first one and displays it. 现在,当生产者正在处理第二个缓冲区时,渲染器与第一个缓冲区一起工作并显示它。

You could use this technique by letting the renderer lock the swap operation such that the producer may have to wait until rendering is finished. 您可以通过让渲染器锁定交换操作来使用此技术,以便生产者可能必须等到渲染完成。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM