提交 9340af1a 编写于 作者: A Alexander Alekhin

core: Async API / AsyncArray

上级 447116a9
......@@ -68,6 +68,7 @@
@defgroup core_c_glue Connections with C++
@}
@defgroup core_array Operations on arrays
@defgroup core_async Asynchronous API
@defgroup core_xml XML/YAML Persistence
@defgroup core_cluster Clustering
@defgroup core_utils Utility and system functions and macros
......
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#ifndef OPENCV_CORE_ASYNC_HPP
#define OPENCV_CORE_ASYNC_HPP
#include <opencv2/core/mat.hpp>
#ifdef CV_CXX11
//#include <future>
#include <chrono>
#endif
namespace cv {
/** @addtogroup core_async
@{
*/
/** @brief Returns result of asynchronous operations
Object has attached asynchronous state.
Assignment operator doesn't clone asynchronous state (it is shared between all instances).
Result can be fetched via get() method only once.
*/
class CV_EXPORTS_W AsyncArray
{
public:
~AsyncArray() CV_NOEXCEPT;
CV_WRAP AsyncArray() CV_NOEXCEPT;
AsyncArray(const AsyncArray& o) CV_NOEXCEPT;
AsyncArray& operator=(const AsyncArray& o) CV_NOEXCEPT;
CV_WRAP void release() CV_NOEXCEPT;
/** Fetch the result.
@param[out] dst destination array
Waits for result until container has valid result.
Throws exception if exception was stored as a result.
Throws exception on invalid container state.
@note Result or stored exception can be fetched only once.
*/
CV_WRAP void get(OutputArray dst) const;
/** Retrieving the result with timeout
@param[out] dst destination array
@param[in] timeoutNs timeout in nanoseconds, -1 for infinite wait
@returns true if result is ready, false if the timeout has expired
@note Result or stored exception can be fetched only once.
*/
bool get(OutputArray dst, int64 timeoutNs) const;
CV_WRAP inline
bool get(OutputArray dst, double timeoutNs) const { return get(dst, (int64)timeoutNs); }
bool wait_for(int64 timeoutNs) const;
CV_WRAP inline
bool wait_for(double timeoutNs) const { return wait_for((int64)timeoutNs); }
CV_WRAP bool valid() const CV_NOEXCEPT;
#ifdef CV_CXX11
inline AsyncArray(AsyncArray&& o) { p = o.p; o.p = NULL; }
inline AsyncArray& operator=(AsyncArray&& o) CV_NOEXCEPT { std::swap(p, o.p); return *this; }
template<typename _Rep, typename _Period>
inline bool get(OutputArray dst, const std::chrono::duration<_Rep, _Period>& timeout)
{
return get(dst, (int64)(std::chrono::nanoseconds(timeout).count()));
}
template<typename _Rep, typename _Period>
inline bool wait_for(const std::chrono::duration<_Rep, _Period>& timeout)
{
return wait_for((int64)(std::chrono::nanoseconds(timeout).count()));
}
#if 0
std::future<Mat> getFutureMat() const;
std::future<UMat> getFutureUMat() const;
#endif
#endif
// PImpl
struct Impl; friend struct Impl;
inline void* _getImpl() const CV_NOEXCEPT { return p; }
protected:
Impl* p;
};
//! @}
} // namespace
#endif // OPENCV_CORE_ASYNC_HPP
......@@ -5,6 +5,9 @@
#ifndef OPENCV_CORE_BINDINGS_UTILS_HPP
#define OPENCV_CORE_BINDINGS_UTILS_HPP
#include <opencv2/core/async.hpp>
#include <opencv2/core/detail/async_promise.hpp>
namespace cv { namespace utils {
//! @addtogroup core_utils
//! @{
......@@ -17,6 +20,29 @@ CV_EXPORTS_W String dumpInputOutputArray(InputOutputArray argument);
CV_EXPORTS_W String dumpInputOutputArrayOfArrays(InputOutputArrayOfArrays argument);
CV_WRAP static inline
AsyncArray testAsyncArray(InputArray argument)
{
AsyncPromise p;
p.setValue(argument);
return p.getArrayResult();
}
CV_WRAP static inline
AsyncArray testAsyncException()
{
AsyncPromise p;
try
{
CV_Error(Error::StsOk, "Test: Generated async error");
}
catch (const cv::Exception& e)
{
p.setException(e);
}
return p.getArrayResult();
}
//! @}
}} // namespace
......
......@@ -622,6 +622,19 @@ Cv64suf;
# define CV_FINAL
#endif
/****************************************************************************************\
* C++11 noexcept *
\****************************************************************************************/
#ifndef CV_NOEXCEPT
# ifdef CV_CXX11
# define CV_NOEXCEPT noexcept
# endif
#endif
#ifndef CV_NOEXCEPT
# define CV_NOEXCEPT
#endif
// Integer types portatibility
......
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#ifndef OPENCV_CORE_ASYNC_PROMISE_HPP
#define OPENCV_CORE_ASYNC_PROMISE_HPP
#include "../async.hpp"
#include "exception_ptr.hpp"
namespace cv {
/** @addtogroup core_async
@{
*/
/** @brief Provides result of asynchronous operations
*/
class CV_EXPORTS AsyncPromise
{
public:
~AsyncPromise() CV_NOEXCEPT;
AsyncPromise() CV_NOEXCEPT;
explicit AsyncPromise(const AsyncPromise& o) CV_NOEXCEPT;
AsyncPromise& operator=(const AsyncPromise& o) CV_NOEXCEPT;
void release() CV_NOEXCEPT;
/** Returns associated AsyncArray
@note Can be called once
*/
AsyncArray getArrayResult();
/** Stores asynchronous result.
@param[in] value result
*/
void setValue(InputArray value);
// TODO "move" setters
#if CV__EXCEPTION_PTR
/** Stores exception.
@param[in] exception exception to be raised in AsyncArray
*/
void setException(std::exception_ptr exception);
#endif
/** Stores exception.
@param[in] exception exception to be raised in AsyncArray
*/
void setException(const cv::Exception& exception);
#ifdef CV_CXX11
explicit AsyncPromise(AsyncPromise&& o) { p = o.p; o.p = NULL; }
AsyncPromise& operator=(AsyncPromise&& o) CV_NOEXCEPT { std::swap(p, o.p); return *this; }
#endif
// PImpl
typedef struct AsyncArray::Impl Impl; friend struct AsyncArray::Impl;
inline void* _getImpl() const CV_NOEXCEPT { return p; }
protected:
Impl* p;
};
//! @}
} // namespace
#endif // OPENCV_CORE_ASYNC_PROMISE_HPP
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#ifndef OPENCV_CORE_DETAILS_EXCEPTION_PTR_H
#define OPENCV_CORE_DETAILS_EXCEPTION_PTR_H
#ifndef CV__EXCEPTION_PTR
# if defined(__ANDROID__) && defined(ATOMIC_INT_LOCK_FREE) && ATOMIC_INT_LOCK_FREE < 2
# define CV__EXCEPTION_PTR 0 // Not supported, details: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=58938
# elif defined(CV_CXX11)
# define CV__EXCEPTION_PTR 1
# elif defined(_MSC_VER)
# define CV__EXCEPTION_PTR (_MSC_VER >= 1600)
# elif defined(__clang__)
# define CV__EXCEPTION_PTR 0 // C++11 only (see above)
# elif defined(__GNUC__) && defined(__GXX_EXPERIMENTAL_CXX0X__)
# define CV__EXCEPTION_PTR (__GXX_EXPERIMENTAL_CXX0X__ > 0)
# endif
#endif
#ifndef CV__EXCEPTION_PTR
# define CV__EXCEPTION_PTR 0
#elif CV__EXCEPTION_PTR
# include <exception> // std::exception_ptr
#endif
#endif // OPENCV_CORE_DETAILS_EXCEPTION_PTR_H
......@@ -377,6 +377,9 @@ public:
void assign(const std::vector<UMat>& v) const;
void assign(const std::vector<Mat>& v) const;
void move(UMat& u) const;
void move(Mat& m) const;
};
......
#ifdef HAVE_OPENCV_CORE
#include "opencv2/core/async.hpp"
CV_PY_TO_CLASS(AsyncArray);
CV_PY_FROM_CLASS(AsyncArray);
#endif
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#include "precomp.hpp"
//#undef CV_CXX11 // debug non C++11 mode
#include "opencv2/core/async.hpp"
#include "opencv2/core/detail/async_promise.hpp"
#include "opencv2/core/cvstd.hpp"
#include <opencv2/core/utils/logger.defines.hpp>
#undef CV_LOG_STRIP_LEVEL
#define CV_LOG_STRIP_LEVEL CV_LOG_LEVEL_DEBUG + 1
#include <opencv2/core/utils/logger.hpp>
#ifdef CV_CXX11
#include <mutex>
#include <condition_variable>
#include <chrono>
#endif
namespace cv {
/**
Manages shared state of asynchronous result
*/
struct AsyncArray::Impl
{
int refcount;
void addrefFuture() CV_NOEXCEPT { CV_XADD(&refcount_future, 1); CV_XADD(&refcount, 1); } \
void releaseFuture() CV_NOEXCEPT { CV_XADD(&refcount_future, -1); if(1 == CV_XADD(&refcount, -1)) delete this; } \
int refcount_future;
void addrefPromise() CV_NOEXCEPT { CV_XADD(&refcount_promise, 1); CV_XADD(&refcount, 1); } \
void releasePromise() CV_NOEXCEPT { CV_XADD(&refcount_promise, -1); if(1 == CV_XADD(&refcount, -1)) delete this; } \
int refcount_promise;
#ifdef CV_CXX11
mutable std::mutex mtx;
mutable std::condition_variable cond_var;
#else
mutable cv::Mutex mtx;
#endif
mutable bool has_result; // Mat, UMat or exception
mutable cv::Ptr<Mat> result_mat;
mutable cv::Ptr<UMat> result_umat;
bool has_exception;
#if CV__EXCEPTION_PTR
std::exception_ptr exception;
#endif
cv::Exception cv_exception;
mutable bool result_is_fetched;
bool future_is_returned;
Impl()
: refcount(1), refcount_future(0), refcount_promise(1)
, has_result(false)
, has_exception(false)
, result_is_fetched(false)
, future_is_returned(false)
{
// nothing
}
~Impl()
{
if (has_result && !result_is_fetched)
{
CV_LOG_INFO(NULL, "Asynchronous result has not been fetched");
}
}
bool get(OutputArray dst, int64 timeoutNs) const
{
CV_Assert(!result_is_fetched);
if (!has_result)
{
if(refcount_promise == 0)
CV_Error(Error::StsInternal, "Asynchronous result producer has been destroyed");
if (!wait_for(timeoutNs))
return false;
}
#ifdef CV_CXX11
std::unique_lock<std::mutex> lock(mtx);
#else
cv::AutoLock lock(mtx);
#endif
if (has_result)
{
if (!result_mat.empty())
{
dst.move(*result_mat.get());
result_mat.release();
result_is_fetched = true;
return true;
}
if (!result_umat.empty())
{
dst.move(*result_umat.get());
result_umat.release();
result_is_fetched = true;
return true;
}
#if CV__EXCEPTION_PTR
if (has_exception && exception)
{
result_is_fetched = true;
std::rethrow_exception(exception);
}
#endif
if (has_exception)
{
result_is_fetched = true;
throw cv_exception;
}
CV_Error(Error::StsInternal, "AsyncArray: invalid state of 'has_result = true'");
}
CV_Assert(!has_result);
CV_Assert(timeoutNs < 0);
return false;
}
bool valid() const CV_NOEXCEPT
{
if (result_is_fetched)
return false;
if (refcount_promise == 0 && !has_result)
return false;
return true;
}
bool wait_for(int64 timeoutNs) const
{
CV_Assert(valid());
if (has_result)
return has_result;
if (timeoutNs == 0)
return has_result;
CV_LOG_INFO(NULL, "Waiting for async result ...");
#ifdef CV_CXX11
std::unique_lock<std::mutex> lock(mtx);
const auto cond_pred = [&]{ return has_result == true; };
if (timeoutNs > 0)
return cond_var.wait_for(lock, std::chrono::nanoseconds(timeoutNs), cond_pred);
else
{
cond_var.wait(lock, cond_pred);
CV_Assert(has_result);
return true;
}
#else
CV_Error(Error::StsNotImplemented, "OpenCV has been built without async waiting support (C++11 is required)");
#endif
}
AsyncArray getArrayResult()
{
CV_Assert(refcount_future == 0);
AsyncArray result;
addrefFuture();
result.p = this;
future_is_returned = true;
return result;
}
void setValue(InputArray value)
{
if (future_is_returned && refcount_future == 0)
CV_Error(Error::StsError, "Associated AsyncArray has been destroyed");
#ifdef CV_CXX11
std::unique_lock<std::mutex> lock(mtx);
#else
cv::AutoLock lock(mtx);
#endif
CV_Assert(!has_result);
int k = value.kind();
if (k == _InputArray::UMAT)
{
result_umat = makePtr<UMat>();
value.copyTo(*result_umat.get());
}
else
{
result_mat = makePtr<Mat>();
value.copyTo(*result_mat.get());
}
has_result = true;
#ifdef CV_CXX11
cond_var.notify_all();
#endif
}
#if CV__EXCEPTION_PTR
void setException(std::exception_ptr e)
{
if (future_is_returned && refcount_future == 0)
CV_Error(Error::StsError, "Associated AsyncArray has been destroyed");
#ifdef CV_CXX11
std::unique_lock<std::mutex> lock(mtx);
#else
cv::AutoLock lock(mtx);
#endif
CV_Assert(!has_result);
has_exception = true;
exception = e;
has_result = true;
#ifdef CV_CXX11
cond_var.notify_all();
#endif
}
#endif
void setException(const cv::Exception e)
{
if (future_is_returned && refcount_future == 0)
CV_Error(Error::StsError, "Associated AsyncArray has been destroyed");
#ifdef CV_CXX11
std::unique_lock<std::mutex> lock(mtx);
#else
cv::AutoLock lock(mtx);
#endif
CV_Assert(!has_result);
has_exception = true;
cv_exception = e;
has_result = true;
#ifdef CV_CXX11
cond_var.notify_all();
#endif
}
};
AsyncArray::AsyncArray() CV_NOEXCEPT
: p(NULL)
{
}
AsyncArray::~AsyncArray() CV_NOEXCEPT
{
release();
}
AsyncArray::AsyncArray(const AsyncArray& o) CV_NOEXCEPT
: p(o.p)
{
if (p)
p->addrefFuture();
}
AsyncArray& AsyncArray::operator=(const AsyncArray& o) CV_NOEXCEPT
{
Impl* newp = o.p;
if (newp)
newp->addrefFuture();
release();
p = newp;
return *this;
}
void AsyncArray::release() CV_NOEXCEPT
{
Impl* impl = p;
p = NULL;
if (impl)
impl->releaseFuture();
}
bool AsyncArray::get(OutputArray dst, int64 timeoutNs) const
{
CV_Assert(p);
return p->get(dst, timeoutNs);
}
void AsyncArray::get(OutputArray dst) const
{
CV_Assert(p);
bool res = p->get(dst, -1);
CV_Assert(res);
}
bool AsyncArray::wait_for(int64 timeoutNs) const
{
CV_Assert(p);
return p->wait_for(timeoutNs);
}
bool AsyncArray::valid() const CV_NOEXCEPT
{
if (!p) return false;
return p->valid();
}
//
// AsyncPromise
//
AsyncPromise::AsyncPromise() CV_NOEXCEPT
: p(new AsyncArray::Impl())
{
}
AsyncPromise::~AsyncPromise() CV_NOEXCEPT
{
release();
}
AsyncPromise::AsyncPromise(const AsyncPromise& o) CV_NOEXCEPT
: p(o.p)
{
if (p)
p->addrefPromise();
}
AsyncPromise& AsyncPromise::operator=(const AsyncPromise& o) CV_NOEXCEPT
{
Impl* newp = o.p;
if (newp)
newp->addrefPromise();
release();
p = newp;
return *this;
}
void AsyncPromise::release() CV_NOEXCEPT
{
Impl* impl = p;
p = NULL;
if (impl)
impl->releasePromise();
}
AsyncArray AsyncPromise::getArrayResult()
{
CV_Assert(p);
return p->getArrayResult();
}
void AsyncPromise::setValue(InputArray value)
{
CV_Assert(p);
return p->setValue(value);
}
void AsyncPromise::setException(const cv::Exception& exception)
{
CV_Assert(p);
return p->setException(exception);
}
#if CV__EXCEPTION_PTR
void AsyncPromise::setException(std::exception_ptr exception)
{
CV_Assert(p);
return p->setException(exception);
}
#endif
} // namespace
......@@ -1879,6 +1879,76 @@ void _OutputArray::assign(const Mat& m) const
}
void _OutputArray::move(UMat& u) const
{
if (fixedSize())
{
// TODO Performance warning
assign(u);
return;
}
int k = kind();
if (k == UMAT)
{
#ifdef CV_CXX11
*(UMat*)obj = std::move(u);
#else
*(UMat*)obj = u;
u.release();
#endif
}
else if (k == MAT)
{
u.copyTo(*(Mat*)obj); // TODO check u.getMat()
u.release();
}
else if (k == MATX)
{
u.copyTo(getMat()); // TODO check u.getMat()
u.release();
}
else
{
CV_Error(Error::StsNotImplemented, "");
}
}
void _OutputArray::move(Mat& m) const
{
if (fixedSize())
{
// TODO Performance warning
assign(m);
return;
}
int k = kind();
if (k == UMAT)
{
m.copyTo(*(UMat*)obj); // TODO check m.getUMat()
m.release();
}
else if (k == MAT)
{
#ifdef CV_CXX11
*(Mat*)obj = std::move(m);
#else
*(Mat*)obj = m;
m.release();
#endif
}
else if (k == MATX)
{
m.copyTo(getMat());
m.release();
}
else
{
CV_Error(Error::StsNotImplemented, "");
}
}
void _OutputArray::assign(const std::vector<UMat>& v) const
{
int k = kind();
......
......@@ -129,27 +129,7 @@
#include "parallel_impl.hpp"
#ifndef CV__EXCEPTION_PTR
# if defined(__ANDROID__) && defined(ATOMIC_INT_LOCK_FREE) && ATOMIC_INT_LOCK_FREE < 2
# define CV__EXCEPTION_PTR 0 // Not supported, details: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=58938
# elif defined(CV_CXX11)
# define CV__EXCEPTION_PTR 1
# elif defined(_MSC_VER)
# define CV__EXCEPTION_PTR (_MSC_VER >= 1600)
# elif defined(__clang__)
# define CV__EXCEPTION_PTR 0 // C++11 only (see above)
# elif defined(__GNUC__) && defined(__GXX_EXPERIMENTAL_CXX0X__)
# define CV__EXCEPTION_PTR (__GXX_EXPERIMENTAL_CXX0X__ > 0)
# endif
#endif
#ifndef CV__EXCEPTION_PTR
# define CV__EXCEPTION_PTR 0
#elif CV__EXCEPTION_PTR
# include <exception> // std::exception_ptr
#endif
#include "opencv2/core/detail/exception_ptr.hpp" // CV__EXCEPTION_PTR = 1 if std::exception_ptr is available
using namespace cv;
......
// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.
#include "test_precomp.hpp"
#include <opencv2/core/async.hpp>
#include <opencv2/core/detail/async_promise.hpp>
#include <opencv2/core/bindings_utils.hpp>
#ifdef CV_CXX11
#include <thread>
#include <chrono>
#endif
namespace opencv_test { namespace {
TEST(Core_Async, BasicCheck)
{
Mat m(3, 3, CV_32FC1, Scalar::all(5.0f));
AsyncPromise p;
AsyncArray r = p.getArrayResult();
EXPECT_TRUE(r.valid());
// Follow the limitations of std::promise::get_future
// https://en.cppreference.com/w/cpp/thread/promise/get_future
EXPECT_THROW(AsyncArray r2 = p.getArrayResult(), cv::Exception);
p.setValue(m);
Mat m2;
r.get(m2);
EXPECT_EQ(0, cvtest::norm(m, m2, NORM_INF));
// Follow the limitations of std::future::get
// https://en.cppreference.com/w/cpp/thread/future/get
EXPECT_FALSE(r.valid());
Mat m3;
EXPECT_THROW(r.get(m3), cv::Exception);
}
TEST(Core_Async, ExceptionCheck)
{
Mat m(3, 3, CV_32FC1, Scalar::all(5.0f));
AsyncPromise p;
AsyncArray r = p.getArrayResult();
EXPECT_TRUE(r.valid());
try
{
CV_Error(Error::StsOk, "Test: Generated async error");
}
catch (const cv::Exception& e)
{
p.setException(e);
}
try {
Mat m2;
r.get(m2);
FAIL() << "Exception is expected";
}
catch (const cv::Exception& e)
{
EXPECT_EQ(Error::StsOk, e.code) << e.what();
}
// Follow the limitations of std::future::get
// https://en.cppreference.com/w/cpp/thread/future/get
EXPECT_FALSE(r.valid());
}
TEST(Core_Async, LikePythonTest)
{
Mat m(3, 3, CV_32FC1, Scalar::all(5.0f));
AsyncArray r = cv::utils::testAsyncArray(m);
EXPECT_TRUE(r.valid());
Mat m2;
r.get(m2);
EXPECT_EQ(0, cvtest::norm(m, m2, NORM_INF));
// Follow the limitations of std::future::get
// https://en.cppreference.com/w/cpp/thread/future/get
EXPECT_FALSE(r.valid());
}
#ifdef CV_CXX11
TEST(Core_Async, AsyncThread_Simple)
{
Mat m(3, 3, CV_32FC1, Scalar::all(5.0f));
AsyncPromise p;
AsyncArray r = p.getArrayResult();
std::thread t([&]{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
try {
p.setValue(m);
} catch (const std::exception& e) {
std::cout << e.what() << std::endl;
} catch (...) {
std::cout << "Unknown C++ exception" << std::endl;
}
});
try
{
Mat m2;
r.get(m2);
EXPECT_EQ(0, cvtest::norm(m, m2, NORM_INF));
t.join();
}
catch (...)
{
t.join();
throw;
}
}
TEST(Core_Async, AsyncThread_DetachedResult)
{
Mat m(3, 3, CV_32FC1, Scalar::all(5.0f));
AsyncPromise p;
{
AsyncArray r = p.getArrayResult();
r.release();
}
bool exception_ok = false;
std::thread t([&]{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
try {
p.setValue(m);
} catch (const cv::Exception& e) {
if (e.code == Error::StsError)
exception_ok = true;
else
std::cout << e.what() << std::endl;
} catch (const std::exception& e) {
std::cout << e.what() << std::endl;
} catch (...) {
std::cout << "Unknown C++ exception" << std::endl;
}
});
t.join();
EXPECT_TRUE(exception_ok);
}
#endif
}} // namespace
#!/usr/bin/env python
from __future__ import print_function
import numpy as np
import cv2 as cv
from tests_common import NewOpenCVTests
class AsyncTest(NewOpenCVTests):
def test_async_simple(self):
m = np.array([[1,2],[3,4],[5,6]])
async_result = cv.utils.testAsyncArray(m)
self.assertTrue(async_result.valid())
ret, result = async_result.get(timeoutNs=10**6) # 1ms
self.assertTrue(ret)
self.assertFalse(async_result.valid())
self.assertEqual(cv.norm(m, result, cv.NORM_INF), 0)
def test_async_exception(self):
async_result = cv.utils.testAsyncException()
self.assertTrue(async_result.valid())
try:
_ret, _result = async_result.get(timeoutNs=10**6) # 1ms
self.fail("Exception expected")
except cv.error as e:
self.assertEqual(cv.Error.StsOk, e.code)
if __name__ == '__main__':
NewOpenCVTests.bootstrap()
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册