Cancelling callbacks in Boost ASIO - multithreading

I've been trying to switch my code from using one io_service per network connection to using shared ones, and I'm seeing some very odd behaviour on server sockets (client ones seem to work OK).
In order to try to work out what's going on I've re-started building up a simple example that will allow me to check my assumptions about everything that ought to happen. The first problem I've hit is that io_service::run doesn't exit when there are no handlers left, and as far as I can tell the handlers aren't removed from the work queue.
I have one thread that does an async_accept followed by an async_read. There is a separate client thread (which has its own io_service). The client thread's io_service is never run, and the server's one is run in yet another thread.
I'm using a condition variable to wait in the server thread for the read to complete (which will never happen as the client never writes). This times out just fine and then I call socket.cancel(). I would expect this to remove the read handler and run to exit as the work queue is now empty.
I do see the read handler get called (with a cancel error), but run never exits. When I tie the socket lifetime to the handler lifetime (by lambda capturing a shared_ptr to the socket) the memory isn't freed either.
The server is set up like this:
std::mutex mutex;
std::unique_lock<std::mutex> lock(mutex);
std::condition_variable signal;
boost::asio::io_service server_service;
boost::asio::ip::tcp::acceptor listener(server_service);
std::mutex read_mutex;
std::unique_lock<std::mutex> read_lock(read_mutex);
std::condition_variable read_done;
std::thread server([&]() {
std::unique_lock<std::mutex> lock(mutex);
listener.open(boost::asio::ip::tcp::v4());
listener.set_option(boost::asio::socket_base::enable_connection_aborted(true));
listener.bind(boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 4567));
listener.listen();
std::shared_ptr<connection> server_cnx(new connection(server_service));
listener.async_accept(server_cnx->socket,
[&, server_cnx](const boost::system::error_code& error) {
log_thread() << "Server got a connection " << error << std::endl;
boost::asio::async_read_until(server_cnx->socket, server_cnx->buffer, '\n',
[&, server_cnx](const boost::system::error_code& error, std::size_t bytes) {
log_thread() << "Got " << bytes << ", " << error << std::endl;
std::unique_lock<std::mutex> lock(read_mutex);
lock.unlock();
read_done.notify_one();
});
});
lock.unlock();
signal.notify_one();
if ( read_done.wait_for(read_lock, std::chrono::seconds(1)) == std::cv_status::timeout ) {
log_thread() << "Server read timed out -- cancelling socket jobs" << std::endl;
server_cnx->socket.cancel();
server_cnx->socket.close();
} else {
log_thread() << "Server data read" << std::endl;
}
log_thread() << "Exiting server thread" << std::endl;
});
signal.wait(lock);
log_thread() << "Server set up" << std::endl;
The io_service thread is set up like this:
std::thread server_io([&]() {
log_thread() << "About to service server IO requests" << std::endl;
try {
server_service.run();
} catch ( ... ) {
log_thread() << "Exception caught" << std::endl;
}
log_thread() << "**** Service jobs all run" << std::endl;
signal.notify_one();
});
The output is as follows:
10.0002 139992957945728 Server set up
10.0005 139992957945728 Client set up
10.0006 139992848398080 About to service server IO requests
10.0006 139992848398080 Server got a connection system:0
11.0003 139992934819584 Server read timed out -- cancelling socket jobs
11.0004 139992934819584 Exiting server thread
11.0004 139992848398080 Got 0, system:125
20.0006 139992957945728 IO thread timed out servicing requests -- stopping it
^^^ This should not happen because the server service should have run out of work
20.0006 139992957945728 Waiting for things to close....
22.0008 139992957945728 Wait over, exiting
(Columns are time + 10s, thread ID, log message)
At the 11 second mark you can see that the async_read_until is called. This is the last handler in the server's io_service and yet run doesn't exit.
Even after the time out waiting for run to exit fires and the waiting thread does io_service::stop(), still run doesn't exit (there's another 2 second wait there).
The full code is on github

