Background
I'm taking Operating Systems class this semester, and I was allowed to C++, but not std::thread
or any other facilities that are present in the linux itself. I decided to build my own, and I was pleasantly surprised that it is quite easy to build a basic version. I didn't intend to duplicate std::thread
, as I believe there is some functionality that is harder to implement.
Code with small tests
#ifndef CPP_THREADS_THREAD_HPP
#define CPP_THREADS_THREAD_HPP
#include <pthread.h>
#include <utility>
#include <stdexcept>
#include <string.h>
#include <tuple>
namespace shino
{
namespace details
{
template<typename Lambda>
void *call_target(void *target)
{
Lambda *lambda = reinterpret_cast<Lambda *>(target);
(*lambda)();
//don't forget to cleanup
delete lambda;
return nullptr;
}
template <typename Callable, typename ... Types>
class arguments_wrapper
{
Callable callable;
std::tuple<std::decay_t<Types>...> args;
public:
arguments_wrapper(Callable&& callable, Types&& ... args) :
callable(std::forward<Callable>(callable)),
args{std::forward<Types>(args)...}
{}
void operator()()
{
apply(std::make_index_sequence<sizeof...(Types)>{});
}
private:
template <std::size_t ... indices>
void apply(std::index_sequence<indices...>)
{
callable(std::move(std::get<indices>(args))...);
}
};
}
class thread
{
pthread_t thread_handle;
bool joinable;
public:
using native_handle_type = pthread_t;
template<typename Callable, typename ... ArgTypes>
thread(Callable&& callable, ArgTypes&& ... args) :
joinable(true)
{
//callable=callable causes decay, which is required to store a function pointer
auto *target_function =
new details::arguments_wrapper<Callable, ArgTypes...>(std::forward<Callable>(callable),
std::forward<ArgTypes>(args)...);
using lambda_type = std::remove_reference_t<decltype(*target_function)>;
auto creation_result = pthread_create(&thread_handle,
nullptr,
&details::call_target<lambda_type>,
target_function);
if (creation_result != 0) {
delete target_function;
throw std::runtime_error{strerror(creation_result)};
}
}
thread(const thread& other) = delete;
thread& operator=(const thread& other) = delete;
thread(thread&& other) :
thread_handle(other.thread_handle),
joinable(other.joinable)
{
other.thread_handle = -1;
other.joinable = false;
}
thread& operator=(thread&& other)
{
if (joinable) {
throw std::logic_error{"trying to drop not joined/not detached thread"};
}
//to be replaced by std::exchange in C++17
thread_handle = other.thread_handle;
joinable = other.joinable;
other.thread_handle = -1;
other.joinable = false;
}
native_handle_type native_handle()
{
return thread_handle;
}
bool is_joinable()
{
return joinable;
}
void join()
{
if (joinable) {
auto join_result = pthread_join(thread_handle, NULL);
if (join_result != 0) {
throw std::runtime_error{strerror(join_result)};
}
}
joinable = false;
}
void detach()
{
if (!joinable) {
throw std::logic_error{"trying to detach already joined/detached thread"};
}
joinable = false;
}
~thread() noexcept(false)
{
if (joinable) {
throw std::logic_error{"a thread was left not joined/not detached"};
}
}
};
}
#endif //CPP_THREADS_THREAD_HPP
#include <iostream>
#include <thread>
void print_hello(int x, int y)
{
std::cout << "hello x: " << x << '\n'
<< "hello y: " << y << '\n';
}
void count_until_ten()
{
for (int i = 0; i < 9; ++i)
{
std::cout << i << ' ';
}
std::cout << '\n';
}
void waiting_func(shino::thread t)
{
std::cout << "waiting for thread termination\n";
t.join();
}
struct copy_move_detector
{
copy_move_detector() = default;
copy_move_detector(const copy_move_detector& )
{
std::cout << "copied\n";
}
copy_move_detector(copy_move_detector&&) noexcept
{
std::cout << "moved\n";
}
};
void detect_copy(copy_move_detector det)
{
std::cout << &det << '\n';
}
int main()
{
shino::thread t1{print_hello, 1, 2};
shino::thread t2{count_until_ten};
waiting_func(std::move(t1));
t2.join();
copy_move_detector detector;
std::cout << "testing behavior of shino::thread\n";
std::cout << "testing move behavior\n";
shino::thread shino_t2{detect_copy, copy_move_detector{}};
shino_t2.join();
std::cout << "testing copy behavior\n";
shino::thread shino_t1{detect_copy, detector};
shino_t1.join();
std::cout << "testing behavior of std::thread\n";
std::cout << "testing move behavior\n";
std::thread std_t2{detect_copy, copy_move_detector{}};
std_t2.join();
std::cout << "testing copy behavior\n";
std::thread std_t1{detect_copy, detector};
std_t1.join();
}
How it works
The main gist is on this line:
auto *target_function = new auto(=, callable = callable { callable(args...); });
It allocates a lambda. Yeah, I got surprised myself. new auto
was added in C++11.
I had to add arguments_wrapper
to provide move support. Basically it just accepts callable and arguments.
The arguments application stage is a little bit more interesting. Basically it just forms an index sequence starting from zero to sizeof...(Types)
. libsdc++ played dirty and moved even if not noexcept
, so I decided to do so too.
Then it just passed to call_target
, which calls it when execution context is changed to the newly created thread. The wrapper will not cause ODR violation, as template parameter is mangled into the name as well. That's basically it.
Surprises
New implementation outperforms libstdc++'s by not doing extra moves (in move tests it does one less). During copy test it even managed to allow compiler to hoist the copy! whereas libstdc++ copied and moved twice. Turns out my implementation was cheating by not decaying arguments, though it still outperforms by one move.
Concerns
Is it possible to get rid of wrapper?
General code quality
Misc
Target compiler is gcc 5.4.
I looked up libc++'s implementation, and it looks more complicated. Am I missing something?