9

当表示资源的对象包含在共享指针中时,应如何处理资源释放期间的错误?

编辑1:

用更具体的术语来说明这个问题:许多 C 风格的接口具有分配资源和释放资源的功能。示例是用于 POSIX 系统上的文件描述符的 open(2) 和 close(2),用于连接到 X 服务器的 XOpenDisplay 和 XCloseDisplay,或用于连接到 SQLite 数据库的 sqlite3_open 和 sqlite3_close。

我喜欢将此类接口封装在 C++ 类中,使用 Pimpl 习惯用法隐藏实现细节,并提供返回共享指针的工厂方法,以确保在没有对资源的引用时释放资源。

但是,在上面给出的所有示例和许多其他示例中,用于释放资源的函数可能会报告错误。如果这个函数被析构函数调用,我不能抛出异常,因为通常析构函数不能抛出。

另一方面,如果我提供了一个公共方法来释放资源,那么我现在有一个具有两种可能状态的类:一种是资源有效,另一种是资源已被释放。这不仅使类的实现复杂化,而且还存在错误使用的可能性。这很糟糕,因为接口应该旨在使使用错误成为不可能。

对于这个问题的任何帮助,我将不胜感激。

问题的原始陈述以及关于可能解决方案的想法如下。

编辑2:

现在有一个赏金这个问题。解决方案必须满足以下要求:

  1. 当且仅当没有对它的引用时才释放该资源。
  2. 对资源的引用可能会被显式销毁。如果在释放资源时发生错误,则会引发异常。
  3. 不能使用已经释放的资源。
  4. 资源的引用计数和释放是线程安全的

解决方案满足以下要求:

  1. 它使用boost提供的共享指针、C++ Technical Report 1 (TR1)和即将推出的 C++ 标准C++0x
  2. 它是通用的。资源类只需要实现资源的释放方式即可。

感谢您的时间和想法。

编辑 3:

感谢所有回答我问题的人。

Alsk 的回答满足了赏金中的所有要求,并被接受。在多线程代码中,此解决方案将需要一个单独的清理线程。

我添加了另一个答案,其中清理期间的任何异常都由实际使用资源的线程引发,而无需单独的清理线程。如果您仍然对这个问题感兴趣(它困扰了我很多),请发表评论。

智能指针是安全管理资源的有用工具。此类资源的示例是内存、磁盘文件、数据库连接或网络连接。

// open a connection to the local HTTP port
boost::shared_ptr<Socket> socket = Socket::connect("localhost:80");

在典型的场景中,封装资源的类应该是不可复制的和多态的。支持这一点的一个好方法是提供一个返回共享指针的工厂方法,并将所有构造函数声明为非公共的。共享指针现在可以自由复制和分配。当没有对它的引用时,该对象将自动销毁,然后析构函数释放资源。

/** A TCP/IP connection. */
class Socket
{
public:
    static boost::shared_ptr<Socket> connect(const std::string& address);
    virtual ~Socket();
protected:
    Socket(const std::string& address);
private:
    // not implemented
    Socket(const Socket&);
    Socket& operator=(const Socket&);
};

但是这种方法存在一个问题。析构函数不能抛出,因此释放资源的失败将不会被检测到。

解决这个问题的一个常见方法是添加一个公共方法来释放资源。

class Socket
{
public:
    virtual void close(); // may throw
    // ...
};

不幸的是,这种方法引入了另一个问题:我们的对象现在可能包含已经释放的资源。这使资源类的实现复杂化。更糟糕的是,它使该类的客户可能错误地使用它。以下示例可能看起来有些牵强,但它是多线程代码中的常见缺陷。

socket->close();
// ...
size_t nread = socket->read(&buffer[0], buffer.size()); // wrong use!

要么我们确保在对象被销毁之前不释放资源,从而失去任何处理失败资源释放的方法。或者我们提供了一种在对象生命周期内显式释放资源的方法,从而可以错误地使用资源类。

有办法摆脱这种困境。但解决方案涉及使用修改后的共享指针类。这些修改可能会引起争议。

典型的共享指针实现,例如 boost::shared_ptr,要求在调用其对象的析构函数时不抛出异常。通常,任何析构函数都不应该抛出,所以这是一个合理的要求。这些实现还允许指定自定义删除函数,当没有对对象的引用时调用该函数以代替析构函数。不抛出要求扩展到此自定义删除器功能。

