找工作小项目:day16-重构核心库、使用智能指针(2)

news2024/11/25 16:53:18

day16-重构核心库、使用智能指针

太多了分一篇写。

5、EventLoop

这是一个事件轮询,在这个部分会通过Poller进行就绪事件的获取,并将事件进行处理。

头文件

这里使用了一个智能指针并使用的是unique_ptr指向Poller红黑树,防止所有权不止一个,也就是出现指针悬挂的问题。

class EventLoop {
 public:
  DISALLOW_COPY_AND_MOVE(EventLoop);
  EventLoop();
  ~EventLoop();

  void Loop() const;
  void UpdateChannel(Channel *ch) const;
  void DeleteChannel(Channel *ch) const;

 private:
  std::unique_ptr<Poller> poller_;
};

实现

这更像是一个中间件,通过初始化一个unique_ptr指针调用Poller中各种方法,由于使用了智能指针,析构函数为空,这是由于在超出作用域后poller_会自动释放。
通过Loop轮询处理将就绪事件链表中的事件进行处理,处理事件的方法在Channel中实现。

EventLoop::EventLoop() { poller_ = std::make_unique<Poller>(); }

EventLoop::~EventLoop() {}

void EventLoop::Loop() const {
  while (true) {
    for (Channel *active_ch : poller_->Poll()) {
      active_ch->HandleEvent();
    }
  }
}

void EventLoop::UpdateChannel(Channel *ch) const { poller_->UpdateChannel(ch); }

void EventLoop::DeleteChannel(Channel *ch) const { poller_->DeleteChannel(ch); }

6、Acceptor

上述几个类处理了事件,但是建立连接只在Socket中出现过,这部分太底层了,我们需要开发更高级的接口,用来建立连接。

头文件

同样禁止了类的复制和移动,这里将构造函数设置为explicit目的是防止发生类似于这样的隐式类型转换:

EventLoop loop;
Acceptor acceptor = loop;

这个类中包含有socket、channel以及回调函数三个属性用以构建连接。这里使用unique_ptr来管理socket、channel属性。

class Acceptor {
 public:
  DISALLOW_COPY_AND_MOVE(Acceptor);
  explicit Acceptor(EventLoop *loop);
  ~Acceptor();

  RC AcceptConnection() const;
  void set_new_connection_callback(std::function<void(int)> const &callback);

 private:
  std::unique_ptr<Socket> socket_;
  std::unique_ptr<Channel> channel_;
  std::function<void(int)> new_connection_callback_;
};

实现

在构造和析构函数中,首先创建指向socket的指针,之后开始调用创建、绑定、将socket设置为可监听的状态,然后构建channel并加入到轮询中,之后将AcceptConnection作为channel中的读回调函数并将通道事件置为可读。由于使用智能指针管理,因此析构函数为空。

Acceptor::Acceptor(EventLoop *loop) {
  socket_ = std::make_unique<Socket>();
  assert(socket_->Create() == RC_SUCCESS);
  assert(socket_->Bind("127.0.0.1", 1234) == RC_SUCCESS);
  assert(socket_->Listen() == RC_SUCCESS);

  channel_ = std::make_unique<Channel>(socket_->fd(), loop);
  std::function<void()> cb = std::bind(&Acceptor::AcceptConnection, this);

  channel_->set_read_callback(cb);
  channel_->EnableRead();
}

Acceptor::~Acceptor() {}

将服务器与请求连接的客户端进行连接,并记录客户端的socket,将建立的连接设置为无阻塞状态,最后调用new_connection_callback_回调函数。

RC Acceptor::AcceptConnection() const{
  int clnt_fd = -1;
  if( socket_->Accept(clnt_fd) != RC_SUCCESS ) {
    return RC_ACCEPTOR_ERROR;
  }
  // TODO: remove
  fcntl(clnt_fd, F_SETFL, fcntl(clnt_fd, F_GETFL) | O_NONBLOCK);  // 新接受到的连接设置为非阻塞式
  if (new_connection_callback_) {
    new_connection_callback_(clnt_fd);
  }
  return RC_SUCCESS;
}