The program is invoking undefined behavior when the server thread attempts to unlock the read_lock that it does not own.
int main()
{
...
std::mutex read_mutex;
std::unique_lock<std::mutex> read_lock(read_mutex); // Acquired by main.
std::condition_variable read_done;
std::thread server([&]() { // Capture lock reference.
std::unique_lock<std::mutex> lock(mutex);
...
// The next line invokes undefined behavior as this thread does did
// not acquire read_lock.mutex().
if (read_done.wait_for(read_lock, ...)
// ^^^^^^^^^ caller does not own.
{
...
}
});
signal.wait(lock);
...
}
In particular, when invoking condition_variable::wait_for(lock), the standard requires that lock.owns_lock() is true and lock.mutex() is locked by the calling thread.
Mixing synchronous and asynchronous flows often add complexity. In this particular case, where the synchronous calls are intertwined throughout each layer using lower-level constructs for event/signal notification without a persisted state, I think that it adds unnecessary complexity and overcomplicates the flow. Furthermore, the broad scope of variables can add complexity. If read_lock had never been captured by the lambdas, then a compiler error would have occurred.
Consider the separation in space when trying to observe two events:
// I will eventually be interested when the server starts
// accepting connections, so start setting up now.
std::mutex server_mutex;
std::unique_lock<std::mutex> server_lock(server_mutex);
std::condition_variable server_started;
std::thread server([&]()
{
// I will eventually be interested when the server reads
// data, so start setting up now.
std::mutex read_mutex;
std::unique_lock<std::mutex> read_lock(read_mutex);
std::condition_variable read_done;
listener.async_accept(...,
[&](...)
{
// Got connection.
async_read_until(...,
[&](...)
{
// Someone may be interested that data has been read,
// so use the correct mutex and condition_variable
// pair.
std::unique_lock<std::mutex> read_lock(read_mutex);
read_lock.unlock();
read_done.notify_one();
});
}); // async_accept
// Someone may be interested that I am accepting connections,
// so use the correct mutex and condition_variable pair.
std::unique_lock<std::mutex> server_lock(server_mutex);
server_lock.unlock();
server_done.notify_one();
// I am now interested in if data has been read.
read_done.wait_for(read_lock);
}); // server thread
// I am now interested in if the server has started.
server_started.wait(server_lock);
The caller has to prepare to handle an event, start an operation, then wait for the event, and the operation must know the event the caller is interested in. To worsen the situation, one must now consider lock ordering to prevent deadlocks. Note how in the above example, the server thread acquires the read_mutex and then the server_mutex. Another thread cannot acquire the mutexes in a difference order without introducing the chance of a deadlock. In terms of complexity, this approach scales poorly with the number of events.
It may be worth considering re-examining the program's flow and control structure. If it can be written to be primarily asynchronous, then callback chains, continuations, or a signal-and-slot system (Boost.Signals) may uncomplicate the solution. If one prefers to have asynchronous code read as if it was synchronous, then Boost.Asio's support for coroutines can provide a clean solution. Finally, if one needs to synchronously wait on an asynchronous operation's result or timeout, then consider using Boost.Asio's support for std::future or using them directly.
// Use an asynchronous operation so that it can be cancelled on timeout.
std::future<std::size_t> on_read = boost::asio::async_read_until(
socket, buffer, '\n',boost::asio::use_future);
// If timeout occurs, then cancel the operation.
if (on_read.wait_for(std::chrono::seconds(1)) == std::future_status::timeout)
{
socket.cancel();
}
// Otherwise, the operation completed (with success or error).
else
{
// If the operation failed, then on_read.get() will throw a
// boost::system::system_error.
auto bytes_transferred = on_read.get();
}
While I would strongly advocate re-examining the overall control structure and reducing variable scope, the following sample is roughly equivalent to the above example, but may be slightly easier to maintain with its use of std::future:
// I will eventually be interested when the server starts
// accepting connections, so start setting up now.
std::promise<void> server_started_promise;
auto server_started = server_started_promise.get_future();
std::thread server([&]()
{
// I will eventually be interested when the server reads
// data, so start setting up now.
std::promise<void> read_done_promise;
auto read_done = read_done_promise.get_future();
listener.async_accept(...,
[&](...)
{
// Got connection.
async_read_until(...,
[&](...)
{
// Someone may be interested that data has been read.
read_done_promise.set_value();
});
}); // async_accept
// Someone may be interested that I am accepting connections.
server_started_promise.set_value();
// I am now interested in if data has been read.
read_done.wait_for(...);
}); // server thread
// I am now interested in if the server has started.
server_started.wait();
Here is a complete example based on the original code that demonstrates using std::future to control flow and timeout asynchronous operations in a synchronous manner:
#include <future>
#include <iostream>
#include <thread>
#include <boost/asio.hpp>
#include <boost/asio/use_future.hpp>
#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>
int main()
{
using boost::asio::ip::tcp;
// Setup server thread.
boost::asio::io_service server_io_service;
std::promise<tcp::endpoint> server_promise;
auto server_future = server_promise.get_future();
// Start server thread.
std::thread server_thread(
[&server_io_service, &server_promise]
{
tcp::acceptor acceptor(server_io_service);
acceptor.open(tcp::v4());
acceptor.set_option(
boost::asio::socket_base::enable_connection_aborted(true));
acceptor.bind(tcp::endpoint(tcp::v4(), 0));
acceptor.listen();
// Handlers will not chain work, so control the io_service with a work
// object.
boost::optional<boost::asio::io_service::work> work(
boost::in_place(std::ref(server_io_service)));
// Accept a connection.
tcp::socket server_socket(server_io_service);
auto on_accept = acceptor.async_accept(server_socket,
boost::asio::use_future);
// Server has started, so notify caller.
server_promise.set_value(acceptor.local_endpoint());
// Wait for connection or error.
boost::system::system_error error =
make_error_code(boost::system::errc::success);
try
{
on_accept.get();
}
catch (const boost::system::system_error& e)
{
error = e;
}
std::cout << "Server got a connection " << error.code() << std::endl;
// Read from connection.
boost::asio::streambuf buffer;
auto on_read = boost::asio::async_read_until(
server_socket, buffer, '\n', boost::asio::use_future);
// The async_read operation is work, so destroy the work object allowing
// run() to exit.
work = boost::none;
// Timeout the async read operation.
if (on_read.wait_for(std::chrono::seconds(1)) ==
std::future_status::timeout)
{
std::cout << "Server read timed out -- cancelling socket jobs"
<< std::endl;
server_socket.close();
}
else
{
error = make_error_code(boost::system::errc::success);
std::size_t bytes_transferred = 0;
try
{
bytes_transferred = on_read.get();
}
catch (const boost::system::system_error& e)
{
error = e;
}
std::cout << "Got " << bytes_transferred << ", "
<< error.code() << std::endl;
}
std::cout << "Exiting server thread" << std::endl;
});
// Wait for server to start accepting connections.
auto server_endpoint = server_future.get();
std::cout << "Server set up" << std::endl;
// Client thread.
std::promise<void> promise;
auto future = promise.get_future();
std::thread client_thread(
[&server_endpoint, &promise]
{
boost::asio::io_service io_service;
tcp::socket client_socket(io_service);
boost::system::error_code error;
client_socket.connect(server_endpoint, error);
std::cout << "Connected " << error << std::endl;
promise.set_value();
// Keep client socket alive, allowing server to timeout.
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Exiting client thread" << std::endl;
});
// Wait for client to connect.
future.get();
std::cout << "Client set up" << std::endl;
// Reset generic promise and future.
promise = std::promise<void>();
future = promise.get_future();
// Run server's io_service.
std::thread server_io_thread(
[&server_io_service, &promise]
{
std::cout << "About to service server IO requests" << std::endl;
try
{
server_io_service.run();
}
catch (const std::exception& e)
{
std::cout << "Exception caught: " << e.what() << std::endl;
}
std::cout << "Service jobs all run" << std::endl;
promise.set_value();
});
if (future.wait_for(std::chrono::seconds(3)) ==
std::future_status::timeout)
{
std::cout << "IO thread timed out servicing requests -- stopping it"
<< std::endl;
server_io_service.stop();
}
// Join all threads.
server_io_thread.join();
server_thread.join();
client_thread.join();
}

So of course this multi-threading is a tricky business. Turns out that in this case the read lock is acquired in the wrong place so the handler is being blocked by the thread waiting for it to complete.
I guess the lesson here is to never handle thread locks without some sort of time out.

Related

Does a condition variable really need another variable?

Note: I'll give examples in C++ but I believe my question is language-agnostic. Correct me if I'm wrong.
Just so you really understand me - what I'm trying to learn here is what the tool does and nothing else. Not what it's usually used for, not what the conventions says, just what the blunt tool does. In this case - what the condition variable does.
So far it seems to me like it's a simple mechanism that allows threads to wait (block) until some other thread signals them (unblocks them). Nothing more, no dealing with critical section access or data access (of course they can be used for that but it's only a matter of programmer's choice). Also the signaling is usually only done when something important happens (e.g. data was loaded) but theoretically it could be called at any time. Correct so far?
Now, every example that I have seen uses a condition variable object (e.g. std::condition_variable) but also some additional variable to mark if something happened (e.g. bool dataWasLoaded). Take a look at this example from https://thispointer.com//c11-multithreading-part-7-condition-variables-explained/:
#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;
class Application
{
std::mutex m_mutex;
std::condition_variable m_condVar;
bool m_bDataLoaded;
public:
Application()
{
m_bDataLoaded = false;
}
void loadData()
{
// Make This Thread sleep for 1 Second
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::cout << "Loading Data from XML" << std::endl;
// Lock The Data structure
std::lock_guard<std::mutex> guard(m_mutex);
// Set the flag to true, means data is loaded
m_bDataLoaded = true;
// Notify the condition variable
m_condVar.notify_one();
}
bool isDataLoaded()
{
return m_bDataLoaded;
}
void mainTask()
{
std::cout << "Do Some Handshaking" << std::endl;
// Acquire the lock
std::unique_lock<std::mutex> mlock(m_mutex);
// Start waiting for the Condition Variable to get signaled
// Wait() will internally release the lock and make the thread to block
// As soon as condition variable get signaled, resume the thread and
// again acquire the lock. Then check if condition is met or not
// If condition is met then continue else again go in wait.
m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));
std::cout << "Do Processing On loaded Data" << std::endl;
}
};
int main()
{
Application app;
std::thread thread_1(&Application::mainTask, &app);
std::thread thread_2(&Application::loadData, &app);
thread_2.join();
thread_1.join();
return 0;
}
Now, other than the std::condition_variable m_condVar it also uses an additional variable bool m_bDataLoaded. But it seems to me that the thread performing mainTask is already notified that the data was loaded by means of std::condition_variable m_condVar. Why also check bool m_bDataLoaded for the same information? Compare (the same code without bool m_bDataLoaded):
#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;
class Application
{
std::mutex m_mutex;
std::condition_variable m_condVar;
public:
Application()
{
}
void loadData()
{
// Make This Thread sleep for 1 Second
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::cout << "Loading Data from XML" << std::endl;
// Lock The Data structure
std::lock_guard<std::mutex> guard(m_mutex);
// Notify the condition variable
m_condVar.notify_one();
}
void mainTask()
{
std::cout << "Do Some Handshaking" << std::endl;
// Acquire the lock
std::unique_lock<std::mutex> mlock(m_mutex);
// Start waiting for the Condition Variable to get signaled
// Wait() will internally release the lock and make the thread to block
// As soon as condition variable get signaled, resume the thread and
// again acquire the lock. Then check if condition is met or not
// If condition is met then continue else again go in wait.
m_condVar.wait(mlock);
std::cout << "Do Processing On loaded Data" << std::endl;
}
};
int main()
{
Application app;
std::thread thread_1(&Application::mainTask, &app);
std::thread thread_2(&Application::loadData, &app);
thread_2.join();
thread_1.join();
return 0;
}
Now I know about spurious wakeups and they alone necessitate the usage of an additional variable. My question is - are they they only reason for it? If they didn't occur could one just use condition variables without any additional variables (and btw wouldn't that make the name "condition variable" a misnomer then)?
Another thing is - isn't the usage of additional variables the only reason why condition variables also require a mutex? If not, what are the other reasons?
If additional variables are necessary (for spurious wakeups or other reasons) why doesn't the API require them (in the 2nd code I didn't have to use them for the code to compile)? (I don't know if it's the same in other languages, so this question might be C++-specific.)
It's not all about spurious wakeups.
When you call m_condvar.wait, how do you know the condition you're waiting for has not already happened?
Maybe 'loadData' has already been called in another thread. When it called notify_one(), nothing happened because there were no threads waiting.
Now if you call condvar.wait, you will wait forever because nothing will signal you.
The original version does not have this problem, because:
If m_bDataLoaded is false, then it knows that the data is not loaded, and that after m_bDataLoaded is set true, the caller will signal the condition;
The lock is held, and we know that m_bDataLoaded cannot be modified in another thread until it's released;
condvar.wait will put the current thread in the waiting queue before releasing the lock, so we know that m_bDataLoaded will be set true after we start waiting, and so notify_one will also be called after we start waiting.
To answer your other questions:
Yes, coordination with additional variables is the reason why condition variables are tied to mutexes.
The API doesn't require, say, a boolean variable, because that's not always the kind of condition you're waiting for.
This kind of thing is common, for example:
Task *getTask() {
//anyone who uses m_taskQueue or m_shutDown must lock this mutex
unique_lock<mutex> lock(m_mutex);
while (m_taskQueue.isEmpty()) {
if (m_shutdown) {
return null;
}
// this is signalled after a task is enqueued
// or m_shutdown is asserted
m_condvar.wait(lock);
}
return taskQueue.pop_front();
}
Here we require the same critical guarantee that the thread starts waiting before the lock is released, but the condition we're waiting for is more complex, involving a variable and separate data structure, and there are multiple ways to exit the wait.
Yes, the condition variable is just useful to wait for an event. In my point of view you should not try to use it for controlling concurrent access of critical data structures.
I just can speak about C++. As you see in the example here https://en.cppreference.com/w/cpp/thread/condition_variable/wait, they used this expression cv.wait(lk, []{return i == 1;});. And []{...} is the expression of a nameless function. So you can also write your own function and give the name of the function:
bool condFn()
{
std::cout << "condFn" << std::endl; // debug output ;)
return i == 1;
}
void waits()
{
std::unique_lock<std::mutex> lk(cv_m);
std::cerr << "Waiting... \n";
cv.wait(lk, condFn);
std::cerr << "...finished waiting. i == 1\n";
}
And inside this function you can evaluate, whatever you want. The thread is always sleeping until it gets notified, then it processes always the function that evaluates the condition for continue working. In case of true, the thread continues, in case of false the programm goes sleeping again.

