如果旧工作完成,如何开始新工作?

时间:2015-12-23 11:31:20

标签: c++ multithreading boost-asio

我正在写一个小游戏,我想将渲染器与主循环分离。在主循环中,我想更新输入,我不想等到我的渲染器完成绘制,但这意味着我只想在渲染器完成绘制时发出绘制命令。

我需要一种方法来了解旧的渲染作业是否已经完成,以便我可以开始一个新的渲染作业。

#include <asio.hpp>
#include <memory>
#include <thread>
#include <iostream>
#include <mutex>
#include <chrono>
struct ready
{
  bool is_ready;
  std::mutex m;
  void set(bool b)
  {
    std::lock_guard<std::mutex> g(m);
    is_ready = b;
  }
  operator bool()
  {
    std::lock_guard<std::mutex> g(m);
    return is_ready;
  }
  ready()
    : is_ready(true)
  {
  }
};

int
main()
{
  auto service = std::make_shared<asio::io_service>();
  auto w = std::make_shared<asio::io_service::work>(*service);
  std::thread t1([&] { service->run(); });
  std::thread t2([&] { service->run(); });
  auto ready_sp = std::make_shared<ready>();
  while (ready_sp) {
    if (*ready_sp) {
      ready_sp->set(false);
      service->dispatch([ready_sp] {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "Draw on thread: " << std::this_thread::get_id()
                  << std::endl;

        ready_sp->set(true);
      });
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << "Doing other stuff" << std::endl;
  }
  w.reset();
  t1.join();
  t2.join();
}

这大致是我会这样做的吗?

3 个答案:

答案 0 :(得分:1)

这有两种一般方法。您需要的具体方法取决于整个应用程序的特定细节。您发布的小代码示例不足以确定所有细节。以下两种常规方法,您需要优化以提出特定于应用程序的解决方案:

可加入线程

使用std::thread::joinable()检查特定线程是否已完成。如果是这样,您可以立即join()该主题,并启动您的下一个主题。

分离的主题

在大多数情况下使用轮询式方法通常很麻烦,基于事件的方法可以更好地运行。这里的典型解决方案是线程将具有std::mutex - 受保护的标志,通常与std::condition_variable配对。线程在线程终止之前设置标志(并可选地发出条件变量信号)。

在这种情况下,通常没有人关心线程的join(),所以线程总是作为一个分离的线程被踢掉,并通过标志检查它的终止(虽然没有&#39; t继续使用可连接线程确实有什么问题,只要第一次检查标志并设置它,检查器就负责加入刚刚终止的线程。

当然,在即将终止的线程设置标志之后有一个小的时间窗口,当它实际终止时,当线程在技术上运行时,这通常是无关紧要的。< / p>

答案 1 :(得分:1)

好的,所以这是我如何接近它的一个例子。

此代码有一个用于渲染的线程(但我们可以使用更多)并使用主线程来玩游戏。

我已将代码拆分为多个问题

scene是描述场景状态的数据

render是呈现场景的自由函数。它不知道线程,锁,互斥或内存管理。

renderer是一个在自己的线程中渲染场景的对象,但只有当被告知时才会这样。

scene_update封装了场景中许多增量更新的概念,但只有在所有增量完成后才应该渲染渲染器 - 即原子更新。

此示例模拟300ms的场景更新计算,但实际渲染需要1秒。因此,我们应该看到每3次更新1次渲染。

希望您同意,因为整个程序使用值语义并封装了所有内存管理和线程问题,所以程序的主体非常容易阅读。

主要是:

int main()
{
    using namespace std;

    // create my scene
    scene my_scene;

    // instantiate my renderer
    renderer my_renderer;

    // tell the renderer that the scene may be rendered
    my_renderer.notify(my_scene);

    // ... while it is doing that...
    // ... lets make our hero march across the wilderness
    for (int x = 0 ; x < 10 ; ++x)
    {
        for(int y = 0 ; y < 10 ; ++y)
        {
            // perform a scene update. the calculations for this update
            // take 300ms (faster than the renderer)
            scene_update u(my_scene, my_renderer);
            {
                my_scene.data().hero_x = x;
                my_scene.data().hero_y = y;
                this_thread::sleep_for(chrono::milliseconds(300));
            }
            // tell the renderer that there is a new scene to render
            u.commit();
        }
    }

    return 0;
}

同样render非常简单:

void render(const scene& s)
{
    using namespace std;

    const auto& data = s.data();

    cout << "the hero is at ";
    cout.flush();
    this_thread::sleep_for(chrono::milliseconds(500));

    cout << data.hero_x << ", ";
    cout.flush();
    this_thread::sleep_for(chrono::milliseconds(500));

    cout << data.hero_y << '\n';
    cout.flush();
}

这是完整的程序:

#include <iostream>
#include <vector>
#include <string>
#include <condition_variable>
#include <thread>
#include <memory>
#include <cassert>


//
// a simple scene object with *value semantics*
// the actual scene data is stored in an inner class, an instance of which is maintained by a unique_ptr
// we could have used a shared_ptr but there is no reason to since we will be taking copies of the scene
// data in order to render it out of line.
// doing it this way means that although the copy might be expensive, it is only performed once
// moves are extremely fast
struct scene
{
    // a type to allow us to create an unitialised scene explicitly
    struct none_type {};

    // a flag object
    static constexpr const none_type none = none_type();

    // this is the actual expensive scene data (simulated)
    struct expensive_large_scene_data
    {
        int hero_x = 0,
        hero_y = 0;
    };

    // a printer function (to help debugging)
    friend std::ostream& operator<<(std::ostream& os, const expensive_large_scene_data& s)
    {
        os << "(" << s.hero_x << ", " << s.hero_y << ")";
        return os;
    }

    // construct empty
    scene(none_type) {
        // no not initialise the pointer
    }

    // construct and initialise a default scene
    scene() : _data(std::make_unique<expensive_large_scene_data>()) {}

    // copy constructor must explicitly clone the pointer (if populated)
    scene(const scene& r)
    : _data(r
            ? std::make_unique<expensive_large_scene_data>(r.data())
            : nullptr)
    {}

    // move constructor
    scene(scene&& r)
    : _data(std::move(r._data))
    {}

    // copy-assignment - take care here too.
    scene& operator=(const scene& r)
    {
        _data = r
        ? std::make_unique<expensive_large_scene_data>(r.data())
        : nullptr;
        return *this;
    }

    // move-assignment is simple
    scene& operator=(scene&& r)
    {
        _data = std::move(r._data);
        return *this;
    }

    // no need for a destructor - we're using unique_ptr

    bool valid() const {
        return bool(_data.get());
    }

    // convertible to bool so we can check whether it is empty easily

    operator bool() const {
        return valid();
    }

    void reset() {
        _data.reset();
    }

    // accessor

    const expensive_large_scene_data& data() const {
        assert(_data.get());
        return *_data;
    }

    expensive_large_scene_data& data() {
        assert(_data.get());
        return *_data;
    }

private:
    std::unique_ptr<expensive_large_scene_data> _data;
};


std::ostream& operator<<(std::ostream& os, const scene& s)
{
    return os << s.data();
}


// a function that renders a scene
// this one takes a second to complete
void render(const scene& s)
{
    using namespace std;

    const auto& data = s.data();

    cout << "the hero is at ";
    cout.flush();
    this_thread::sleep_for(chrono::milliseconds(500));

    cout << data.hero_x << ", ";
    cout.flush();
    this_thread::sleep_for(chrono::milliseconds(500));

    cout << data.hero_y << '\n';
    cout.flush();
}

// the renderer
struct renderer
{
    using mutex_type = std::mutex;
    using lock_type = std::unique_lock<mutex_type>;

    // start thread in constructor - do not copy this object (you can't anyway because of the mutex)
    renderer()
    : _render_thread(std::bind(&renderer::loop, this))
    {}

    // shut down cleanly on destruction
    ~renderer()
    {
        auto lock = lock_type(_mutex);
        _cancelled = true;
        lock.unlock();

        if (_render_thread.joinable())
        {
            _render_thread.join();
        }
    }

    // notify the renderer that a new scene is ready
    void notify(const scene& s)
    {
        auto lock = lock_type(_mutex);
        _pending_scene = s;
        lock.unlock();
        _cv.notify_all();
    }

private:
    void loop()
    {
        for(;;)
        {
            auto lock = lock_type(_mutex);
            _cv.wait(lock, [this] {
                // wait for either a cancel event or for a new scene to be ready
                return _cancelled or _pending_scene;
            });

            if (_cancelled) return;

            // move the pending scene to our scene-render buffer - this is very cheap
            _current_scene = std::move(_pending_scene);
            _pending_scene.reset();
            lock.unlock();

            // unlock early to allow mainline code to continue


            // now take our time rendering the scene
            render(_current_scene);
            _current_scene.reset();
        }
    }

private:
    mutex_type _mutex;
    std::condition_variable _cv;
    bool _cancelled = false;
    scene _pending_scene = scene(scene::none);
    scene _current_scene = scene(scene::none);
    std::thread _render_thread;
};

// an object to connect a scene update 'transaction' with the renderer
struct scene_update
{
    scene_update(scene& s, renderer& r)
    : _s(s), _r(r) {}

    void commit()
    {
        _r.notify(_s);
    }

    scene& _s;
    renderer& _r;
};



int main()
{
    using namespace std;

    // create my scene
    scene my_scene;

    // instantiate my renderer
    renderer my_renderer;

    // tell the renderer that the scene may be rendered
    my_renderer.notify(my_scene);

    // ... while it is doing that...
    for (int x = 0 ; x < 10 ; ++x)
    {
        for(int y = 0 ; y < 10 ; ++y)
        {
            // perform a scene update. the calculations for this update
            // take 300ms (faster than the renderer)
            scene_update u(my_scene, my_renderer);
            {
                my_scene.data().hero_x = x;
                my_scene.data().hero_y = y;
                this_thread::sleep_for(chrono::milliseconds(300));
            }
            // tell the renderer that there is a new scene to render
            u.commit();
        }
    }

    return 0;
}

预期产出:

the hero is at 0, 0            
the hero is at 0, 2         <<-- note the missing updates
the hero is at 0, 5         <<-- because rendering takes longer
the hero is at 0, 8         <<-- than calculation
the hero is at 1, 2

答案 2 :(得分:0)

继续第二个答案。

此版本扩展了将渲染器与实际绘制函数分离的想法。

它创建了两个渲染器,每个渲染器都在自己的线程中。每个渲染器都负责运行自己的视图。

在这种情况下,我们通过为玩家提供第一人称视角和整体地图视图来改进游戏。由于地图视图较小,因此渲染时间较短。

因为每个视图都在自己的线程中运行,并且拥有自己的场景数据副本,所以较小的视图可以通过更频繁地更新来获益 - 但仍然不会比场景模型的实际原子更新更频繁。

以下是修改过的代码:

#include <iostream>
#include <vector>
#include <string>
#include <condition_variable>
#include <thread>
#include <memory>
#include <cassert>
#include <sstream>


//
// a simple scene object with *value semantics*
// the actual scene data is stored in an inner class, an instance of which is maintained by a unique_ptr
// we could have used a shared_ptr but there is no reason to since we will be taking copies of the scene
// data in order to render it out of line.
// doing it this way means that although the copy might be expensive, it is only performed once
// moves are extremely fast
struct scene
{
    // a type to allow us to create an unitialised scene explicitly
    struct none_type {};

    // a flag object
    static constexpr const none_type none = none_type();

    // this is the actual expensive scene data (simulated)
    struct expensive_large_scene_data
    {
        int hero_x = 0,
        hero_y = 0;
    };

    // a printer function (to help debugging)
    friend std::ostream& operator<<(std::ostream& os, const expensive_large_scene_data& s)
    {
        os << "(" << s.hero_x << ", " << s.hero_y << ")";
        return os;
    }

    // construct empty
    scene(none_type) {
        // no not initialise the pointer
    }

    // construct and initialise a default scene
    scene() : _data(std::make_unique<expensive_large_scene_data>()) {}

    // copy constructor must explicitly clone the pointer (if populated)
    scene(const scene& r)
    : _data(r
            ? std::make_unique<expensive_large_scene_data>(r.data())
            : nullptr)
    {}

    // move constructor
    scene(scene&& r)
    : _data(std::move(r._data))
    {}

    // copy-assignment - take care here too.
    scene& operator=(const scene& r)
    {
        _data = r
        ? std::make_unique<expensive_large_scene_data>(r.data())
        : nullptr;
        return *this;
    }

    // move-assignment is simple
    scene& operator=(scene&& r)
    {
        _data = std::move(r._data);
        return *this;
    }

    // no need for a destructor - we're using unique_ptr

    bool valid() const {
        return bool(_data.get());
    }

    // convertible to bool so we can check whether it is empty easily

    operator bool() const {
        return valid();
    }

    void reset() {
        _data.reset();
    }

    // accessor

    const expensive_large_scene_data& data() const {
        assert(_data.get());
        return *_data;
    }

    expensive_large_scene_data& data() {
        assert(_data.get());
        return *_data;
    }

private:
    std::unique_ptr<expensive_large_scene_data> _data;
};


std::ostream& operator<<(std::ostream& os, const scene& s)
{
    return os << s.data();
}


// helper function to serialise access to cout
void emit(const std::string& display_id, const std::string& s)
{
    static std::mutex _m;
    auto lock = std::unique_lock<std::mutex>(_m);
    std::cout << display_id << " : " << s << std::endl;
    std::cout.flush();
}

// this renderer renders the scene from the perspective of the hero. it takes a second to draw
struct first_person_view
{
    // a function that renders a scene
    // this one takes a second to complete
    void operator()(const scene& s) const
    {
        using namespace std;

        const auto& data = s.data();
        std::ostringstream ss;


        ss << "I am at ";
        this_thread::sleep_for(chrono::milliseconds(500));

        ss << data.hero_x << ", ";

        this_thread::sleep_for(chrono::milliseconds(500));

        ss << data.hero_y;
        emit("first_person_view", ss.str());
    }
};

// this renderer renders the scene from the perspective of a top-dowm map. it takes half a second to draw
struct map_view
{
    // a function that renders a scene
    // this one takes half a second to complete
    void operator()(const scene& s) const
    {
        using namespace std;

        const auto& data = s.data();
        std::ostringstream ss;

        ss << "the hero is at ";
        ss << data.hero_x << ", ";

        this_thread::sleep_for(chrono::milliseconds(500));

        ss << data.hero_y;
        emit("map_view", ss.str());
    }
};



// the renderer
template<class RenderFunction>
struct renderer
{
    using mutex_type = std::mutex;
    using lock_type = std::unique_lock<mutex_type>;

    using render_function = RenderFunction;

    // start thread in constructor - do not copy this object (you can't anyway because of the mutex)
    renderer(render_function f)
    : _render_function(std::move(f))
    {
        // defer thread start until all data members initialised
        _render_thread = std::thread(std::bind(&renderer::loop, this));
    }

    // shut down cleanly on destruction
    ~renderer()
    {
        auto lock = lock_type(_mutex);
        _cancelled = true;
        lock.unlock();

        if (_render_thread.joinable())
        {
            _render_thread.join();
        }
    }

    // notify the renderer that a new scene is ready
    void notify(const scene& s)
    {
        auto lock = lock_type(_mutex);
        _pending_scene = s;
        lock.unlock();
        _cv.notify_all();
    }

private:
    void loop()
    {
        for(;;)
        {
            auto lock = lock_type(_mutex);
            _cv.wait(lock, [this] {
                // wait for either a cancel event or for a new scene to be ready
                return _cancelled or _pending_scene;
            });

            if (_cancelled) return;

            // move the pending scene to our scene-render buffer - this is very cheap
            _current_scene = std::move(_pending_scene);
            _pending_scene.reset();
            lock.unlock();

            // unlock early to allow mainline code to continue


            // now take our time rendering the scene
            _render_function(_current_scene);
            _current_scene.reset();
        }
    }

private:
    render_function _render_function;
    mutex_type _mutex;
    std::condition_variable _cv;
    bool _cancelled = false;
    scene _pending_scene = scene(scene::none);
    scene _current_scene = scene(scene::none);
    std::thread _render_thread;
};

template<class...R>
void notify(const scene& the_scene, R&... each_renderer)
{
    using expand = int[];
    (void) expand { 0, (each_renderer.notify(the_scene), 0)... };
}

int main()
{
    using namespace std;

    // create my scene
    scene my_scene;

    // instantiate my renderer
    renderer<first_person_view> my_renderer { first_person_view() };
    renderer<map_view> map_renderer { map_view() };

    // tell the renderer that the scene may be rendered
    notify(my_scene, my_renderer, map_renderer);

    // ... while it is doing that...
    for (int x = 0 ; x < 10 ; ++x)
    {
        for(int y = 0 ; y < 10 ; ++y)
        {
            // perform a scene update. the calculations for this update
            // take 300ms (faster than the renderer)
            my_scene.data().hero_x = x;
            my_scene.data().hero_y = y;
            this_thread::sleep_for(chrono::milliseconds(300));
            // tell the renderer that there is a new scene to render
            notify(my_scene, my_renderer, map_renderer);
        }
    }

    return 0;
}

...和示例输出:

...snip
first_person_view : I am at 7, 5
map_view : the hero is at 7, 7
map_view : the hero is at 7, 8
map_view : the hero is at 8, 0
first_person_view : I am at 7, 8
map_view : the hero is at 8, 2
map_view : the hero is at 8, 3
first_person_view : I am at 8, 2
map_view : the hero is at 8, 5
...snip