set_new_connection_callback设置回调函数,在设置的过程中有个小细节就是利用std::move转移所有权,也就是callback变成了右值。

void Acceptor::set_new_connection_callback(std::function<void(int)> const &callback) {
  new_connection_callback_ = std::move(callback);
}

7、Connection

Connection 类的主要任务是管理一个单一的网络连接。它封装了连接的各种操作和状态,提供了一种面向对象的方式来处理读写数据、维护连接状态以及处理事件,使得用户可以通过面向对象的方式来管理每一个连接,并且可以通过设置回调函数来处理连接建立、数据接收等事件。

头文件

提供了标志连接状态的标志位State,主要有socket、channel、buf、state、以及一些回调函数。可以通过这些属性看出来,在这个类中完成了所有连接上可能发生的事件。

class Connection {
 public:
  enum State {
    Invalid = 0,
    Connecting,
    Connected,
    Closed,
  };
  DISALLOW_COPY_AND_MOVE(Connection);
  Connection(int fd, EventLoop *loop);
  ~Connection();

  void set_delete_connection(std::function<void(int)> const &fn);
  void set_on_connect(std::function<void(Connection *)> const &fn);
  void set_on_recv(std::function<void(Connection *)> const &fn);
  State state() const;
  Socket *socket() const;
  void set_send_buf(const char *str);
  Buffer *read_buf();
  Buffer *send_buf();

  RC Read();
  RC Write();
  RC Send(std::string msg);

  void Close();

  void onConnect(std::function<void()> fn);
  void onMessage(std::function<void()> fn);

 private:
  void Business();
  RC ReadNonBlocking();
  RC WriteNonBlocking();
  RC ReadBlocking();
  RC WriteBlocking();

 private:
  std::unique_ptr<Socket> socket_;
  std::unique_ptr<Channel> channel_;

  State state_;
  std::unique_ptr<Buffer> read_buf_;
  std::unique_ptr<Buffer> send_buf_;

  std::function<void(int)> delete_connectioin_;
  std::function<void(Connection *)> on_recv_;
};

实现

构造函数中首先创建了指向socket的指针并为socket进行赋值,再创建Channel将socket和事件类型进行绑定,并初始化读缓存和发送缓存,将状态设置为已连接。。
析构函数由于使用智能指针管理了几个类,会自动释放,因此析构函数仍然为空。

Connection::Connection(int fd, EventLoop *loop) {
  socket_ = std::make_unique<Socket>();
  socket_->set_fd(fd);
  if (loop != nullptr) {
    channel_ = std::make_unique<Channel>(fd, loop);
    channel_->EnableRead();
    channel_->EnableET();
  }
  read_buf_ = std::make_unique<Buffer>();
  send_buf_ = std::make_unique<Buffer>();

  state_ = State::Connected;
}

Connection::~Connection() {}

对读写事件逻辑进行设置,首先判断链接状态,如果Connection是未连接的状态则无法进行任何操作。在读事件中需要先清空缓存数据再读,防止上次遗留数据发生影响。在写事件中需要先操作再清空数据,防止对下次写入产生影响。这样无论写读还是读写都不会发生问题。

RC Connection::Read() {
  if (state_ != State::Connected) {
    perror("Connection is not connected, can not read");
    return RC_CONNECTION_ERROR;
  }
  assert(state_ == State::Connected && "connection state is disconnected!");
  read_buf_->Clear();
  if (socket_->IsNonBlocking()) {
    return ReadNonBlocking();
  } else {
    return ReadBlocking();
  }
}
RC Connection::Write() {
  if (state_ != State::Connected) {
    perror("Connection is not connected, can not write");
    return RC_CONNECTION_ERROR;
  }
  RC rc = RC_UNDEFINED;
  if (socket_->IsNonBlocking()) {
    rc = WriteNonBlocking();
  } else {
    rc = WriteBlocking();
  }
  send_buf_->Clear();
  return rc;
}

接下来是无阻塞和有阻塞读事件的具体代码,无阻塞的逻辑是一直在死循环中读取数据,只要数据就绪就进行读取,没有数据是则进行轮询;而阻塞的逻辑是一直读,如果没有数据就停在那里等待数据。

