注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
大家看到上一篇文章的示例会觉得代码结构混乱,使用起来很复杂,又要发送心跳,又要检查,客户端发送信息也不知道有没有连接上等等之类的。
为了使用起来更加方便,我们决定将连接封装起来(就像之前介绍reactor那样),由于要定义的变量多,为了代码看起来结构更加清晰。如果只用c的话,后面修改起来太复杂(虽然也可以实现),这里我选择使用C++的类,同时方便进行继承和扩展
一、封装三个类
1. KcpOpt 类
| 项目 | 说明 |
|---|---|
| 类名 | KcpOpt |
| 核心功能 | 存储和管理 KCP 协议的配置参数,作为 KCP 会话的 "配置选项容器" |
| 关键成员 / 参数 | - is_server:标识是否为服务器端 - server_keep_alive_timeout/client_keep_alive_timeout:服务器 / 客户端心跳超时时间(毫秒) - conv:KCP 会话唯一标识(32 位整数) - sndwnd/rcvwnd:KCP 发送 / 接收窗口大小 - nodelay/interval/resend/nc:KCP 底层参数(控制低延迟模式、超时重传间隔等) |
| 作用 | 集中管理 KCP 会话的初始化参数,为KcpSession提供配置依据,简化参数配置流程 |
2. UdpSocket 类
| 项目 | 说明 |
|---|---|
| 类名 | UdpSocket |
| 核心功能 | 封装 UDP 套接字的底层操作,提供 UDP 数据传输的基础能力 |
| 关键成员 / 方法 | - fd_:UDP 套接字文件描述符 - myaddr_:本地 IP 和端口信息(sockaddr_in) - SendTo(...):向指定目标地址发送 UDP 数据 - RecvFrom(...):从指定源地址接收 UDP 数据 - Bind():绑定本地 IP 和端口 - GetAddrString():获取本地地址的字符串表示(如 "192.168.248.130:8888") -SetNoblock():开启非阻塞模式 -SetBlock():关闭非阻塞模式 -IsBlock():判断是否为阻塞模式 |
| 作用 | 作为 KCP 协议的底层传输载体,屏蔽 UDP 套接字的系统调用细节(如socket/sendto/recvfrom),为KcpSession提供可靠的 UDP 数据收发能力 |
3. KcpSession 类
| 项目 | 说明 |
|---|---|
| 类名 | KcpSession |
| 核心功能 | 封装单个 KCP 会话的完整生命周期管理,实现 KCP 协议的核心逻辑 |
| 关键成员 / 方法 | - kcp_:KCP 协议实例(ikcpcb*) - socket_:关联的UdpSocket实例(用于 UDP 传输) - oppaddr_:对端 IP 和端口信息(sockaddr_in) - state_ : 判断连接状态 - Update(...):定时更新 KCP 状态,检测心跳超时(服务器端超时断开,客户端定时发心跳) - Send(...)/Recv(...):通过 KCP 发送 / 接收可靠数据(内部调用ikcp_send/ikcp_recv) -SendTo():发送UDP包 -Flush():对标ikcp_flush() - Input(...):将接收到的 UDP 数据注入 KCP 解析 - CheckSpecial(...):处理控制命令(如连接请求 / 响应、断开通知、心跳) -GetOppAddrToString():获取对方的ip和port字符串 -SetOppAddr():修改对方的addr(用于conv对应的addr改变时使用) - Connect(): 发起连接 |
| 作用 | 管理单个 KCP 会话的完整逻辑(协议交互、数据可靠传输、连接保活与断开),作为上层业务与底层 UDP/KCP 协议之间的桥梁 |
二、为什么这么设计?怎么想到的?
一句话从功能和简化代码出发:上面定义了三个类,事实上就只有一个KcpSession类,另外2个类是为KcpSession服务的。换句话说是在编写KcpSession过程中为了简化代码才定义了另外两个类!!!
从KcpSession的要实现的功能出发,我们就能理解为什么这么设计了:
-
为了方便的初始化KcpSession, 我们定义了KcpOpt类
-
为了实现UDP包的发送和接收,根据sendto和recvfrom所需的参数,我们知道KcpSession必须要有己方的UdpSocket_fd,和对方的addr,如果fd作为服务器需要绑定地址的话,我们还需知道己方的addr。
为了简化代码量:我们封装一个UdpSocket类,然后在KcpSession中定义一个指向UdpSocket类的指针,就可以获取所有关于己方socket的各种信息了,同时还能调用UDP的各种接口!!!
- 仿照上一篇博文的示例我们知道KcpSession必须实现数据的发送和读取,则不难想到:
Send: 对标ikcp_send
Recv: 对标ikcp_recv
Input : 对标ikcp_input
SendTo : 对标output中UDP包的发送 (output中传入的user必须知道己方fd和对方addr,KcpSession就能实现)
Update:对标ikcp_update, 同时我们还额外实现了心跳检测功能。
Flush: 对标ikcp_flush
CheckSpecial:用于检测的特殊命令,比如发起连接,同意连接,断开连接,心跳检测等,随自己拓展
Connect:客户端向服务器发起连接请求,接收到回应后,判定逻辑连接建立
总的来说:
| 类名 | 核心职责 | 遵循的设计原则 | 设计原因与具体思路 |
|---|---|---|---|
KcpOpt |
KCP 协议配置参数的集中管理 | 单一职责原则 | 1. KCP 依赖大量参数(如conv、窗口大小、超时时间等),分散管理会导致KcpSession职责过重,不符合单一职责; 2. 集中管理实现 "配置与逻辑分离",初始化KcpSession时只需传入KcpOpt对象,参数修改无需改动KcpSession核心逻辑; 3. 可作为配置模板复用,简化批量会话初始化流程。 |
UdpSocket |
UDP 底层传输操作的封装 | 封装性、低耦合 | 1. UDP 底层操作(如socket、sendto等)涉及系统 API,细节复杂且平台相关,直接嵌入KcpSession会导致职责混乱、耦合度高; 2. 封装为类后,向KcpSession提供简洁接口,隐藏底层细节,实现 "传输层与协议逻辑分离"; 3. 智能指针设计支持多会话共享 UDP 套接字,提升资源利用率。 |
KcpSession |
KCP 会话的全生命周期管理 | 高内聚、线程安全、单一职责 | 1. 集中处理 KCP 核心逻辑(数据可靠传输、心跳保活、连接状态维护),这些逻辑紧密关联,适合高内聚封装; 2. 封装ikcp库 API(如ikcp_send/ikcp_recv),降低上层使用门槛; 3. 通过Update方法收口定时维护逻辑,CheckSpecial处理控制命令,分离业务数据与控制交互; 4. 用mutex保证多线程安全,避免并发操作错乱。 |
三、代码
1. kcp_session.hpp
cpp
#ifndef __KCP_SESSION_HPP__
#define __KCP_SESSION_HPP__
#include "ikcp.h"
#include <cstdint>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <iostream>
#include <sstream>
#include <memory>
#include <unistd.h>
#include <cstring>
#include <unordered_map>
#include <mutex>
#include <thread>
#include <fcntl.h>
#define KEEP_ALIVE_CMD "KEEP_ALIVE_CMD"
#define CONNECTION_ASK "CONNECTION_ASK"
#define CONNECTION_RES "CONNECTION_RES"
#define CONNECTION_OFF "CONNECTION_OFF"
#define KCP_HEADER 24
#define CONNECT_TIME_OUT 1000000 // 1s中没连接上就判定超时
#define Server_ip "192.168.248.130"
#define Server_port 8888
#define BUFFER_LENGTH 1024
#define TRACE(...) trace("[Log: ", __func__, ":", __LINE__, "] :", __VA_ARGS__);
template <typename T, typename... Args>
inline void trace(T &&tmp, Args &&...args)
{
// constexpr if:编译期判断剩余参数个数,避免运行期分支开销
if constexpr (sizeof...(args) > 0)
{
// 若有剩余参数:打印当前参数+空格,递归处理下一个参数
std::cout << tmp << " ";
// 完美转发剩余参数(保持参数原始值类别,左值仍为左值,右值仍为右值)
trace(std::forward<Args>(args)...);
}
else
{
// 若无剩余参数:打印当前参数+换行(结束递归,避免末尾空格)
std::cout << tmp << std::endl;
}
}
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user);
class KcpOpt
{
public:
bool is_server = true;
int64_t server_keep_alive_timeout = 5000; // 定义64位才不会超出,注意,kcp中定义uint32_t,是因为取得是跟0比较的插值,我们这里取得是跟非0比较的插值
int64_t client_keep_alive_timeout = 4000; // 设置的比服务器快一点,防止延迟导致断开连接
uint32_t conv = 0;
int sndwnd = 32;
int rcvwnd = 128;
int nodelay = 0;
int interval = 10;
int resend = 0;
int nc = 0;
};
// 获取当前毫秒时间
int64_t current_ms()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
// 存放本端的地址
class UdpSocket
{
public:
using ptr = std::shared_ptr<UdpSocket>;
UdpSocket(const char *ip = Server_ip, uint16_t port = Server_port, int domain = AF_INET)
{
fd_ = socket(domain, SOCK_DGRAM, 0);
myaddr_.sin_family = AF_INET;
myaddr_.sin_addr.s_addr = inet_addr(ip);
myaddr_.sin_port = htons(port);
TRACE("UDPSocket");
}
~UdpSocket()
{
close(fd_);
}
int SendTo(const void *buffer, int len, sockaddr_in &oppaddr)
{
int ret = sendto(fd_, buffer, len, 0, (struct sockaddr *)&oppaddr, sizeof(struct sockaddr));
return ret;
}
int RecvFrom(void *buffer, int len, sockaddr_in &oppaddr)
{
socklen_t socklen = sizeof(oppaddr);
int ret = recvfrom(fd_, buffer, len, 0, (struct sockaddr *)&oppaddr, &socklen);
return ret;
}
int Bind()
{
bind(fd_, (struct sockaddr *)&myaddr_, sizeof(struct sockaddr));
return 0;
}
std::string GetAddrString()
{
std::stringstream ss;
ss << inet_ntoa(myaddr_.sin_addr) << ":" << ntohs(myaddr_.sin_port);
return ss.str(); // 返回拼接后的字符串
}
// 设置为非阻塞模式
bool SetNoblock()
{
// 1. 获取当前套接字的状态标志
int flag = fcntl(fd_, F_GETFL);
// 2. 设置非阻塞标志(O_NONBLOCK),保留原有其他标志
return fcntl(fd_, F_SETFL, flag | O_NONBLOCK) == 0;
}
// 设置为阻塞模式
bool SetBlock()
{
// 1. 获取当前套接字的状态标志(包含所有已设置的标志,如 O_NONBLOCK 等)
int flags = fcntl(fd_, F_GETFL);
if (flags == -1) // 获取标志失败(如 fd_ 无效)
{
return false;
}
// 2. 清除 O_NONBLOCK 标志(保留其他原有标志)
// 按位与 (~O_NONBLOCK) 表示"除了 O_NONBLOCK 之外的所有位都保留"
flags &= ~O_NONBLOCK;
// 3. 设置新的标志(此时已不含 O_NONBLOCK,即回到阻塞模式)
return fcntl(fd_, F_SETFL, flags) == 0;
}
bool IsBlock()
{
// 1. 获取 fd 的当前状态标志(F_GETFL 命令)
int flags = fcntl(fd_, F_GETFL);
if (flags == -1)
{
// 获取标志失败(可能 fd 无效,如已关闭或非法)
// 可根据需要处理错误,例如打印日志
// perror("fcntl F_GETFL failed");
return false; // 错误时返回 false(视为非阻塞或无效状态)
}
// 2. 检查是否包含 O_NONBLOCK 标志
// 按位与结果为 0 → 无 O_NONBLOCK → 阻塞模式
// 按位与结果非 0 → 有 O_NONBLOCK → 非阻塞模式
return (flags & O_NONBLOCK) == 0;
}
private:
int fd_;
sockaddr_in myaddr_;
};
// 这是一掉连接,它提供发送、接收、心跳监测,断开的工作
class KcpSession
{
public:
using Lock = std::unique_lock<std::mutex>;
using ptr = std::shared_ptr<KcpSession>;
KcpSession(const KcpOpt &opt, const sockaddr_in &oppaddr, UdpSocket::ptr ptr)
: opt_(opt), oppaddr_(oppaddr), socket_(ptr)
{
kcp_ = ikcp_create(opt_.conv, this);
kcp_->output = udp_output;
ikcp_nodelay(kcp_, opt_.nodelay, opt_.interval, opt_.resend, opt_.nc);
ikcp_wndsize(kcp_, opt_.sndwnd, opt_.rcvwnd);
}
~KcpSession()
{
ikcp_release(kcp_);
}
void Update(int64_t current)
{
{
// 必须加锁,Update是单独开一个线程执行的,多线程可能出现同时访问snd_buf的场景
Lock lock(mtx_);
ikcp_update(kcp_, current);
}
if (last_recv_ms == 0)
{
last_recv_ms = current;
}
// 心跳监测,控制连接状态
if (opt_.is_server && current - last_recv_ms > opt_.server_keep_alive_timeout)
{
TRACE("conv: ", opt_.conv, " timeout"); // 打印超时日志
state_ = 0;
}
if (!opt_.is_server && current - last_send_ms > opt_.client_keep_alive_timeout)
{
Send(KEEP_ALIVE_CMD, strlen(KEEP_ALIVE_CMD));
last_send_ms = current;
TRACE(KEEP_ALIVE_CMD, strlen(KEEP_ALIVE_CMD));
}
}
int SendTo(const void *buffer, int len)
{
int ret = socket_->SendTo(buffer, len, oppaddr_);
return ret;
}
bool Input(const char *buffer, long len)
{
Lock lock(mtx_);
int ret = ikcp_input(kcp_, buffer, len);
return ret;
}
int Recv(char *buffer, int len)
{
int ret = 0;
{
Lock lock(mtx_);
ret = ikcp_recv(kcp_, buffer, len);
}
if (ret > 0)
{
last_recv_ms = current_ms();
}
return ret;
}
// TRACE("Recv:");
int Send(const char *buffer, int len)
{
Lock lock(mtx_);
int ret = ikcp_send(kcp_, buffer, len);
// TRACE("SEND:", buffer);
return ret;
}
void Flush()
{
Lock lock(mtx_);
ikcp_flush(kcp_);
}
int Connect()
{
Send(CONNECTION_ASK, strlen(CONNECTION_ASK));
char buffer[BUFFER_LENGTH] = {0};
int64_t start = current_ms();
int flag = 0;
// 记录下当前连接是否为阻塞模式
if (socket_->IsBlock())
{
flag = 1;
}
// 这里使用非阻塞模式来遍历,防止阻塞
socket_->SetNoblock();
while (true)
{
int len = socket_->RecvFrom(buffer, BUFFER_LENGTH, oppaddr_);
Update(current_ms());
usleep(10000);
if (current_ms() - start > CONNECT_TIME_OUT)
{
TRACE("CONNECT_TIME_OUT");
// 恢复阻塞模式
if (flag == 1)
{
socket_->SetBlock();
}
return -1;
}
if (len < KCP_HEADER)
{
continue;
}
else
{
Input(buffer, len);
int recv_len = Recv(buffer, BUFFER_LENGTH);
if (recv_len <= 0)
continue;
buffer[recv_len] = '\0';
if (strcmp(buffer, CONNECTION_RES) == 0)
{
TRACE("connect to", GetOppAddrToString());
// 恢复阻塞模式
if (flag == 1)
{
socket_->SetBlock();
}
return 0;
}
}
}
}
void SetOppAddr(sockaddr_in oppaddr) { oppaddr_ = oppaddr; }
int Getconv() { return opt_.conv; }
bool CheckSpecial(char *buffer)
{
if (strcmp(buffer, KEEP_ALIVE_CMD) == 0)
{
last_recv_ms = current_ms();
TRACE("PING");
return true;
}
if (strcmp(buffer, CONNECTION_ASK) == 0)
{
TRACE("connected by", GetOppAddrToString());
Send(CONNECTION_RES, strlen(CONNECTION_RES));
return true;
}
if (strcmp(buffer, CONNECTION_OFF) == 0)
{
TRACE("disconnect to", GetOppAddrToString());
state_ = 0;
return true;
}
if (strcmp(buffer, CONNECTION_RES) == 0)
{
TRACE("connect to", GetOppAddrToString());
return true;
}
return false;
}
std::string GetOppAddrToString() const
{
std::stringstream ss;
ss << inet_ntoa(oppaddr_.sin_addr) << ":" << ntohs(oppaddr_.sin_port);
return ss.str();
}
UdpSocket::ptr GetSocket() { return socket_; }
bool state() const { return state_; };
private:
UdpSocket::ptr socket_;
ikcpcb *kcp_;
KcpOpt opt_;
sockaddr_in oppaddr_;
int64_t last_send_ms = 0; // 心跳发送的时间 KEEP_ALIVE_CMD (含别的信息接收时间)
int64_t last_recv_ms = 0; // 心跳发送的时间 KEEP_ALIVE_CMD (不含别的信息发送时间,无法确定update调用是否发送信息出去,所以不计算别的信息发送时间)
bool state_ = 1;
std::mutex mtx_;
};
// user必须携带对方的地址,
int udp_output(const char *buf, int len, ikcpcb *kcp, void *user)
{
KcpSession *session = (KcpSession *)user;
int ret = session->SendTo(buf, len);
return ret;
}
#endif
2. kcp_server.cpp
每次收到数据都根据conv去找对应的session 。(类似之前reactor介绍那里根据fd去找对应的conn)这里再次说明了KCP协议给每个对话都赋给conv,实现了逻辑层面上的连接。
cpp
#include "kcp_session.hpp"
UdpSocket::ptr server_socket = std::make_shared<UdpSocket>();
std::unordered_map<uint32_t, KcpSession::ptr> session_map;
std::mutex session_map_mutex; // 新增全局锁
KcpSession::ptr NewSession(uint32_t conv, const sockaddr_in &oppo_addr)
{
TRACE("NewSession");
KcpOpt opt;
opt.conv = conv;
KcpSession::ptr session = std::make_shared<KcpSession>(opt, oppo_addr, server_socket);
return session;
}
KcpSession::ptr GetSession(uint32_t conv, const sockaddr_in &oppo_addr)
{
std::lock_guard<std::mutex> lock(session_map_mutex);
auto it = session_map.find(conv);
if (it == session_map.end())
{
session_map[conv] = NewSession(conv, oppo_addr);
TRACE("conv: ", conv);
}
return session_map[conv];
}
int main()
{
server_socket->Bind();
char buffer[BUFFER_LENGTH] = {0};
std::thread t([&]()
{
while (true)
{
usleep(10000);
std::lock_guard<std::mutex> lock(session_map_mutex);
for(auto it = session_map.begin(); it != session_map.end(); )
{
it->second->Update(current_ms());
if (it->second->state() == 0)
{
it->second->Send(CONNECTION_OFF, strlen(CONNECTION_OFF));
it->second->Flush();
it = session_map.erase(it);
}
else
{
it++;
}
}
} });
while (1)
{
sockaddr_in client_addr;
int len = server_socket->RecvFrom(buffer, BUFFER_LENGTH, client_addr);
if (len < KCP_HEADER)
{
continue;
}
else
{
uint32_t conv = ikcp_getconv(buffer);
KcpSession::ptr session = GetSession(conv, client_addr);
session->Input(buffer, len);
int recv_len;
recv_len = session->Recv(buffer, BUFFER_LENGTH);
if (recv_len <= 0) continue;
buffer[recv_len] = '\0';
if (!session->CheckSpecial(buffer))
{
std::cout << "[Recv from " << session->GetOppAddrToString() << "]: ";
std::cout << buffer << std::endl;
session->Send(buffer, recv_len);
}
}
}
}
2. kcp_client.cpp
我们采用UUID算法来生成conv,避免服务器端在客户端连接多的时候出现重复的conv
cpp
#include "kcp_session.hpp"
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
uint32_t GenerateUniqueConv()
{
// 1. 生成 128 位随机 UUID(版本 4,基于密码学安全随机数)
boost::uuids::uuid uuid = boost::uuids::random_generator()();
// 2. 获取 UUID 的原始字节数据(16 字节,uint8_t[16],与 uuid.data() 返回类型一致)
const uint8_t *uuid_bytes = uuid.data;
uint32_t conv = 0;
// 3. 按"4 字节一组"拆分 16 字节 UUID,通过异或累积为 32 位整数
for (int i = 0; i < 16; i += 4)
{
uint32_t chunk = 0;
// 安全复制 4 字节(UUID 共 16 字节,i 取 0/4/8/12,不会越界)
std::memcpy(&chunk, &uuid_bytes[i], sizeof(chunk));
conv ^= chunk; // 异或操作分散随机性,降低碰撞概率
}
return conv;
}
int main()
{
KcpOpt opt;
opt.is_server = false;
opt.conv = GenerateUniqueConv();
UdpSocket::ptr client_socket = std::make_shared<UdpSocket>();
sockaddr_in server_addr;
server_addr.sin_addr.s_addr = inet_addr(Server_ip);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(Server_port);
KcpSession::ptr session = std::make_shared<KcpSession>(opt, server_addr, client_socket);
char send_buf[BUFFER_LENGTH + 1] = {0};
char recv_buf[BUFFER_LENGTH + 1] = {0};
int ret = session->Connect();
if (ret == -1)
{
return -1;
}
std::thread t1([&]()
{
while (true)
{
usleep(10000);
session->Update(current_ms());
if (session->state() == 0)
{
std::cout << "server disconnect" << std::endl;
return 0;
}
} });
std::thread t2([&]()
{
while (true)
{
int len = client_socket->RecvFrom(recv_buf, BUFFER_LENGTH, server_addr);
if (len < KCP_HEADER)
{
continue;
}
else
{
session->Input(recv_buf, len);
int recv_len = session->Recv(recv_buf, BUFFER_LENGTH);
if(recv_len <= 0) continue;
recv_buf[recv_len] = '\0';
if (!session->CheckSpecial(recv_buf))
{
std::cout << "[Recv from " << session->GetOppAddrToString() << "]: ";
std::cout << recv_buf << std::endl;
}
}
} });
while (1)
{
fgets(send_buf, BUFFER_LENGTH, stdin); // 遇到回车结束,将末尾置为'\0'
send_buf[strcspn(send_buf, "\n")] = '\0';
int send_len = strlen(send_buf);
session->Send(send_buf, send_len);
}
}
4. 测试