这个要求的基本原理很清楚:共享指针的析构函数不能抛出。如果删除函数不抛出,共享指针的析构函数也不会抛出。然而,对于导致资源释放的共享指针的其他成员函数也是如此,例如reset():如果资源释放失败,则不会抛出异常。

这里提出的解决方案是允许自定义删除函数抛出。这意味着修改后的共享指针的析构函数必须捕获删除函数抛出的异常。另一方面,除析构函数之外的成员函数,例如reset(),不应捕获删除函数的异常(并且它们的实现变得更加复杂)。

这是原始示例,使用抛出删除器函数:

/** A TCP/IP connection. */
class Socket
{
public:
    static SharedPtr<Socket> connect(const std::string& address);
protected:
    Socket(const std::string& address);
    virtual Socket() { }
private:
    struct Deleter;

    // not implemented
    Socket(const Socket&);
    Socket& operator=(const Socket&);
};

struct Socket::Deleter
{
    void operator()(Socket* socket)
    {
        // Close the connection. If an error occurs, delete the socket
        // and throw an exception.

        delete socket;
    }
};

SharedPtr<Socket> Socket::connect(const std::string& address)
{
    return SharedPtr<Socket>(new Socket(address), Deleter());
}

我们现在可以使用 reset() 显式释放资源。如果在另一个线程或程序的另一部分中仍然存在对该资源的引用,则调用 reset() 只会减少引用计数。如果这是对该资源的最后一次引用,则释放该资源。如果资源释放失败,则会引发异常。

SharedPtr<Socket> socket = Socket::connect("localhost:80");
// ...
socket.reset();

编辑:

这是删除器的完整(但依赖于平台)实现:

struct Socket::Deleter
{
    void operator()(Socket* socket)
    {
        if (close(socket->m_impl.fd) < 0)
        {
            int error = errno;
            delete socket;
            throw Exception::fromErrno(error);
        }

        delete socket;
     }
};
4

6 回答 6

4

如果释放某些资源实际上会失败,那么析构函数显然是一个错误的抽象使用。无论情况如何,析构函数都旨在清理干净。一种close()方法(或任何你想命名的方法)可能是唯一的方法。

但是仔细想想。如果释放资源实际上失败了,你能做什么?这样的错误可以恢复吗?如果是,您的代码的哪一部分应该处理它?恢复的方式可能是高度特定于应用程序的,并且与应用程序的其他部分相关联。您实际上不太可能希望这种情况自动发生,在代码中发生释放资源并触发错误的任意位置。共享指针抽象并不能真正模拟您要实现的目标。如果是这样,那么您显然需要创建自己的抽象来模拟您请求的行为。滥用共享指针做他们不应该做的事情不是正确的方法。

另外,请阅读

编辑:
如果您只想通知用户在崩溃之前发生了什么,那么考虑将其包装Socket另一个包装器对象中,该包装器对象会在销毁时调用删除器,捕获任何抛出的异常并通过向用户显示消息框来处理它们或任何。然后把这个包装对象放在一个boost::shared_ptr.

于 2010-05-16T20:59:09.483 回答
4

我们需要将分配的资源存储在某个地方(正如DeadMG已经提到的那样),并在任何析构函数之外显式调用一些报告/抛出函数。但这并不妨碍我们利用 boost::shared_ptr 中实现的引用计数。

/** A TCP/IP connection. */
class Socket
{
private:
    //store internally every allocated resource here
    static std::vector<boost::shared_ptr<Socket> > pool;
public:
    static boost::shared_ptr<Socket> connect(const std::string& address)
    {
         //...
         boost::shared_ptr<Socket> socket(new Socket(address));
         pool.push_back(socket); //the socket won't be actually 
                                 //destroyed until we want it to
         return socket;
    }
    virtual ~Socket();

    //call cleanupAndReport() as often as needed
    //probably, on a separate thread, or by timer 
    static void cleanupAndReport()
    {
        //find resources without clients
        foreach(boost::shared_ptr<Socket>& socket, pool)
        {
            if(socket.unique()) //there are no clients for this socket, i.e. 
                  //there are no shared_ptr's elsewhere pointing to this socket
            {
                 //try to deallocate this resource
                 if (close(socket->m_impl.fd) < 0)
                 {
                     int error = errno;
                     socket.reset(); //destroys Socket object
                     //throw an exception or handle error in-place
                     //... 
                     //throw Exception::fromErrno(error);
                 }
                 else
                 {
                     socket.reset();
                 } 
            } 
        } //foreach socket
    }
protected:
    Socket(const std::string& address);
private:
    // not implemented
    Socket(const Socket&);
    Socket& operator=(const Socket&);
};