RC Connection::ReadNonBlocking() {
  int sockfd = socket_->fd();
  char buf[1024];  // 这个buf大小无所谓
  while (true) {   // 使用非阻塞IO,读取客户端buffer,一次读取buf大小数据,直到全部读取完毕
    memset(buf, 0, sizeof(buf));
    ssize_t bytes_read = read(sockfd, buf, sizeof(buf));
    if (bytes_read > 0) {
      read_buf_->Append(buf, bytes_read);
    } else if (bytes_read == -1 && errno == EINTR) {  // 程序正常中断、继续读取
      printf("continue reading\n");
      continue;
    } else if (bytes_read == -1 &&
               ((errno == EAGAIN) || (errno == EWOULDBLOCK))) {  // 非阻塞IO,这个条件表示数据全部读取完毕
      break;
    } else if (bytes_read == 0) {  // EOF,客户端断开连接
      printf("read EOF, client fd %d disconnected\n", sockfd);
      state_ = State::Closed;
      Close();
      break;
    } else {
      printf("Other error on client fd %d\n", sockfd);
      state_ = State::Closed;
      Close();
      break;
    }
  }
  return RC_SUCCESS;
}

RC Connection::ReadBlocking() {
  int sockfd = socket_->fd();
  // unsigned int rcv_size = 0;
  // socklen_t len = sizeof(rcv_size);
  // getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcv_size, &len);
  size_t data_size = socket_->RecvBufSize();
  char buf[1024];
  ssize_t bytes_read = read(sockfd, buf, sizeof(buf));
  if (bytes_read > 0) {
    read_buf_->Append(buf, bytes_read);
  } else if (bytes_read == 0) {
    printf("read EOF, blocking client fd %d disconnected\n", sockfd);
    state_ = State::Closed;
  } else if (bytes_read == -1) {
    printf("Other error on blocking client fd %d\n", sockfd);
    state_ = State::Closed;
  }
  return RC_SUCCESS;
}

在写数据时阻塞和非阻塞差别和读事件时一样,但是需要注意的是在写事件中我们需要记录每一次写到了缓存中那部分,下次从新的位置开始写入。

RC Connection::WriteNonBlocking() {
  int sockfd = socket_->fd();
  char buf[send_buf_->Size()];
  memcpy(buf, send_buf_->c_str(), send_buf_->Size());
  int data_size = send_buf_->Size();
  int data_left = data_size;
  while (data_left > 0) {
    ssize_t bytes_write = write(sockfd, buf + data_size - data_left, data_left);
    if (bytes_write == -1 && errno == EINTR) {
      printf("continue writing\n");
      continue;
    }
    if (bytes_write == -1 && errno == EAGAIN) {
      break;
    }
    if (bytes_write == -1) {
      printf("Other error on client fd %d\n", sockfd);
      state_ = State::Closed;
      break;
    }
    data_left -= bytes_write;
  }
  return RC_SUCCESS;
}

RC Connection::WriteBlocking() {
  // 没有处理send_buffer_数据大于TCP写缓冲区,的情况,可能会有bug
  int sockfd = socket_->fd();
  ssize_t bytes_write = write(sockfd, send_buf_->buf().c_str(), send_buf_->Size());
  if (bytes_write == -1) {
    printf("Other error on blocking client fd %d\n", sockfd);
    state_ = State::Closed;
  }
  return RC_SUCCESS;
}

这部分向send_buf_中写入信息数据,并开始传输。这是对外的最主要的接口,首先通过set_send_buf写入写缓存,再通过Write写入临时缓存。

RC Connection::Send(std::string msg) {
  set_send_buf(msg.c_str());
  Write();
  return RC_SUCCESS;
}
void Connection::set_send_buf(const char *str) { send_buf_->set_buf(str); }