Get result of future without blocking

This question has been asked before and if I am not wrong, the only way to read the result of a future is either to call get() and block until it is ready or using wait_for() with zero duration as mentioned in the answer - Get the status of a std::future
But, if I just want a worker thread to return me a result that I want it to compute and not wait or block myself for it to complete, can I not just pass it a callback that the worker thread can call when it has computed the result for me? Something like below -
#include <iostream>
#include <thread>
#include <functional>
void foo(std::function<void(int)> callback)
{
int result = 5;
callback(result);
}
int main()
{
int result = 0;
std::thread worker(foo, [](int result)
{
std::cout << "Result from worker is " << result << std::endl;
});
worker.join();
}
Here, the worker thread would just execute the callback when it has computed the result for me. I don't have to wait for it to finish or block or check in a loop to know when it's ready.
Please advice is this is a good approach to be used as currently there is no way to do this without blocking or checking for it in a loop?
You can certainly create your own thread with a callback, but as soon as you move away from a toy example you will notice that you have potentially created a synchronization problem. This is because your callback is being invoked from a separate thread. So you may want to have the worker thread instead post a message to a queue which you will read later, unless there is no shared state or a mutex is already in place.
In your specific example, let's add one line of code:
int main()
{
std::thread worker(foo, [](int result)
{
std::cout << "Result from worker is " << result << std::endl;
});
std::cout << "I am the main thread" << std::endl; // added
worker.join();
}
You might think that there are only two possible outputs:
I am the main thread
Result from worker is 5
and
Result from worker is 5
I am the main thread
But in fact there are other possible outputs, such as:
Result from worker is I am the main thread
5
So you have created a bug. You either need synchronization on your shared state (which includes I/O), or you need to orchestrate everything from the main thread (which is what blocking or checking for a future result gives you).