测试成功:
客户端发情连接请求,服务端回应,逻辑连接建立成功!!!
客户端定时发送心跳检测,同时客户端主动发送消息,服务端会会主动回显。当客户端异常断开时,服务端检测到,及时清楚逻辑连接。
四、代码总结
1. 断开之后的处理方式由用户自定义
事实上你会发现当连接断开时(state = 0 ),并没有做具体的处理。而是交给用户层去做。
这样使得库的使用性更加广泛,毕竟连接的断开(以上述为例)客户端和服务端的处理逻辑是不同的。
2. KCP协议的使用方式(引入KcpSession后):
2.1 服务端:
建立连接表:unordered_map ( conv --> KcpSession::ptr)
初始化 UdpSocket::ptr
单开线程:遍历map执行, session->Update,同时根据返回值执行具体的断开操作
建立 clientaddr
开始接收数据
获取包的conv --> 根据map去找对应的session、没有则创建
处理数据,利用session精确找到对方,然后回数据
2.2 客户端:
初始化 UdpSocket::ptr
初始化KcpOpt(conv,is_sever)
初始化 KcpSession::ptr
调用Connect:
发送建立连接请求:CONNECTION_ASK
收到回复:CONNECTION_RES
判定逻辑连接建立成功!!!
单开线程:session->Update,同时根据state = 0 时执行具体的断开操作
单开线程:接收数据
单开线程:发送数据
3. 在自定义KcpSession后代码逻辑更加清晰简单了
你会发现,现在用户使用KCP,只需单开一个线程执行Update(相当于内核,在管理连接状态,同时定期发送消息)。然后判断state的值,如果为0就是断开的了,我们执行断开的后续处理。
而用户发数据只需调用Send,收数据只需调用RecvFrom,然后执行Input(相当于将数据包给内核解析),然后再调用Recv就可以收到消息了!!!
相比于之前没有封装,现在使用KCP协议的代码量大大减少了,使用起来更加简单顺手,几乎跟UDP、TCP的使用一个样!!!而且由于封装了锁,收发数据可以正常用于多线程。