这部分在设置回调函数的过程中就调用了读事件进行数据读取,首先通过read将数据读取到读缓存,再通过on_recv_做显示。注意:虽然代码中使用了临时的 buf 数组来暂存待发送的数据,但这些数据最终会通过操作系统内核的套接字发送缓冲区发送到目标端。发送过程中的详细流程是由操作系统网络栈和驱动程序处理的,应用程序通过 write 函数将数据提交给操作系统,而后续的网络传输则由操作系统负责管理。
临时缓冲区更易于维护,在写入前读取,在错误时不必重新构建整个send_buf_,而且不会发生竞态问题,提高效率。

void Connection::Business() {
  Read();
  on_recv_(this);
}
void Connection::set_on_recv(std::function<void(Connection *)> const &fn) {
  on_recv_ = std::move(fn);
  std::function<void()> bus = std::bind(&Connection::Business, this);
  channel_->set_read_callback(bus);
}

获取/设置一些属性。

void Connection::set_delete_connection(std::function<void(int)> const &fn) { delete_connectioin_ = std::move(fn); }
void Connection::Close() { delete_connectioin_(socket_->fd()); }
Connection::State Connection::state() const { return state_; }
Socket *Connection::socket() const { return socket_.get(); }
Buffer *Connection::read_buf() { return read_buf_.get(); }
Buffer *Connection::send_buf() { return send_buf_.get(); }

8、Buffer

这里就是缓冲区的构建,注意如果不是在单线程并且没有涉及到异步非阻塞的网络操作时,可以不显式的构造send_buf_和read_buf_而只需要一个buf;在多线程的情况下,使用send_buf_和read_buf_可以同时进行读写操作。

头文件

在缓冲区中只有一个属性buf_,而且使用默认构造/析构函数。

class Buffer {
 public:
  DISALLOW_COPY_AND_MOVE(Buffer);
  Buffer() = default;
  ~Buffer() = default;

  const std::string &buf() const;
  const char* c_str() const;
  void set_buf(const char *buf);

  size_t Size() const;
  void Append(const char *_str, int _size);
  void Clear();

 private:
  std::string buf_;
};

实现

这里获取buf和buf中数据的方法都被定义为const,代表在方法内不能修改数据,返回的数据不能被修改。
之后还有一个将buf中的数据交换到buf_的操作,注意这里由于buf由const修饰,所以不能直接swap,需要利用一个临时变量进行swap,string中接收使用const char * 类型的参数进行初始化。
之后有返回缓冲区字符长度以及清空缓存的方法。将字符串写入buf_中的方法。

const std::string &Buffer::buf() const { return buf_; }

const char *Buffer::c_str() const { return buf_.c_str(); }

void Buffer::set_buf(const char *buf) {
  std::string new_buf(buf);
  buf_.swap(new_buf);
}

size_t Buffer::Size() const { return buf_.size(); }

void Buffer::Append(const char *str, int size) {
  for (int i = 0; i < size; ++i) {
    if (str[i] == '\0') {
      break;
    }
    buf_.push_back(str[i]);
  }
}

void Buffer::Clear() { buf_.clear(); }

9、ThreadPool

线程池用来处理多线程的部分。

头文件

在线程池中有打工人(线程容器),任务(任务队列),锁,条件变量以及标志位。
由于线程池需要处理返回值参数各不相同的任务,因此在添加任务的方法上设置比较困难。

template<class F, class… Args>:这是模板函数的模板参数部分。它声明了函数模板add,其中class F表示一个类型参数,class… Args表示一个模板参数包(C++11引入的可变参数模板)。

auto add(F&& f, Args&&… args):这是函数的声明部分。auto表示返回类型会根据函数体内的具体实现而推导得出。F&& f表示一个通用引用(universal reference),用于接收传递给函数的函数对象f。Args&&… args表示一个模板参数包,用于接收传递给函数的参数列表。

-> std::future<typename std::result_of<F(Args…)>::type>:这是函数的返回类型部分。->后面跟着返回类型的声明。std::future是C++标准库中用于异步操作的类模板,它表示一个可能还没有完成的异步操作。typename std::result_of<F(Args…)>::type其中std::result_of用于获取函数调用的返回类型,它接受函数类型和参数类型,并提供一个type成员,表示函数调用的返回类型。在这里,F表示函数类型,Args…表示参数类型,因此std::result_of<F(Args…)>表示函数f的返回类型,而typename std::result_of<F(Args…)>::type表示该返回类型。