c++11 threads vs async

Consider the following two snippets of code where I am trying to launch 10000 threads:
Snippet 1
std::array<std::future<void>, 10000> furArr_;
try
{
size_t index = 0;
for (auto & fut : furArr_)
{
std::cout << "Created thread # " << index++ << std::endl;
fut = std::async(std::launch::async, fun);
}
}
catch (std::system_error & ex)
{
std::string str = ex.what();
std::cout << "Caught : " << str.c_str() << std::endl;
}
// I will call get afterwards, still 10000 threads should be active by now assuming "fun" is time consuming
Snippet 2
std::array<std::thread, 10000> threadArr;
try
{
size_t index = 0;
for (auto & thr : threadArr)
{
std::cout << "Created thread # " << index++ << std::endl;
thr = std::thread(fun);
}
}
catch (std::system_error & ex)
{
std::string str = ex.what();
std::cout << "Caught : " << str.c_str() << std::endl;
}
The first case always succeeds .i.e. I am able to create 10000 threads and then I have to wait for all of them to finish. In the second case, almost always I end up getting an exception("resource unavailable try again") after creating 1600+ threads.
With a launch policy of std::launch::async, I thought that the two snippets should behave the same way. How different std::async with a launch policy of async is from launching a thread explicitly using std::thread?
I am on Windows 10, VS2015, binary is built in x86 release mode.
Firstly, thanks to Igor Tandetnik for giving me the direction for this answer.
When we use std::async (with async launch policy), we are saying:
“I want to get this work done on a separate thread”.
When we use std::thread we are saying:
“I want to get this work done on a new thread”.
The subtle difference means that async (is usually) implemented using thread pools. Which means if we have invoked a method using async multiple times, often the thread id inside that method will repeat i.e. async allocates multiple jobs to the same set of threads from the pool. Whereas with std::thread, it never will.
This difference means that launching threads explicitly will be potentially more resource intensive (and thus the exception) than using async with async launch policy.