cleanupAndReport() 的实现应该稍微复杂一点:在当前版本中,池在清理后填充了空指针,如果抛出异常,我们必须调用该函数,直到它不再抛出等等,但我希望,它很好地说明了这个想法。

现在,更通用的解决方案:

//forward declarations
template<class Resource>
boost::shared_ptr<Resource> make_shared_resource();
template<class Resource>
void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource> deallocator);

//for every type of used resource there will be a template instance with a static pool
template<class Resource>
class pool_holder
{
private:
        friend boost::shared_ptr<Resource> make_shared_resource<Resource>();
        friend void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource>);
        static std::vector<boost::shared_ptr<Resource> > pool;
};
template<class Resource>
std::vector<boost::shared_ptr<Resource> > pool_holder<Resource>::pool;

template<class Resource>
boost::shared_ptr<Resource> make_shared_resource()
{
        boost::shared_ptr<Resource> res(new Resource);
        pool_holder<Resource>::pool.push_back(res);
        return res;
}
template<class Resource>
void cleanupAndReport(boost::function1<void,boost::shared_ptr<Resource> > deallocator)
{
    foreach(boost::shared_ptr<Resource>& res, pool_holder<Resource>::pool)
    {
        if(res.unique()) 
        {
             deallocator(res);
        }
    } //foreach
}
//usage
        {
           boost::shared_ptr<A> a = make_shared_resource<A>();
           boost::shared_ptr<A> a2 = make_shared_resource<A>();
           boost::shared_ptr<B> b = make_shared_resource<B>();
           //...
        }
        cleanupAndReport<A>(deallocate_A);
        cleanupAndReport<B>(deallocate_B);
于 2010-05-24T21:29:27.367 回答
1

如问题中所述,编辑3:

这是另一种解决方案,据我所知,它满足问题中的要求。它类似于原始问题中描述的解决方案,但使用boost::shared_ptr而不是自定义智能指针。

该解决方案的中心思想是提供release()shared_ptr. 如果我们可以让shared_ptr对象放弃它的所有权,我们可以自由调用清理函数,删除对象,并在清理过程中发生错误时抛出异常。

Boost 有充分的理由 不提供对 的release()操作shared_ptr

shared_ptr 不能放弃所有权,除非它是 unique() 因为另一个副本仍然会破坏该对象。

考虑:

shared_ptr<int> a(new int);
shared_ptr<int> b(a); // a.use_count() == b.use_count() == 2

int * p = a.release();

// Who owns p now? b will still call delete on it in its destructor.

此外,release() 返回的指针很难可靠地释放,因为源 shared_ptr 可能是使用自定义删除器创建的。

反对release()操作的第一个论点是,根据 的性质shared_ptr,许多指针共享对象的所有权,因此它们中的任何一个都不能简单地释放该所有权。但是,如果release()还有其他引用,函数返回一个空指针怎么办?可以可靠地确定这shared_ptr一点,而无需竞争条件。

反对该release()操作的第二个论点是,如果将自定义删除器传递给shared_ptr,您应该使用它来释放对象,而不是简单地删除它。但是release() 除了原始指针之外,还可以返回一个函数对象,以使其调用者能够可靠地释放指针。

但是,在我们的特定场景中,自定义删除器不会成为问题,因为我们不必处理任意自定义删除器。这将从下面给出的代码中变得更清楚。

当然,如果没有 hack,就不可能在不修改其实现的情况下提供release()操作。shared_ptr下面代码中使用的 hack 依赖于线程局部变量来防止我们的自定义删除器实际删除对象。

也就是说,这是代码,主要由 header Resource.hpp和一个小的 implementation file组成Resource.cpp-lboost_thread-mt请注意,由于线程局部变量,它必须链接。

// ---------------------------------------------------------------------
// Resource.hpp
// ---------------------------------------------------------------------

#include <boost/assert.hpp>
#include <boost/ref.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread/tss.hpp>


/// Factory for a resource.
template<typename T>
struct ResourceFactory
{
    /// Create a resource.
    static boost::shared_ptr<T>
    create()
    {
        return boost::shared_ptr<T>(new T, ResourceFactory());
    }

    template<typename A1>
    static boost::shared_ptr<T>
    create(const A1& a1)
    {
        return boost::shared_ptr<T>(new T(a1), ResourceFactory());
    }

