代码之家  ›  专栏  ›  技术社区  ›  Takatoshi Kondo

C++17和使用move capture lambda表达式调用的异步成员函数

  •  -1
  • Takatoshi Kondo  · 技术社区  · 6 年前

    在里面 the other question 我问过,我了解到一些评估顺序是从C++17开始定义的。后缀表达式,例如 a->f(...) a.b(...) 是他们的一部分。看见 https://timsong-cpp.github.io/cppwp/n4659/expr.call#5

    Boost.Asio ,以下样式的异步成员函数调用是典型的模式。

    auto sp_object = std::make_shared<object>(...);
    sp_object->async_func(
        params,
        [sp_object]
        (boost::syste_error_code const&e, ...) {
            if (e) return;
            sp_object->other_async_func(
                params,
                [sp_object]
                (boost::syste_error_code const&e, ...) {
                    if (e) return;
                    // do some
                }
            );
        }
    );
    

    我想澄清以下三种情况的安全性。

    案例1:共享移动和成员功能

    auto sp_object = std::make_shared<object>(...);
    sp_object->async_func(
        params,
        [sp_object = std::move(sp_object)]
        (boost::syste_error_code const&e, ...)  mutable { // mutable is for move
            if (e) return;
            sp_object->other_async_func(
                params,
                [sp_object = std::move(sp_object)]
                (boost::syste_error_code const&e, ...) {
                    if (e) return;
                    // do some
                }
            );
        }
    );
    

    这种模式就像 https://www.boost.org/doc/libs/1_70_0/doc/html/boost_asio/reference/basic_stream_socket/async_read_some.html

    我认为它是安全的,因为后缀表达式 -> 以前评估过吗 sp_object = std::move(sp_object) .

    案例2:值移动和成员函数

    some_type object(...);
    object.async_func(
        params,
        [object = std::move(object)]
        (boost::syste_error_code const&e, ...)  mutable { // mutable is for move
            if (e) return;
            object.other_async_func(
                params,
                [object = std::move(object)]
                (boost::syste_error_code const&e, ...) {
                    if (e) return;
                    // do some
                }
            );
        }
    );
    

    我认为这很危险,因为即使后缀表达式 . 以前评估过吗 object = std::move(object) , async_func 可以访问 object .

    案例3:共享移动和自由功能

    auto sp_object = std::make_shared<object>(...);
    async_func(
        *sp_object,
        params,
        [sp_object = std::move(sp_object)]
        (boost::syste_error_code const&e, ...)  mutable { // mutable is for move
            if (e) return;
            other_async_func(
                *sp_object,
                params,
                [sp_object = std::move(sp_object)]
                (boost::syste_error_code const&e, ...) {
                    if (e) return;
                    // do some
                }
            );
        }
    );
    

    这种模式就像 https://www.boost.org/doc/libs/1_70_0/doc/html/boost_asio/reference/async_read/overload1.html

    我认为这很危险,因为没有后缀表达式。所以 sp_object 可以在解除引用之前通过第三个参数移动捕获 *sp_object 根据第一个论点。

    结论

    只有情况1是安全的,其他情况是危险的(未定义的行为)。 我需要注意的是,它在C++14和更旧的编译器上是不安全的。 它可以加速 调用异步成员函数 因为shared_ptr的原子计数器操作没有发生。看见 Why would I std::move an std::shared_ptr? 但我也需要考虑这个优势可以忽略,这取决于应用。

    我是否正确理解C++17求值顺序更改(精确定义)和异步操作关系?

    0 回复  |  直到 6 年前
        1
  •  1
  •   Takatoshi Kondo    6 年前

    答复

    多亏了探险家的评论。我得到了答案。

    我问“案例1是安全的,但案例2和案例3是不安全的,是吗?”。然而,案例1是安全的 当且仅当满足我稍后编写的约束(*1)时 .这意味着 案例1一般不安全 .

    这取决于 async_func()

    以下是一个不安全的案例:

    #include <iostream>
    #include <memory>
    #include <boost/asio.hpp>
    
    struct object : std::enable_shared_from_this<object> {
        object(boost::asio::io_context& ioc):ioc(ioc) {
            std::cout << "object constructor this: " << this << std::endl;
        }
    
        template <typename Handler>
        void async_func(Handler&& h) {
            std::cout << "this in async_func: " << this << std::endl;
            h(123); // how about here?
            std::cout << "call shared_from_this in async_func: " << this << std::endl;
            auto sp = shared_from_this();
            std::cout << "sp->get() in async_func: " << sp.get() << std::endl;
        }
    
        template <typename Handler>
        void other_async_func(Handler&& h) {
            std::cout << "this in other_async_func: " << this << std::endl;
            h(123); // how about here?
            std::cout << "call shared_from_this in other_async_func: " << this << std::endl;
            auto sp = shared_from_this();
            std::cout << "sp->get() in other_async_func: " << sp.get() << std::endl;
        }
    
        boost::asio::io_context& ioc;
    };
    
    int main() {
        boost::asio::io_context ioc;
        auto sp_object = std::make_shared<object>(ioc);
    
        sp_object->async_func(
            [sp_object = std::move(sp_object)]
            (int v) mutable { // mutable is for move
                std::cout << v << std::endl;
                sp_object->other_async_func(
                    [sp_object = std::move(sp_object)]
                    (int v) {
                        std::cout << v << std::endl;
                    }
                );
            }
        );
        ioc.run();
    }
    

    运行演示 https://wandbox.org/permlink/uk74ACox5EEvt14o

    我考虑了为什么第一个 shared_from_this() 没关系,但第二次通话 std::bad_weak_ptr 在上面的代码中。这是因为回调处理程序是从 async_func other_async_func 直接地这一举动发生了两次。所以第一层( 异步函数 ) shared_from_this 他失败了。

    即使回调处理程序不是直接从异步函数调用的,在多线程情况下也是不安全的。

    下面是一个不安全的代码:

    #include <iostream>
    #include <memory>
    #include <boost/asio.hpp>
    
    struct object : std::enable_shared_from_this<object> {
        object(boost::asio::io_context& ioc):ioc(ioc) {
            std::cout << "object constructor this: " << this << std::endl;
        }
    
        template <typename Handler>
        void async_func(Handler&& h) {
            std::cout << "this in async_func: " << this << std::endl;
    
            ioc.post(
                [this, h = std::forward<Handler>(h)] () mutable {
                    h(123);
                    sleep(1);
                    auto sp = shared_from_this();
                    std::cout << "sp->get() in async_func: " << sp.get() << std::endl;
                }
            );
        }
    
        template <typename Handler>
        void other_async_func(Handler&& h) {
            std::cout << "this in other_async_func: " << this << std::endl;
    
            ioc.post(
                [this, h = std::forward<Handler>(h)] () {
                    h(456);
                    auto sp = shared_from_this();
                    std::cout << "sp->get() in other_async_func: " << sp.get() << std::endl;
                }
            );
        }
    
        boost::asio::io_context& ioc;
    };
    
    int main() {
        boost::asio::io_context ioc;
        auto sp_object = std::make_shared<object>(ioc);
    
        sp_object->async_func(
            [sp_object = std::move(sp_object)]
            (int v) mutable { // mutable is for move
                std::cout << v << std::endl;
                sp_object->other_async_func(
                    [sp_object = std::move(sp_object)]
                    (int v) {
                        std::cout << v << std::endl;
                    }
                );
            }
        );
        std::vector<std::thread> ths;
        ths.reserve(2);
        for (std::size_t i = 0; i != 2; ++i) {
            ths.emplace_back(
                [&ioc] {
                    ioc.run();
                }
            );
        }
        for (auto& t : ths) t.join();
    }
    

    运行演示: https://wandbox.org/permlink/xjLZWoLdn8xL89QJ

    案例1的约束是安全的

    *1 然而,在案例1中,当且仅当 struct object 不希望它被共享的ptr持有,它是安全的。换句话说,只要 结构对象 不用 分享了这个 这是安全的。

    另一种控制顺序的方法。(支持C++14)

    当且仅当满足上述约束时,我们可以在不使用C++17序列定义的情况下控制求值序列。 它支持案例1和案例3。只需获取shared_ptr持有的指针对象的引用即可。关键的一点是,即使移动了共享的_ptr,指针对象也会被保留。因此,在移动共享_ptr之前获取指针对象的引用,然后移动共享_ptr,指针对象不受影响。

    然而,这是一个例外。它直接使用共享的ptr机制。所以这会受到共享ptr移动的影响。因此它是不安全的。这就是限制的原因。

    案例1

    // The class of sp_object class doesn't use shared_from_this mechanism
    auto sp_object = std::make_shared<object>(...);
    auto& r = *sp_object;
    r.async_func(
        params,
        [sp_object]
        (boost::syste_error_code const&e, ...) {
            if (e) return;
            auto& r = *sp_object;
            r.other_async_func(
                params,
                [sp_object]
                (boost::syste_error_code const&e, ...) {
                    if (e) return;
                    // do some
                }
            );
        }
    );
    

    案例3

    // The class of sp_object class doesn't use shared_from_this mechanism
    auto sp_object = std::make_shared<object>(...);
    auto& r = *sp_object;
    async_func(
        r,
        params,
        [sp_object = std::move(sp_object)]
        (boost::syste_error_code const&e, ...)  mutable { // mutable is for move
            if (e) return;
            auto& r = *sp_object;
            other_async_func(
                r,
                params,
                [sp_object = std::move(sp_object)]
                (boost::syste_error_code const&e, ...) {
                    if (e) return;
                    // do some
                }
            );
        }
    );
    
        2
  •  0
  •   Eric    6 年前

    您的问题可以大大简化为“以下各项是否安全”:

    some_object.foo([bound_object = std::move(some_object)]() {
        bound_object.bar()
    });
    

    根据你的相关问题, the standard says

    参数计算的所有副作用在函数输入之前都是按顺序排列的

    这样的副作用之一是 move 从某个物体——这相当于:

    auto callback = [bound_object = std::move(some_object)]() {
        bound_object.bar()
    }
    some_object.foo(std::move(callback));
    

    很明显,这已经超出了 some_object foo 方法被调用。这是安全的当且仅当 对从对象移动的对象调用。


    利用这些知识:

    • 案例1可能会出现故障,而且肯定不安全,因为打电话 operator->() 在从 shared_ptr 返回 nullptr ,然后你就叫它 ->async_func 在…上
    • 案例2只有在打电话时才安全 async_func 在从 some_type 是安全的,但除非类型实际上没有定义移动构造函数,否则它不太可能实现您想要的功能。
    • 案例3是不安全的,因为在取消引用之后移动共享指针是可以的(因为共享指针不改变它指向的对象),C++不能保证首先对哪个函数参数进行评估。