Как правильно удалить указатель на функцию обратного вызова.
- У меня есть MainProgram.exe, который вызывает MyDll.dll и использует curl для получения данных о функции обратного вызова.
- Я завернул curl в функцию CurlGetData, которая создает экземпляр curl и выполняет curl_easy_perform.
Вот мой код:
//Interface class to derive from
class ICurlCallbackHandler
{
public:
virtual size_t CurlDataCallback( void* pData, size_t tSize ) = 0;
};
//Class that implements interface
class CurlCallbackHandler : public ICurlCallbackHandler
{
public:
bool m_exit = false;
virtual size_t CurlDataCallback( void* pData, size_t tSize ) override
{
if(m_exit)
return CURL_READFUNC_ABORT;
// do stuff with the curl data
return tSize;
}
}
CurlCallbackHandler *m_curlHandler;
//Create an instance of above class in my dll constructor
MyDll:MyDll()
{
m_curlHandler = new CurlCallbackHandler();
}
//Cleanup above class in my dll destructor
MyDll:~MyDll()
{
delete m_curlHandler;
m_curlHandler = nullptr;
}
//Function to start receiving data asynchronously
void MyDll::GetDataAsync()
{
std::async([=]
{
//This will receive data in a new thread and call CurlDataCallback above
//This basically calls easy_perform
CurlGetData(m_curlHandler);
}
}
//Will cause the curl callback to return CURL_READFUNC_ABORT
void MyDll::StopDataAsync()
{
m_curlHandler->m_exit = true;
}
Функция GetDataAsync вызывается из моей основной программы, и она в основном вызывает curl_easy_perform и использует m_curlHandler в качестве своей функции обратного вызова, которая вызывает обратно в CurlDataCallback.
Это все работает нормально, но всякий раз, когда моя основная программа завершает работу, она вызывает MyDll::StopDataAsync, который останавливает обратный вызов данных curl, а затем вызывается деструктор MyDll, который очищает m_curlHandler.
Но я обнаружил, что в этот момент curl еще не закончил с этим обратным вызовом, и программа падает, так как m_curlHandler был удален, но обратный вызов curl в новом асинхронном потоке все еще использует его.
Иногда он нормально закрывается, но иногда происходит сбой из-за того, что curlcallback пытается получить доступ к указателю, удаленному деструктором.
Как мне лучше очистить m_curlHandler? Я хочу избегать тайм-аутов ожидания, так как это повлияет на производительность моей основной программы.
1 ответ
Согласно стандарту C++ MyDll::GetDataAsync()
функция не должна возвращаться немедленно, она должна блокироваться до тех пор, пока асинхронный поток не завершится, что эффективно сделает операцию синхронной. Однако я считаю, что Microsoft намеренно нарушила эту часть std::async
спецификацией, так что на самом деле он возвращается немедленно, и вы можете уничтожить обратный вызов, пока асинхронный поток все еще использует его (именно этой проблемы можно было бы избежать, если бы реализация Microsoft следовала стандарту!)
Решение состоит в том, чтобы держать std::future
тот std::async
возвращает, а затем блокирует это будущее (что обеспечивает завершение асинхронного потока) перед уничтожением обратного вызова.
class MyDLL
{
std::future<void> m_future;
...
};
MyDll:~MyDll()
{
StopDataAsync();
m_future.get(); // wait for async thread to exit.
delete m_curlHandler; // now it's safe to do this
}
//Function to start receiving data asynchronously
void MyDll::GetDataAsync()
{
m_future = std::async([=]
{
//This will receive data in a new thread and call CurlDataCallback above
//This basically calls easy_perform
CurlGetData(m_curlHandler);
}
}
NB ваш m_exit
участник должен быть std::atomic<bool>
(или вы должны использовать мьютекс для защиты всех операций чтения и записи в него), иначе ваша программа имеет гонку данных и поэтому имеет неопределенное поведение.
Я бы также использовал std::unique_ptr<CurlCallbackHandler>
за m_curlHandler
,
Я хочу избегать тайм-аутов ожидания, так как это повлияет на производительность моей основной программы.
Приведенное выше решение заставит вашего деструктора ждать, но только до тех пор, пока обратный вызов не заметит, что m_exit == true
и заставить асинхронный поток прекратить работу. Это означает, что вы ожидаете только столько времени, сколько необходимо, а не больше, в отличие от тайм-аутов, которые означают, что вы должны угадать, как долго это "достаточно долго", а затем, вероятно, добавить немного больше, чтобы быть в безопасности.