Безопасно ли использовать shared_ptr и weak_ptr для управления временем жизни std::function?

#c #boost-asio #shared-ptr #boost-thread #std-function

#c #boost-asio #shared-ptr #boost-thread #std-функция

Вопрос:

Я создал оболочку вокруг boost::asio::io_service для обработки асинхронных задач в потоке GUI приложения OpenGL.

Задачи могут создаваться из других потоков, поэтому boost::asio это кажется идеальным для этой цели и означает, что мне не нужно писать свою собственную очередь задач с соответствующими мьютексами и блокировкой. Я хочу, чтобы работа над каждым кадром выполнялась ниже допустимого порога (например, 5 мс), поэтому я звоню poll_one до тех пор, пока не будет превышен желаемый бюджет, а не вызываю run . Насколько я могу судить, это требует от меня вызова reset всякий раз, когда публикуются новые задачи, которые, похоже, работают хорошо.

Поскольку он короткий, вот и все, без #include :

 typedef std::function<void(void)> VoidFunc;
typedef std::shared_ptr<class UiTaskQueue> UiTaskQueueRef;

class UiTaskQueue {

public:

    static UiTaskQueueRef create()
    {
        return UiTaskQueueRef( new UiTaskQueue() );
    }

    ~UiTaskQueue() {} 

    // normally just hand off the results of std/boost::bind to this function:
    void pushTask( VoidFunc f )
    {
        mService.post( f );
        mService.reset();
    }

    // called from UI thread; defaults to ~5ms budget (but always does one call)        
    void update( const float amp;budgetSeconds = 0.005f )
    {
        // getElapsedSeconds is a utility function from the GUI lib I'm using
        const float t = getElapsedSeconds();
        while ( mService.poll_one() amp;amp; getElapsedSeconds() - t < budgetSeconds );
    }

private:

    UiTaskQueue() {}

    boost::asio::io_service mService;
};
  

Я сохраняю экземпляр UiTaskQueueRef в своем основном классе приложения и вызываю mUiTaskQueue->update() из цикла анимации моего приложения.

Я хотел бы расширить функциональность этого класса, чтобы разрешить отмену задачи. Моя предыдущая реализация (использующая почти тот же интерфейс) возвращала числовой идентификатор для каждой задачи и разрешала отменять задачи с использованием этого идентификатора. Но теперь управление очередью и связанной с ней блокировкой осуществляется boost::asio я не уверен, как лучше это сделать.

Я предпринял попытку, обернув любые задачи, которые я, возможно, захочу отменить, в shared_ptr и создав объект-оболочку, который хранит weak_ptr для задачи и реализует () оператор, чтобы его можно было передать io_service . Это выглядит так:

 struct CancelableTask {
    CancelableTask( std::weak_ptr<VoidFunc> f ): mFunc(f) {}
    void operator()(void) const {
        std::shared_ptr<VoidFunc> f = mFunc.lock();
        if (f) {
            (*f)();
        }
    }
    std::weak_ptr<VoidFunc> mFunc;
};
  

Затем у меня возникает перегрузка моего pushTask метода, которая выглядит следующим образом:

 void pushTask( std::weak_ptr<VoidFunc> f )
{
    mService.post( CancelableTask(f) );
    mService.reset();
}
  

Затем я отправляю отменяемые задачи в очередь, используя:

 std::function<void(void)> *task = new std::function<void(void)>( boost::bind(amp;MyApp::doUiTask, this) );
mTask = std::shared_ptr< std::function<void(void)> >( task );
mUiTaskQueue->pushTask( std::weak_ptr< std::function<void(void)> >( mTask ) );
  

Или с помощью VoidFunc typedef, если вы предпочитаете:

 VoidFunc *task = new VoidFunc( std::bind(amp;MyApp::doUiTask, this) );
mTask = std::shared_ptr<VoidFunc>( task );
mUiTaskQueue->pushTask( std::weak_ptr<VoidFunc>( mTask ) );
  

Пока я сохраняю shared_ptr to mTask , то io_service будет выполняться задача. Если я вызываю reset on mTask , то weak_ptr не удается заблокировать, и задача пропускается по желанию.

Мой вопрос действительно касается уверенности во всех этих новых инструментах: new std::function<void(void)>( std::bind( ... ) ) нормально ли это делать и безопасно ли управлять с помощью shared_ptr ?

Ответ №1:

Да, это безопасно.

Для кода:

 VoidFunc *task = new VoidFunc( std::bind(amp;MyApp::doUiTask, this) );
mTask = std::shared_ptr<VoidFunc>( task );
  

Просто сделайте:

 mTask.reset(new VoidFunc( std::bind(amp;MyApp::doUiTask, this) ) );
  

(и в других местах).

Имейте в виду, что вам нужно иметь дело с состоянием гонки, когда протектор может блокировать weak_ptr непосредственно перед сбросом shared_ptr, сохраняя обратный вызов, и в результате вы иногда будете видеть обратные вызовы, даже если вы пошли по пути кода, сбрасывая обратный вызов shared_ptr.

Комментарии:

1. Большое спасибо! Я думаю, что смогу избежать состояния гонки, только отменив из потока пользовательского интерфейса, где будут обрабатываться задачи, но я буду иметь это в виду для многопоточной версии этого кода.

2. Также спасибо за совет по инициализации!