我正在写一个小游戏,我想将渲染器与主循环分离。在主循环中,我想更新输入,我不想等到我的渲染器完成绘制,但这意味着我只想在渲染器完成绘制时发出绘制命令。
我需要一种方法来了解旧的渲染作业是否已经完成,以便我可以开始一个新的渲染作业。
#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();
}
这大致是我会这样做的吗?
答案 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