利用智能指针实现伪闭包延长session生命周期

291 阅读9分钟

为什么报错# error C2589: “(”:“::”右边的非法标记错误的处理

std::max、std::min error C2589: “(”:“::”右边的非法标记,error C2059: 语法错误:“::” - rainbow70626 - 博客园 (cnblogs.com)

闭包:子函数如果引用外部的局部变量,这个res的生命周期就会得到延伸,只要内部函数没有被释放掉,那么这个res也不会被释放掉 C++里面是没有闭包的机制的,只能实现伪闭包

func DeferReturn(res int){
    defer func(){
        res++
        log.Println(res)
        }()
        return 0
}

假设把智能指针传递给回调函数,对于这样一个函数对象,对象还在智能指针就还在,回调函数内部的函数再使用这个智能指针,那么智能指针就不会被释放掉了,但也不能保证,比如这个函数还没被调用呢,智能指针引用计数就不会加一,所以需要用lamda表达式通过bind的方式强制把智能指针引用计数加一

我们可以通过智能指针的方式管理Session类,将acceptor接收的链接保存在Session类型的智能指针里。由于智能指针会在引用计数为0时自动析构,所以为了防止其被自动回收,也方便Server管理Session,因为我们后期会做一些重连踢人等业务逻辑,我们在Server类中添加成员变量,该变量为一个map类型,key为Session的uid,value为该Session的智能指针。

#pragma once
#include"Session.h"
#include<map>
#include<boost/asio.hpp>
#include<memory.h>
#include<map>

using boost::asio::ip::tcp;

class Server {//服务器接受连接
public:
	Server(boost::asio::io_context& ioc, short port);
	void ClearSession(std::string uuid);
private:
	void start_accept();//有连接到来后会调用回调函数
	void handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& ec);
	boost::asio::io_context& _ioc;//io_context官方不支持拷贝赋值啥的,所以用引用作为一个成员变量
	tcp::acceptor _acceptor;

	std::map<std::string, std::shared_ptr<Session>> _sessions;
};

通过Server中的_sessions这个map管理链接,可以增加Session智能指针的引用计数,只有当Session从这个map中移除后,Session才会被释放。
所以在接收连接的逻辑里将Session放入map

void Server::start_accept() {
	std::shared_ptr<Session> new_session = make_shared<Session>(_ioc, this);
	//把新的socket传进去和欢迎套接字建立一个新的连接
	//有客户连接就要调用回调函数了
	_acceptor.async_accept(new_session->Socket(), std::bind(&Server::handle_accept, this, new_session, placeholders::_1));

}//这个右括号执行完后new_session会被释放么?不会的
//智能指针通过值的复制或者拷贝或者被用到内部函数里的时候他的引用计数加一
//只要这个bind的函数没有被调用或者没有被asio底层函数队列移除,newsession生命周期都是存在的

void Server::handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& ec) {
	if (!ec) {
		new_session->Start();
		_sessions.insert(std::make_pair(new_session->GetUuid(), new_session));
	}
	else {//出问题都没连接成功,还没插入所以不需要在这里处理
		//delete new_session;
		cout << "session accept failed, error is " << ec.what() << endl;
	}

	//这个回话处理完后,还要接待新的客户
	start_accept();
}

_sessions.insert(std::make_pair(new_session->GetUuid(), new_session)); 这行代码的作用是将 new_session 对象插入到 _sessions 映射中,使用 new_session 的 UUID 作为键。这样,后续可以通过 UUID 快速查找对应的会话对象。

在这个案例中,智能指针 std::shared_ptr 用于管理 Session 对象的生命周期。std::shared_ptr 通过引用计数来管理对象的生命周期,当有新的 shared_ptr 拷贝指向同一个对象时,引用计数会增加,当 shared_ptr 被销毁时,引用计数会减少。