Using boost::thread to start/stop logging data

I'm currently trying to log real-time data by using boost::thread and a check box. When I check the box, the logging thread starts. When I uncheck, the logging thread stops. The problem arises when I check/uncheck repeatedly and very fast (program crashes, some files aren't logged, etc.). How can I write a reliable thread-safe program where these problems don't occur when repeatedly and quickly checking/unchecking? I also don't want to use join() since this temporarily stops the data input coming from the main thread. Below is a code snippet:
//Main thread
if(m_loggingCheckBox->isChecked())
{
...
if(m_ThreadLogData.InitializeReadThread(socketInfo))//opens the socket.
//If socket is opened and can be read, start thread.
m_ThreadLogData.StartReadThread();
else
std::cout << "Did not initialize thread\n";
}
else if(!m_loggingCheckBox->isChecked())
{
m_ThreadLogData.StopReadThread();
}
void ThreadLogData::StartReadThread()
{
//std::cout << "Thread started." << std::endl;
m_stopLogThread = false;
m_threadSendData = boost::thread(&ThreadLogData::LogData,this);
}
void ThreadLogData::StopReadThread()
{
m_stopLogThread = true;
m_ReadDataSocket.close_socket(); // close the socket
if(ofstreamLogFile.is_open())
{
ofstreamLogFile.flush(); //flush the log file before closing it.
ofstreamLogFile.close(); // close the log file
}
m_threadSendData.interrupt(); // interrupt the thread
//m_threadSendData.join(); // join the thread. Commented out since this temporarily stops data input.
}
//secondary thread
bool ThreadLogData::LogData()
{
...
while(!m_stopLogThread)
{
try {
//log the data to an output file
...
boost::this_thread::interruption_point();
} catch (boost::thread_interrupted& interruption) {
std::cout << "ThreadLogData::LogData(): Caught Interruption thread." << std::endl;
StopReadThread();
} catch (...) {
std::cout << "ThreadLogData::LogData(): Caught Something." << std::endl;
StopReadThread();
}
} // end while()
}