由于C++并非不支持模板分离编译(或者单独写一个文件能解决),因此需要在头文件中进行方法实现,首先利用using定义类型别名

创建了一个std::shared_ptr指向一个std::packaged_task对象的智能指针task。packaged_task用于包装可调用对象,并将其结果存储在std::future中,以便在需要时获取。

std::future<return_type> res = task->get_future();: 获取了task中的std::future对象,以便在任务执行完成后获取任务的返回值。

之后就是开始加锁并检查线程池是否停止运行,如果停止运行则抛出异常。

tasks.emplace(task{ (*task)(); });: 将一个 lambda 表达式添加到任务队列tasks中。这个 lambda 表达式捕获了task指针,并调用(*task)(),即执行被包装的任务。

cv.notify_one();: 通过条件变量cv通知一个等待的线程有任务可执行。

return res;: 返回任务的std::future对象,以便调用者可以在需要时获取任务的返回值。

至于为什么不使用unique_ptr呢?
1、任务对象被添加到任务队列后,可能会被多个工作线程共享执行。使用 std::shared_ptr 可以确保任务对象在所有持有者释放之前都不会被销毁,这样在多线程环境下更加安全。
2、在添加任务到任务队列时,任务对象需要被捕获到 lambda 表达式中,以便在工作线程中执行。由于 std::unique_ptr 是独占所有权的智能指针,它不能被复制或移动,这使得在多线程环境中捕获到 lambda 中有所限制。
3、跨线程传递任务对象可能是必须的,而 std::shared_ptr 提供了原子操作和线程安全的引用计数,使得跨线程传递更为方便。

class ThreadPool {
 public:
  DISALLOW_COPY_AND_MOVE(ThreadPool);
  explicit ThreadPool(unsigned int size = std::thread::hardware_concurrency());
  ~ThreadPool();

  template <class F, class... Args>
  auto Add(F &&f, Args &&...args) -> std::future<typename std::invoke_result<F, Args...>::type>;

 private:
  std::vector<std::thread> workers_;
  std::queue<std::function<void()>> tasks_;
  std::mutex queue_mutex_;
  std::condition_variable condition_variable_;
  std::atomic<bool> stop_{false};
};

// 不能放在cpp文件,C++编译器不支持模版的分离编译
template <class F, class... Args>
auto ThreadPool::Add(F &&f, Args &&...args) -> std::future<typename std::invoke_result<F, Args...>::type> {
  using return_type = typename std::invoke_result<F, Args...>::type;

  auto task =
      std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));

  std::future<return_type> res = task->get_future();
  {
    std::unique_lock<std::mutex> lock(queue_mutex_);

    // don't allow enqueueing after stopping the pool
    if (stop_) {
      throw std::runtime_error("enqueue on stopped ThreadPool");
    }

    tasks_.emplace_back([task]() { (*task)(); });
  }
  condition_variable_.notify_one();
  return res;
}

实现

构造函数中首先创建足够的打工人,之后在死循环中不断加入任务,当且仅当停止标志为真且任务队列为空才会停止所有线程。
析构函数中调用锁将停止位置真,调用 condition_variable_.notify_all(),通知所有等待在 condition_variable_ 条件变量上的线程,告知它们有可能发生了状态变化,需要重新检查条件,接着,使用循环遍历线程池中的工作线程,并逐一判断是否可加入(joinable),如果是,则调用 join() 方法等待线程执行完毕并回收资源。

析构函数设置 stop_ 为 true 并通知所有线程,这使得所有工作线程停止获取新任务并安全地退出。如果在设置 stop_ 标志时队列中仍有未完成的任务,则已经被工作线程获取的任务会继续执行,但线程池不会再处理新的任务。

ThreadPool::ThreadPool(unsigned int size) {
  for (unsigned int i = 0; i < size; ++i) {
    workers_.emplace_back(std::thread([this]() {
      while (true) {
        std::function<void()> task;
        {
          std::unique_lock<std::mutex> lock(queue_mutex_);
          condition_variable_.wait(lock, [this]() { return stop_ || !tasks_.empty(); });
          if (stop_ && tasks_.empty()) {
            return;
          }
          //从这里开始是以获取的任务
          task = tasks_.front();
          tasks_.pop();
        }
        task();
      }
    }));
  }
}

