我更简单地重述此问题,并且在较早的版本中使用较简单的MCVE并没有获得太大的吸引力。
我给人的印象是,在main()
结束之后,发生的所有事情都是按顺序破坏全局对象,然后是静态对象。
我从来没有考虑过在main()
结束到过程结束之间的这段时间内发生其他“东西”的可能性。但是我最近一直在使用Linux计时器,从实验上看,似乎可以在进程的“后期”,在main()
退出后,甚至在销毁静态全局对象之后调用计时器的回调。
问题:该评估正确吗?静态全局对象销毁后可以调用计时器回调吗?
我从未想过这个“后期”在流程生命周期中会发生什么。我想在main()
退出后,我天真地假设了“某事”,“已预防”,“某事正在发生”。
问题:我的计时器回调使用静态的全局对象-目的是使该对象“始终”存在,而不管何时调用该回调。但是,如果可以在销毁静态全局对象之后调用计时器回调,则该策略是不安全的。有没有一种众所周知的/正确的方法来处理此问题:即防止计时器回调访问过无效的对象/内存?
下面的代码创建了“许多”计时器,这些计时器设置为在将来的2秒后过期,其回调引用了一个静态全局对象。在调用计时器回调时,main()
大约在中间退出。 cout
s表明,在仍然调用计时器回调的同时,静态全局对象已被破坏。
// main.cpp
#include <algorithm>
#include <cerrno>
#include <csignal>
#include <cstring>
#include <iostream>
#include <map>
#include <mutex>
#include <string>
#include <unistd.h>
using namespace std;
static int tmp = ((srand ( time( NULL ) )), 0);
class Foo { // Encapsulates a random-sized, random-content string.
public:
Foo() {
uint32_t size = (rand() % 24) + 1;
std::generate_n( std::back_inserter( s_ ), size, randChar );
}
void operator=( const Foo& other ) { s_ = other.s_; }
std::string s_;
private:
static char randChar() { return ('a' + rand() % 26); }
};
class GlobalObj { // Encapsulates a map<timer_t, Foo>.
public:
~GlobalObj() { std::cout << __FUNCTION__ << std::endl; }
Foo* getFoo( const timer_t& timer ) {
Foo* ret = NULL;
{
std::lock_guard<std::mutex> l( mutex_ );
std::map<timer_t, Foo*>::iterator i = map_.find( timer );
if ( map_.end() != i ) {
ret = i->second;
map_.erase( i );
}
}
return ret;
}
void setFoo( const timer_t& timer, Foo* foo ) {
std::lock_guard<std::mutex> l( mutex_ );
map_[timer] = foo;
}
private:
std::mutex mutex_;
std::map<timer_t, Foo*> map_;
};
static GlobalObj global_obj; // static global GlobalObj instance.
void osTimerCallback( union sigval sv ) { // The timer callback
timer_t* timer = (timer_t*)(sv.sival_ptr);
if ( timer ) {
Foo* foo = global_obj.getFoo(*timer);
if ( foo ) {
cout << "timer[" << *timer << "]: " << foo->s_ << endl;
delete foo;
}
delete timer;
}
}
bool createTimer( const struct timespec& when ) { // Creates an armed timer.
timer_t* timer = new timer_t;
struct sigevent se;
static clockid_t clock_id =
#ifdef CLOCK_MONOTONIC
CLOCK_MONOTONIC;
#else
CLOCK_REALTIME;
#endif
memset( &se, 0, sizeof se );
se.sigev_notify = SIGEV_THREAD;
se.sigev_value.sival_ptr = timer;
se.sigev_notify_function = osTimerCallback;
if ( timer_create( clock_id, &se, timer ) ) {
cerr << "timer_create() err " << errno << " " << strerror( errno ) << endl;
return false;
}
{
struct itimerspec its;
memset( &its, 0, sizeof its );
its.it_value.tv_sec = when.tv_sec;
its.it_value.tv_nsec = when.tv_nsec;
if ( timer_settime( *timer, 0, &its, NULL ) ) {
cerr << "timer_settime err " << errno << " " << strerror( errno ) << endl;
return false;
}
global_obj.setFoo( *timer, new Foo );
}
return true;
}
int main( int argc, char* argv[] ) { // Creates many armed timers, then exits
static const struct timespec when = { 2, 0 };
for ( uint32_t i = 0; i < 100; ++i ) {
createTimer( when );
}
usleep( 2000010 );
return 0;
}
错误示例:$ g++ --version && g++ -g ./main.cpp -lrt && ./a.out
g++ (Debian 6.3.0-18+deb9u1) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
timer[timer[~GlobalObj0x55b34c17bd700x55b34c17be60
]: gx
*** Error in `./a.out': double free or corruption (fasttop): 0xtimer[0x55b34c17bf50]: wsngolhdjvhx
]: npscgelwujjfp
Aborted
请注意,错误中提到“双重释放”;上面的代码有两个delete
语句:删除它们似乎并不影响问题的可重复性。由于访问无效的内存,我认为该错误消息是红色鲱鱼。将
usleep()
中的main()
增加到足够大,以允许所有计时器回调调用在静态全局对象破坏导致一致成功执行之前发生。
最佳答案
不,没有什么能阻止计时器在main
结束后触发。
防止这种情况在C++中发生的常见方法是为每种需要手动释放的资源类型创建一个小型资源拥有类。参见RAII和The rule of three/five/zero。
这样的类的基础可能看起来像这样:
#include <cerrno> // errno
#include <cstring> // std::strerror
#include <stdexcept> // std::runtime_error
#include <string> // std::string
#include <utility> // std::exchange
class Timer {
public:
Timer(clockid_t clockid, sigevent& sev) {
if(timer_create(clockid, &sev, &timerid))
throw std::runtime_error(std::string("timer_create: ") +
std::strerror(errno));
}
// rule of 5
Timer(const Timer&) = delete; // no copy construction
Timer(Timer&& rhs) : // move construction ok
timerid(std::exchange(rhs.timerid, nullptr)) {}
Timer& operator=(const Timer&) = delete; // no copy assignment
Timer& operator=(Timer&& rhs) { // move assignment ok
if(this != &rhs) {
if(timerid) timer_delete(timerid);
timerid = std::exchange(rhs.timerid, nullptr);
}
return *this;
}
~Timer() {
if(timerid) timer_delete(timerid);
}
private:
timer_t timerid;
};
现在,您可以将Timer
存储在容器中,并且当容器超出范围时,它们将被正确删除。每当需要处理C API:s中常见的一对
create
/ delete
对中的一种时,采用这种方法通常会限制像您得到的那样的惊喜数量。另请阅读有关Static Initialization Order Fiasco的信息,以避免其他潜在的陷阱。
注意:此实现利用了
timer_t
是我的系统上的指针类型这一事实,我不知道是否总是这样。
关于c++ - 全局静态对象销毁后,是否有可能调用OS计时器回调?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64841725/