5
\$\begingroup\$

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();
}

Live on Wandbox.

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?

\$\endgroup\$
7
\$\begingroup\$

The pthread library is a C Library. As such it only understands the C ABI. Therefore pthread_create can only be passed functions that have a C ABI which means they need to be declared with extern "C".

This means you can not portably pass a template function!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.