ThreadPool::~ThreadPool() {
  {
    std::unique_lock<std::mutex> lock(queue_mutex_);
    stop_ = true;
  }
  condition_variable_.notify_all();
  for (std::thread &th : workers_) {
    if (th.joinable()) {
      th.join();
    }
  }
}

10、TcpServer

在这里插入图片描述
这部分就包含了所有操作,从创建socket、建立连接、处理事件、删除链接,这个类提供了一个最朴实对外的接口。

头文件

在这部分包含有一个MainReactor用以完成初始化、分发连接,Acceptor用以建立连接,SubReactor用以处理事件并接收响应。并定义了一个线程池用以多线程完成高并发的任务,并且有两个回调函数。

class TcpServer {
 public:
  DISALLOW_COPY_AND_MOVE(TcpServer);
  TcpServer();
  ~TcpServer();
  void Start();

  RC NewConnection(int fd);
  RC DeleteConnection(int fd);

  void onConnect(std::function<void(Connection *)> fn);
  void onRecv(std::function<void(Connection *)> fn);

 private:
  std::unique_ptr<EventLoop> main_reactor_;
  std::unique_ptr<Acceptor> acceptor_;

  std::unordered_map<int, std::unique_ptr<Connection>> connections_;
  std::vector<std::unique_ptr<EventLoop>> sub_reactors_;

  std::unique_ptr<ThreadPool> thread_pool_;

  std::function<void(Connection *)> on_connect_;
  std::function<void(Connection *)> on_recv_;
};

实现

构造函数中首先构建一个指向轮询的MainReactor智能指针,之后从指针中利用.get()获取EventLoop并建立Acceptor(这里创建socket、绑定、监听),之后设置新连接到来时的回调函数为NewConnection,创建一个线程池并创建多个子 Reactor 。

析构函数同样由于管理由智能指针完成,析构函数为空。

TcpServer::TcpServer() {
  main_reactor_ = std::make_unique<EventLoop>();
  acceptor_ = std::make_unique<Acceptor>(main_reactor_.get());
  std::function<void(int)> cb = std::bind(&TcpServer::NewConnection, this, std::placeholders::_1);
  acceptor_->set_new_connection_callback(cb);

  unsigned int size = std::thread::hardware_concurrency();
  thread_pool_ = std::make_unique<ThreadPool>(size);

  for (size_t i = 0; i < size; ++i) {
    std::unique_ptr<EventLoop> sub_reactor = std::make_unique<EventLoop>();
    sub_reactors_.push_back(std::move(sub_reactor));
  }
}

TcpServer::~TcpServer() {}

在NewConnection方法中,创建一个新的 Connection 对象并关联到合适的子 Reactor 上,设置删除连接/接收数据的回调函数 DeleteConnection/on_recv_,将创建的 Connection 对象存储在 connections_ 容器中,如果有设置连接回调函数 on_connect_,则调用该函数。

RC TcpServer::NewConnection(int fd) {
  assert(fd != -1);
  uint64_t random = fd % sub_reactors_.size();

  std::unique_ptr<Connection> conn = std::make_unique<Connection>(fd, sub_reactors_[random].get());
  std::function<void(int)> cb = std::bind(&TcpServer::DeleteConnection, this, std::placeholders::_1);
  
  conn->set_delete_connection(cb);
  conn->set_on_recv(on_recv_);

  connections_[fd] = std::move(conn);
  if (on_connect_) {
    on_connect_(connections_[fd].get());
  }
  return RC_SUCCESS;
}

删除指定文件描述符的连接,在 connections_ 容器中查找并移除对应的 Connection 对象。

RC TcpServer::DeleteConnection(int fd) {
  auto it = connections_.find(fd);
  assert( it != connections_.end() );
  connections_.erase(fd);
  return RC_SUCCESS;
}