生命周期延长的机制

  1. 创建 new_session

    std::shared_ptr<Session> new_session = make_shared<Session>(_ioc, this);
    

    这里通过 std::make_shared 创建了一个新的 Session 对象,并且返回了一个指向它的 std::shared_ptr。此时,new_session 是唯一指向这个 Session 对象的智能指针,引用计数为1。

  2. 异步接受连接时

    _acceptor.async_accept(new_session->Socket(), std::bind(&Server::handle_accept, this, new_session, placeholders::_1));
    

    这里将 new_session 作为参数传递给了 async_accept 函数。由于 std::shared_ptr 是通过值传递的,所以引用计数会增加1(现在引用计数为2)。即使 start_accept 函数执行完毕后,new_session 的本地副本会被销毁,但传递给 async_acceptnew_session 副本仍然存在,因此 Session 对象不会被释放。

  3. handle_accept 函数中

    void Server::handle_accept(std::shared_ptr<Session> new_session, const boost::system::error_code& ec) {
    

    当异步接受操作完成时,handle_accept 函数会被调用。new_session 参数是 std::shared_ptr 的一个新副本,它也指向同一个 Session 对象,引用计数再次增加。

  4. 插入到 _sessions 映射中

    _sessions.insert(std::make_pair(new_session->GetUuid(), new_session));
    

    这里将 new_session 插入到 _sessions 映射中。映射的值也是 std::shared_ptr,这意味着 new_session 被赋值给映射的值,引用计数再次增加。

  5. handle_accept 函数返回后: 函数执行完毕后,局部变量 new_session 的生命周期结束,引用计数减少1。但由于 _sessions 仍然持有这个 shared_ptr,所以 Session 对象不会被释放。

  6. 后续的异步操作: 只要 asio 的异步操作没有完成,或者 handle_accept 函数没有被调用,new_session 的生命周期就会持续。即使 start_accept 函数再次被调用,开始新的异步接受操作,之前的 new_session 仍然会被保持,直到所有相关的异步操作完成。

结论

智能指针 std::shared_ptr 的生命周期是由引用计数管理的。在这个案例中,通过将 new_session 传递给异步操作,以及将其存储在 _sessions 映射中,确保了 Session 对象的生命周期至少持续到异步操作完成。每次 new_session 被复制或赋值时,引用计数都会增加,从而延长了它的生命周期。 这段代码是一个异步服务器框架的一部分,展示了 Session 类如何处理异步读写操作。以下是代码的详细解释:

Session::Start()

void Session::Start() {
    memset(_data, 0, MAX_LENGTH); // 数据数组全设0;
    _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
        bind(&Session::handle_read, this, placeholders::_1, placeholders::_2, shared_from_this())); // 绑定的是类成员函数,要用this
}
  • Start() 方法是会话的起点,它首先将接收缓冲区 _data 的内容清零。
  • 然后调用 async_read_some 方法异步读取数据。读取操作完成后,会自动调用 handle_read 方法。
  • bind 用于绑定 handle_read 方法,以便在读取操作完成时调用。placeholders::_1placeholders::_2 分别代表错误码和传输的字节数。
  • shared_from_this() 返回当前对象的 shared_ptr,用于在异步操作中保持 Session 对象的生命周期。

Session::handle_read()

void Session::handle_read(const boost::system::error_code& ec, size_t bytes_transferred, shared_ptr<Session> _self_shared) {
    if (!ec) {
        cout << "server recervi data is" << _data << endl;
        boost::asio::async_write(_socket,
            boost::asio::buffer(_data, bytes_transferred),
            bind(&Session::handle_write, this, placeholders::_1, _self_shared));
    } else {
        cout << "read error" << endl;
        //delete this;
        _server->ClearSession(_uuid);
    }
}
  • handle_read() 方法在异步读取操作完成时被调用。
  • 如果读取成功(ec 为空),则将接收到的数据写回客户端。
  • 如果读取失败,则调用 _server->ClearSession(_uuid) 来销毁会话。

Session::GetUuid()

std::string& Session::GetUuid() {
    return _uuid;
}
  • GetUuid() 方法返回会话的唯一标识符。

Session::handle_write()

void Session::handle_write(const boost::system::error_code& ec, shared_ptr<Session> _self_shared) {
    if (!ec) {
        memset(_data, 0, MAX_LENGTH); 
        _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH),
            bind(&Session::handle_read, this, placeholders::_1, placeholders::_2, _self_shared));
    } else {
        cout << "write error" << endl;
        //delete this; // 把Session销毁掉,实际这么做有隐患
        _server->ClearSession(_uuid);
    }
}
  • handle_write() 方法在异步写入操作完成时被调用。
  • 如果写入成功,再次调用 async_read_some 方法准备接收下一批数据。
  • 如果写入失败,则调用 _server->ClearSession(_uuid) 来销毁会话。

智能指针生命周期管理

在这个代码中,智能指针 std::shared_ptr 用于管理 Session 对象的生命周期。以下是关键点:

  1. shared_from_this():在异步操作中,shared_from_this() 用于获取当前对象的 shared_ptr。这确保了只要异步操作还在等待,Session 对象就不会被销毁。我们的shred_ptr是存在server的map里的,这里其实可以用指向server的数据成员获取到sessions的map,结合uuid获取到当前session的shared_ptr,也就是将“_server->_sessions【_uuid】”填入bind的最后一个参数只要别重新构造智能指针造成重复析构就好

  2. bind:通过 bindSession 对象的 shared_ptr 被传递给异步操作的回调函数,这延长了对象的生命周期。

  3. ClearSession:当读取或写入操作失败时,ClearSession 方法被调用以销毁会话。这通常会涉及到从容器中移除会话对象,并可能减少引用计数到0,从而导致对象被销毁。

  4. 生命周期的延续:每次异步操作(读取或写入)都会创建一个新的 shared_ptr 副本,这增加了引用计数。只要引用计数不为0,Session 对象就会保持活动状态。

  5. 避免循环引用:在这个代码中,没有明显的循环引用问题,因为 Session 对象的生命周期完全由异步操作和服务器的会话管理控制。

通过这种方式,Session 对象的生命周期被管理得当,直到所有相关的异步操作完成,或者出现错误需要销毁会话。

map在这里面起到一个什么作用呢?

我想的是每次对智能指针的引用都会创建一个副本存到map里面,他们都指向同一个对象共享同一个计数, 初始创建会话的时候为1,传给async_accept的时候为2,Start()启动会话的时候因为用了shared_fromthis()调用,为3,作为handle_read的形参传到里面异步写bind的handle_write函数的时候,又加1为4,而作为handle_write的形参传到里面异步读bind的handle_read函数的时候加1为5,