alsora
alsora

Reputation: 663

C++11 shared_ptr and pthread

I have a library with APIs using std::shared_ptr as arguments.

I would like to use these APIs together with pthreads.

What I'm doing is: getting a raw pointer from the shared_ptr, in order to pass it to the pthread. Create a new shared_ptr from the raw one and call my API from another thread. However I get a double free or corruption error when converting the raw pointer back to a shared one.

This is my code

#include <memory>
#include <iostream>
#include <thread>
#include <pthread.h>

void* print_task(void* ptr) 
{
    int* val_raw = static_cast<int*>(ptr);
    std::shared_ptr<int> val(val_raw);

    // CALL MY API WHICH TAKES A SHARED_PTR AS ARGUMENT
    std::cout<<"thread job done \n";
}

int main(int argc, char ** argv)
{

   pthread_t thread;

   std::shared_ptr<int> val = std::make_shared<int>(10);
   pthread_create(&thread, nullptr, &print_task, static_cast<void *>(val.get()));

   std::this_thread::sleep_for(std::chrono::seconds(5));

   return 0;
}

I guess that I'm doing something wrong with all the conversions from shared to raw pointer, because the same code using std::threads (where I can pass directly a shared_ptr) works. However I need to set thread priorities, thus I'm trying to do this with pthreads.

Do you know how to change my code in order to be able to pass a shared pointer and use it inside a pthread?

Upvotes: 2

Views: 2197

Answers (2)

Galik
Galik

Reputation: 48655

I think this is a tricky little conundrum to be honest. The problem you are having is that by passing the raw pointer to your thread function you end up with two independant shared pointers managing the same object instead of two connected shared pointers sharing ownership.

Consequently both shared pointers try to delete it.

You also have a nasty lifetime problem to avoid if you pass a pointer to the shared pointer itself because you can not guarantee the shared pointer will not go out of scope before the new thread has copied it.

I get round that by passing a second dynamically allocated std::shared_ptr created from the original std::shared_ptr which guarantees the shared pointer's control block will live until the new thread tries to copy it.

#include <memory>
#include <iostream>
#include <thread>
#include <pthread.h>

void* print_task(void* ptr)
{
    // obtain the shared pointer from the dynamically created one guarantees
    // we will bee accessing a living control block preventing an
    // end-of-lifetime catastrophe
    std::shared_ptr<int> val = *static_cast<std::shared_ptr<int>*>(ptr);

    // DON'T FORGET TO DELETE THIS!!!
    // We had to allocate this dynamically to guarantee it lived until after
    // it was used.
    delete static_cast<std::shared_ptr<int>*>(ptr);

    // CALL MY API WHICH TAKES A SHARED_PTR AS ARGUMENT
    std::cout << "thread job done \n";

    return nullptr;
}

int main()
{
    pthread_t thread;

    // create the shared resource. This MAY go out of scope
    // before the new thread copies it (however unlikely you
    // think that is).
    std::shared_ptr<int> val = std::make_shared<int>(10);

    // So instead of sending the shared pointer we create a NEW std::shared_ptr
    // which will keep the shared pointer's control block alive even if the original
    // shared pointer goes out of scope.
    pthread_create(&thread, nullptr, &print_task,
        static_cast<void*>(new std::shared_ptr<int>(val)));

    // Do other time consuming thread stuff here.
    std::this_thread::sleep_for(std::chrono::seconds(2));


    // Tidy up.
    void* ret = nullptr;
    pthread_join(thread, &ret);

    return 0;
}

Of course you now have a new/delete pair here but you still gain in passing the shared ownership round between threads.

I suppose you can even get rid of that delete by adopting the passed in raw pointer by a temporary std::unique_ptr.

void* print_task(void* ptr)
{
    // obtain the shared pointer from the dynamically created one guarantees
    // we will bee accessing a living control block preventing an
    // end-of-lifetime catastrophe
    std::shared_ptr<int> val = *std::unique_ptr<std::shared_ptr<int>>(static_cast<std::shared_ptr<int>*>(ptr));

    // CALL MY API WHICH TAKES A SHARED_PTR AS ARGUMENT
    std::cout << "thread job done \n";

    return nullptr;
}

Upvotes: 1

Ulrich Eckhardt
Ulrich Eckhardt

Reputation: 17444

As mentioned in the comments already, the problem is passing a shared pointer through a raw void pointer, so I'll ignore the threading part for now:

// this is what we have and what we want to pass to the given function
shared_ptr<some_type> sptr;

// function to somehow pass the shared pointer to
void function(void* ptr);

// As always, when passing anything that doesn't fit into
// the raw pointer, we need to do dynamic allocation:
void* arg = new shared_ptr<some_type>(sptr);

// we can now pass this to the function as intended:
function(arg);

// Note that we give up ownership of the dynamically allocated
// shared pointer instance. Hence, the called function must
// release that object again (it takes ownership). The function
// therefore starts like this:
void function(void* ptr)
{
    // convert the typeless pointer to a typed pointer again
    shared_ptr<some_type>* psptr = static_cast<shared_ptr<some_type>*>(ptr);
    // move the content to a new, local instance
    shared_ptr<some_type> sptr = *psptr;
    // release the dynamically allocated shared pointer again
    delete psptr;
    /// ... code using sptr here ...
}

Now, while this is guaranteed to work, under some circumstances this may not be an optimal solution:

  • Firstly, the ups and downs of the reference counter don't come for free, especially since that is done in a thread-safe, atomic way. Copying the shared pointer inside the function just to then delete the copied pointer can be avoided. Just create as empty instance and swap() it with the pointer to copy. Assuming swap() is specialized, which is a safe bet because it's an obvious optimization, that then boils down to swapping two raw pointers. This swap doesn't have to be thread-safe and is therefore much faster.
  • Secondly, dynamic allocations are expensive. You could avoid this and the manual release by passing the address of the original object to the function, but then you would have to guarantee that the object is not touched while the function is still executing. Especially with threads, that requires taking extra care.

Upvotes: 2

Related Questions