启动服务器的事件循环,使用线程池中的线程执行每个子 Reactor 的事件循环,主 Reactor 执行自己的事件循环。

void TcpServer::Start() {
  for (size_t i = 0; i < sub_reactors_.size(); ++i) {
    std::function<void()> sub_loop = std::bind(&EventLoop::Loop, sub_reactors_[i].get());
    thread_pool_->Add(std::move(sub_loop));
  }
  main_reactor_->Loop();
}

注意:线程池中的线程会专门处理读写事件,而不处理accept、connection这个过程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1832888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

应变玻璃合金是航天产业重要弹性材料 研究开发意义重大

应变玻璃合金是航天产业重要弹性材料 研究开发意义重大 应变玻璃&#xff0c;是一种形状记忆合金&#xff0c;为纳米级材料&#xff0c;其短程有序晶格应变区域呈冻结状态&#xff0c;具有典型的玻璃化转变特征&#xff0c;可以对外界刺激产生应变反应&#xff0c;也称为应变玻…

【Kaggle量化比赛】Top讨论

问: 惊人的单模型得分,请问您使用了多少个特征来获得如此高的得分?我也在使用LGB模型。 答 235个特征(180个基本特征+滚动特征) 问: 您是在使用Polars进行特征工程还是仅依赖于Pandas+Numba/多进程?即使进行了Numba优化,我也发现当滚动特征过多时,推理速度会非常慢。在…

云服务器ECS是什么?云服务器ECS有什么方便之处

什么是云服务器ECS&#xff1f; 云服务器 ECS 本质上是一种基于云计算技术的虚拟服务器。与传统服务器不同&#xff0c;它打破了传统物理服务器的局限性&#xff0c;为我们提供了一种全新的计算资源使用方式。 我们不再需要购买和维护昂贵的硬件设备&#xff0c;而是可以根据…

C++新特性复习1 版本11

参照来自于&#xff1a; cppreference.com 老实说&#xff0c;我是毕业不久就开始用C&#xff0c;原因就是VC&#xff0c;当时用来做界面。还好吧&#xff0c;不是觉得太难&#xff0c;起码对数学底子没有要求&#xff0c;后面偶尔也用用&#xff0c;但是整体还是C居多。现在项…

msvcr120.dll是干嘛的,找不到msvcr120.dll无法执行如何解决方法

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“msvcr120.dll丢失”。那么&#xff0c;msvcr120.dll到底是什么&#xff1f;它为什么会丢失&#xff1f;丢失后会对电脑产生什么影响&#xff1f;如何解决这个问题&#xff1f;本文将为您详…

纷享销客PaaS平台基础能力:一文说清 “业务定制能力”

01、业务对象定制能力 一个优秀的PaaS(平台即服务)平台的业务对象定制能力应该具备以下特点&#xff1a; 敏捷的业务模型&#xff1a; 能够根据用户的业务需求&#xff0c;提供可定制的数据模型和数据处理能力&#xff0c;支持各种数据类型和数据操作。 可视化的界面定制能力…

长短期记忆神经网络(LSTM)的回归预测(免费完整源代码)【MATLAB】

LSTM&#xff08;Long Short-Term Memory&#xff0c;长短期记忆网络&#xff09;是一种特殊类型的递归神经网络&#xff08;RNN&#xff09;&#xff0c;专门用于处理和预测基于时间序列的数据。与传统RNN相比&#xff0c;LSTM在处理长期依赖问题时具有显著优势。 LSTM的基本…

聊聊DoIP吧(三)-端口号port

DoIP在UDP和TCP建立连接和发送诊断报文的过程中使用的端口定义如下&#xff1a;

springboot宠物领养系统-计算机毕业设计源码07863

摘 要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存…

Linux C语言:字符串处理函数

一、字符串函数 1、C库中实现了很多字符串处理函数 #include <string.h> ① 求字符串长度的函数strlen② 字符串拷贝函数strcpy③ 字符串连接函数strcat④ 字符串比较函数strcmp 2、字符串长度函数strlen 格式&#xff1a;strlen(字符数组)功能&#xff1a;计算字符串…

LLM中表格处理与多模态表格理解