    template<typename A1, typename A2>
    static boost::shared_ptr<T>
    create(const A1& a1, const A2& a2)
    {
        return boost::shared_ptr<T>(new T(a1, a2), ResourceFactory());
    }

    // ...

    /// Destroy a resource.
    static void destroy(boost::shared_ptr<T>& resource);

    /// Deleter for boost::shared_ptr<T>.
    void operator()(T* resource);
};


namespace impl
{

// ---------------------------------------------------------------------

/// Return the last reference to the resource, or zero. Resets the pointer.
template<typename T>
T* release(boost::shared_ptr<T>& resource);

/// Return true if the resource should be deleted (thread-local).
bool wantDelete();

// ---------------------------------------------------------------------

} // namespace impl


template<typename T>
inline
void ResourceFactory<T>::destroy(boost::shared_ptr<T>& ptr)
{
    T* resource = impl::release(ptr);

    if (resource != 0) // Is it the last reference?
    {
        try
        {
            resource->close();
        }
        catch (...)
        {
            delete resource;

            throw;
        }

        delete resource;
    }
}

// ---------------------------------------------------------------------

template<typename T>
inline
void ResourceFactory<T>::operator()(T* resource)
{
    if (impl::wantDelete())
    {
        try
        {
            resource->close();
        }
        catch (...)
        {
        }

        delete resource;
    }
}


namespace impl
{

// ---------------------------------------------------------------------

/// Flag in thread-local storage.
class Flag
{
public:
    ~Flag()
    {
        m_ptr.release();
    }

    Flag& operator=(bool value)
    {
        if (value != static_cast<bool>(*this))
        {
            if (value)
            {
                m_ptr.reset(s_true); // may throw boost::thread_resource_error!
            }
            else
            {
                m_ptr.release();
            }
        }

        return *this;
    }

    operator bool()
    {
        return m_ptr.get() == s_true;
    }

private:
    boost::thread_specific_ptr<char> m_ptr;

    static char* s_true;
};

// ---------------------------------------------------------------------

/// Flag to prevent deletion.
extern Flag t_nodelete;

// ---------------------------------------------------------------------

/// Return the last reference to the resource, or zero.
template<typename T>
T* release(boost::shared_ptr<T>& resource)
{
    try
    {
        BOOST_ASSERT(!t_nodelete);

        t_nodelete = true; // may throw boost::thread_resource_error!
    }
    catch (...)
    {
        t_nodelete = false;

        resource.reset();

        throw;
    }

    T* rv = resource.get();

    resource.reset();

    return wantDelete() ? rv : 0;
}

// ---------------------------------------------------------------------

} // namespace impl

以及实现文件:

// ---------------------------------------------------------------------
// Resource.cpp
// ---------------------------------------------------------------------

#include "Resource.hpp"


namespace impl
{

// ---------------------------------------------------------------------

bool wantDelete()
{
    bool rv = !t_nodelete;

    t_nodelete = false;

    return rv;
}

// ---------------------------------------------------------------------

Flag t_nodelete;

// ---------------------------------------------------------------------

char* Flag::s_true((char*)0x1);

// ---------------------------------------------------------------------

} // namespace impl

这是使用此解决方案实现的资源类的示例:

// ---------------------------------------------------------------------
// example.cpp
// ---------------------------------------------------------------------
#include "Resource.hpp"

#include <cstdlib>
#include <string>
#include <stdexcept>
#include <iostream>


// uncomment to test failed resource allocation, usage, and deallocation

//#define TEST_CREAT_FAILURE
//#define TEST_USAGE_FAILURE
//#define TEST_CLOSE_FAILURE

// ---------------------------------------------------------------------

/// The low-level resource type.
struct foo { char c; };

// ---------------------------------------------------------------------

/// The low-level function to allocate the resource.
foo* foo_open()
{
#ifdef TEST_CREAT_FAILURE
    return 0;
#else
    return (foo*) std::malloc(sizeof(foo));
#endif
}

// ---------------------------------------------------------------------

/// Some low-level function using the resource.
int foo_use(foo*)
{
#ifdef TEST_USAGE_FAILURE
    return -1;
#else
    return 0;
#endif
}

// ---------------------------------------------------------------------

/// The low-level function to free the resource.
int foo_close(foo* foo)
{
    std::free(foo);
#ifdef TEST_CLOSE_FAILURE
    return -1;
#else
    return 0;
#endif
}

// ---------------------------------------------------------------------