Thread management in a game loop?

I am in the middle of developing a game and came across the problem of multithreading. I already used multithreading successfully when loading resources. I did that by creating some threads at some point, assigned them functions, and waited for them to finish, while drawing a loading screen, pretty straightforward.
Now I want to create some threads, that can wait idle till they receive a function, when they do, solve that, then stay idle again. They must operate in a game loop, which is roughly like this (I came up with these function names just for easy visualization):
std::thread t0,t1;
while(gamerunning)
{
UpdateGame();
t0.receiveFunc( RenderShadow );
t1.receiveFunc( RenderScene );
WaitForThreadstoFinishWork();
RenderEverything(); //Only draw everything if the threads finished (D3D11's Deferred Context rendering)
}
t0.Destroy();
t1.Destroy();
My rendering engine is working, and for the time being (for testing), I created threads in my game loop, which is a terrible way of even a quick test, because my rendering speed even slowed down. By the way, I am using C++11's library.
Long story short, I want to create threads before my game loop takes place, and use those in the game loop afterwards, hope someone can help me out. If it is an option, I would really want to stay away from the lower levels of threading, I just need the most straightforward way of doing this.
Following your most recent comments, here is an example implementation of a thread that wakes up on demand, runs its corresponding task and then goes back to sleep, along with the necessary functions to manage it (wait for task completion, ask for shutdown, wait for shutdown).
Since your set of functions is fixed, all you'll have left to do is to create as much threads as you need (ie. 7, probably in a vector), each with its own corresponding task.
Note that once you remove the debugging couts there's little code left, so I don't think there is a need to explain the code (it's pretty self-explanatory IMHO). However don't hesitate to ask if you need explanations on some details.
class TaskThread {
public:
TaskThread(std::function<void ()> task)
: m_task(std::move(task)),
m_wakeup(false),
m_stop(false),
m_thread(&TaskThread::taskFunc, this)
{}
~TaskThread() { stop(); join(); }
// wake up the thread and execute the task
void wakeup() {
auto lock = std::unique_lock<std::mutex>(m_wakemutex);
std::cout << "main: sending wakeup signal..." << std::endl;
m_wakeup = true;
m_wakecond.notify_one();
}
// wait for the task to complete
void wait() {
auto lock = std::unique_lock<std::mutex>(m_waitmutex);
std::cout << "main: waiting for task completion..." << std::endl;
while (m_wakeup)
m_waitcond.wait(lock);
std::cout << "main: task completed!" << std::endl;
}
// ask the thread to stop
void stop() {
auto lock = std::unique_lock<std::mutex>(m_wakemutex);
std::cout << "main: sending stop signal..." << std::endl;
m_stop = true;
m_wakecond.notify_one();
}
// wait for the thread to actually be stopped
void join() {
std::cout << "main: waiting for join..." << std::endl;
m_thread.join();
std::cout << "main: joined!" << std::endl;
}
private:
std::function<void ()> m_task;
// wake up the thread
std::atomic<bool> m_wakeup;
bool m_stop;
std::mutex m_wakemutex;
std::condition_variable m_wakecond;
// wait for the thread to finish its task
std::mutex m_waitmutex;
std::condition_variable m_waitcond;
std::thread m_thread;
void taskFunc() {
while (true) {
{
auto lock = std::unique_lock<std::mutex>(m_wakemutex);
std::cout << "thread: waiting for wakeup or stop signal..." << std::endl;
while (!m_wakeup && !m_stop)
m_wakecond.wait(lock);
if (m_stop) {
std::cout << "thread: got stop signal!" << std::endl;
return;
}
std::cout << "thread: got wakeup signal!" << std::endl;
}
std::cout << "thread: running the task..." << std::endl;
// you should probably do something cleaner than catch (...)
// just ensure that no exception propagates from m_task() to taskFunc()
try { m_task(); } catch (...) {}
std::cout << "thread: task completed!" << std::endl;
std::cout << "thread: sending task completed signal..." << std::endl;
// m_wakeup is atomic so there is no concurrency issue with wait()
m_wakeup = false;
m_waitcond.notify_all();
}
}
};
int main()
{
// example thread, you should really make a pool (eg. vector<TaskThread>)
TaskThread thread([]() { std::cout << "task: running!" << std::endl; });
for (int i = 0; i < 2; ++i) { // dummy example loop
thread.wakeup();
// wake up other threads in your thread pool
thread.wait();
// wait for other threads in your thread pool
}
}
Here's what I get (actual order varies from run to run depending on thread scheduling):
main: sending wakeup signal...
main: waiting for task completion...
thread: waiting for wakeup or stop signal...
thread: got wakeup signal!
thread: running the task...
task: running!
thread: task completed!
thread: sending task completed signal...
thread: waiting for wakeup or stop signal...
main: task completed!
main: sending wakeup signal...
main: waiting for task completion...
thread: got wakeup signal!
thread: running the task...
task: running!
thread: task completed!
thread: sending task completed signal...
thread: waiting for wakeup or stop signal...
main: task completed!
main: sending stop signal...
main: waiting for join...
thread: got stop signal!
main: joined!

Resources