文档处理中不可避免的遇到表格&#xff0c;关于表格的处理问题&#xff0c;整理如下&#xff0c;供各位参考。 问题描述 RAG中&#xff0c;对上传文档完成版式处理后进行切片&#xff0c;切片前如果识别文档元素是表格&#xff0c;那么则需要对表格进行处理。一般而言&#x…

【UML用户指南】-17-对基本行为建模-交互

目录 1、消息的可视化表示 2、对象与角色 3、链和连接件 4、消息 5、序列 6、创建、修改和撤销 7、表示法 8、常用建模技术 8.1、对控制流建模 8.1.1、基于时间的控制流 8.1.2、基于结构的控制流 在任何有意义的系统中&#xff0c;对象都不是孤立存在的&#xff0c;…

SQL优化选对执行计划,查询速度提升1000倍 | OceanBase 应用实践

作者&#xff1a;爱可生数据库高级工程师任仲禹&#xff0c;擅长故障分析和性能优化。 本文通过一个案例&#xff0c;分享使用OceanBase时&#xff0c;SQL走错执行计划&#xff0c;而导致慢SQL的排查方法论。 案例背景 在使用OceanBase 3.2.3 版本的过程中&#xff0c;项目组反…

Spring的SmartLifecycle可以没用过,但没听过就不好了! - 第517篇

历史文章&#xff08;文章累计500&#xff09; 《国内最全的Spring Boot系列之一》 《国内最全的Spring Boot系列之二》 《国内最全的Spring Boot系列之三》 《国内最全的Spring Boot系列之四》 《国内最全的Spring Boot系列之五》 《国内最全的Spring Boot系列之六》 《…

HTML基础结构入门

HTML&#xff08;超文本标记语言&#xff09;是构建网页的基础语言。它用于描述网页的结构和内容。让我们从最基本的HTML文档开始。 HTML基础结构 一个基本的HTML文档结构如下&#xff1a; <!DOCTYPE html> <html lang"zh-CN"> <head><meta …

PMP证书有何用?

PMP证书有何用&#xff1f; PMP项目管理专业人士资格认证证书对从事或希望从事项目管理工作的人员有重要意义&#xff0c;具体体现在以下几个方面&#xff1a; 1. 提供职业机会&#xff1a; PMP是项目管理领域的国际认可标准&#xff0c;拥有该证书的人在求职时具备了更强的…

python-docx-template 的 Replace docx pictures 占位图片名称从哪来?

python-docx-template 的 Replace docx pictures 占位图片名称从哪来&#xff1f; 在 Word 中看占位图片名称用代码输出输出结果找对应图片 使用 replace_pic参考资料 在 Word 中看占位图片名称 右键图片 》查看可选文字 用代码输出 from docxtpl import DocxTemplate# 初始化…

【proteus仿真】基于51单片机的电压检测系统

【proteus仿真】基于51单片机的电压检测系统 资料下载地址&#xff1a;关注公众号 小邵爱电子 获取 1.前言 使用51单片机和ADC模块设计一个数字电压表&#xff0c;将模拟信号0~5V之间的电压转换为数字量信号&#xff0c;并通过LED实时显示电压数据 、 2.仿真原理图 3.硬件…

python安装目录文件说明----Dlls文件夹

在Python的安装目录下&#xff0c;通常会有一个DLLs文件夹&#xff0c;它是Python标准库的一部分。这个文件夹包含了一些动态链接库&#xff08;Dynamic Link Libraries&#xff0c;DLL&#xff09;&#xff0c;这些库提供了Python解释器和标准库的一些关键功能。以下是对这个文…

Linux(Centos7)OpenSSH漏洞修复,升级最新openssh-9.7p1

OpenSSH更新 一、OpenSSH漏洞二、安装zlib三、安装OpenSSL四、安装OpenSSH 一、OpenSSH漏洞 服务器被扫描出了漏洞需要修复&#xff0c;准备升级为最新openssh服务 1. 使用ssh -v查看本机ssh服务版本号 ssh -V虚拟机为OpenSSH7.4p1&#xff0c;现在准备升级为OpenSSH9.7p1…