/// The C++ wrapper around the low-level resource.
class Foo
{
public:
    void use()
    {
        if (foo_use(m_foo) < 0)
        {
            throw std::runtime_error("foo_use");
        }
    }

protected:
    Foo()
        : m_foo(foo_open())
    {
        if (m_foo == 0)
        {
            throw std::runtime_error("foo_open");
        }
    }

    void close()
    {
        if (foo_close(m_foo) < 0)
        {
            throw std::runtime_error("foo_close");
        }
    }

private:
    foo* m_foo;

    friend struct ResourceFactory<Foo>;
};

// ---------------------------------------------------------------------

typedef ResourceFactory<Foo> FooFactory;

// ---------------------------------------------------------------------

/// Main function.
int main()
{
    try
    {
        boost::shared_ptr<Foo> resource = FooFactory::create();

        resource->use();

        FooFactory::destroy(resource);
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

最后,这是一个小的 Makefile 来构建所有这些:

# Makefile

CXXFLAGS = -g -Wall

example: example.cpp Resource.hpp Resource.o
    $(CXX) $(CXXFLAGS) -o example example.cpp Resource.o -lboost_thread-mt

Resource.o: Resource.cpp Resource.hpp
    $(CXX) $(CXXFLAGS) -c Resource.cpp -o Resource.o

clean:
    rm -f Resource.o example
于 2010-06-16T23:26:08.670 回答
1

引用 Herb Sutter,“Exceptional C++”的作者(来自这里):

如果析构函数抛出异常,就会发生坏事。具体来说,考虑如下代码:

//  The problem
//
class X {
public:
  ~X() { throw 1; }
};

void f() {
  X x;
  throw 2;
} // calls X::~X (which throws), then calls terminate()

如果析构函数抛出异常而另一个异常已经处于活动状态(即,在堆栈展开期间),则程序将终止。这通常不是一件好事。

换句话说,无论在这种情况下你想相信什么是优雅的,你都不能轻率地在析构函数中抛出异常,除非你能保证在处理另一个异常时它不会被抛出。

此外,如果您无法成功摆脱资源,您该怎么办?应该为可以更高层处理的事情抛出异常,而不是错误。如果您想报告奇怪的行为,请记录发布失败并继续。或终止。

于 2010-05-24T16:57:22.787 回答
0

好吧,首先,我在这里没有看到问题。其次,我不得不说这是一个坏主意。你会在这一切中获得什么?当指向资源的最后一个共享指针被销毁并且你的抛出删除器被调用时,你会发现自己有资源泄漏。您将丢失未能释放的资源的所有句柄。你将永远无法再试一次。

您希望使用 RAII 对象是一件好事,但智能指针根本不足以完成任务。你需要的东西需要更聪明。你需要一些能够在无法完全崩溃时重建自身的东西。对于这样的接口,析构函数是不够的。

您确实向自己介绍了某人可能导致资源具有句柄但无效的滥用。您在这里处理的资源类型很适合这个问题。有很多方法可以解决这个问题。一种方法可能是使用句柄/正文习语以及状态模式。接口的实现可以处于以下两种状态之一:已连接或未连接。句柄只是将请求传递给内部主体/状态。已连接的工作方式与正常的一样,未连接的在所有适用的请求中抛出异常/断言。

这个东西需要一个除 ~ 以外的函数来销毁它的句柄。您可以考虑一个可以抛出的 destroy() 函数。如果您在调用它时发现错误,您不会删除句柄,而是以您需要的任何特定于应用程序的方式处理问题。如果您没有从 destroy() 中捕获错误,则让句柄超出范围,重置它,或其他任何方式。然后函数destroy() 对资源计数进行递减,如果该计数为0,则尝试释放内部资源。成功后,句柄切换到未连接状态,失败时会生成一个可捕获的错误,客户端可以尝试处理但离开手柄处于连接状态。

写起来并不是一件完全微不足道的事情,但是您想要做的事情,将异常引入破坏,根本行不通。

于 2010-05-16T20:04:59.063 回答
0

一般来说,如果一个资源的 C 风格的闭包失败,那么它是 API 的问题,而不是你的代码的问题。但是,我想做的是,如果销毁失败,请将其添加到需要稍后重新尝试销毁/清理的资源列表中,例如,当应用程序退出时、定期退出或其他类似资源被销毁时,以及然后尝试重新销毁。如果在任意时间留下任何东西,请给用户错误并退出。

于 2010-05-21T20:03:45.347 回答