Linux 多线程 TCP 服务端与客户端:使用 C++ std::thread 实现
std::thread 传参:彻底解决了 pthread 的"悬空指针"问题
一、先回顾 pthread 的那个"坑"
在 pthread 中,线程传参的典型错误是这样的:
cpp
// ❌ pthread 经典错误
for (int i = 0; i < 3; ++i) {
pthread_t tid;
// 传的是 i 的地址!
pthread_create(&tid, nullptr, run_client, &i);
}
执行时序:
主线程 新线程
│ │
│ i = 0 │
│ pthread_create(&i) ────────► │ 准备读 *(int*)arg
│ │
│ i = 1 ← 主线程继续循环 │ 读到的值已经是 1 了!
│ 修改了 i! │
│ │
│ i = 2 ← 又改了! │ 读到的值变成 2 了!
│ │
│ 循环结束 i = 3 │ 读到的值变成 3 了!
结果:三个线程可能全部读到同一个值,而且行为不确定。
pthread 的正确做法必须堆分配:
cpp
// ✅ pthread 正确做法
for (int i = 0; i < 3; ++i) {
int* idx = new int(i); // 每个线程独立的堆内存
pthread_create(&tid, nullptr, run_client, idx);
}
// 线程函数内部
void* run_client(void* arg) {
int index = *(int*)arg;
delete (int*)arg; // 用完必须手动释放
...
}
这样做的原因:
new int(0) ──► 堆地址 A ──► 线程1 读 A ──► 值 = 0 ✓
new int(1) ──► 堆地址 B ──► 线程2 读 B ──► 值 = 1 ✓
new int(2) ──► 堆地址 C ──► 线程3 读 C ──► 值 = 2 ✓
每个地址独立,主线程修改 i 不影响堆上的值
二、std::thread 为什么不需要担心这个问题
2.1 核心机制:参数会被拷贝/移动到线程内部存储
std::thread 构造函数的行为等价于:
cpp
// 你写的代码
std::thread t(func, arg1, arg2, arg3);
标准库内部做的事情:
std::thread 构造函数
│
├── 1. 把 func 拷贝/移动到内部存储
│
├── 2. 把 arg1 拷贝/移动到内部存储(decay-copy)
│
├── 3. 把 arg2 拷贝/移动到内部存储(decay-copy)
│
├── 4. 把 arg3 拷贝/移动到内部存储(decay-copy)
│
└── 5. 在新线程中调用 func(arg1_copy, arg2_copy, arg3_copy)
标准原文的语义是:构造函数以 std::forward<Args>(args)... 的方式将参数**衰减复制(decay-copy)**到线程对象的内部存储中,新线程启动后使用这些副本调用可调用对象。
2.2 对比图
pthread: std::thread:
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
int* p = new int(i); std::thread t(run_client, i);
pthread_create(&tid, │
nullptr, ├── 内部拷贝一份 i=0
run_client, │ (独立存储)
p); │
} std::thread t(run_client, i);
│
主线程修改 i ──► 堆上的 *p 不受影响 ├── 内部拷贝一份 i=1
│ (独立存储)
│
std::thread t(run_client, i);
│
└── 内部拷贝一份 i=2
(独立存储)
}
主线程修改 i ──► 内部副本不受影响
✅ 安全 ✅ 安全
2.3 具体例子
cpp
void run_client(int index);
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i) {
// 直接传 i,std::thread 会拷贝一份 i 的值
threads.emplace_back(run_client, i);
}
// 这里 i 变成 3,但线程内部各自保存的是 0, 1, 2
// 完全不受影响!
for (auto& t : threads) t.join();
}
执行过程:
循环第 0 轮:
i = 0
emplace_back(run_client, 0)
│
└── 线程内部存储:index = 0(拷贝)
循环第 1 轮:
i = 1(主线程的 i 变了)
emplace_back(run_client, 1)
│
└── 线程内部存储:index = 1(拷贝)
└── 第 0 轮的线程仍然持有 index = 0 ✓
循环第 2 轮:
i = 2(主线程的 i 又变了)
emplace_back(run_client, 2)
│
└── 线程内部存储:index = 2(拷贝)
└── 第 0 轮:index = 0 ✓
└── 第 1 轮:index = 1 ✓
三、但仍然有陷阱!引用和指针要注意
3.1 传引用:必须用 std::ref
std::thread 默认是按值拷贝,即使线程函数声明了引用参数:
cpp
void add_one(int& value) {
++value;
}
int main() {
int value = 10;
// ❌ 错误!value 会被拷贝,线程修改的是副本
std::thread t(add_one, value);
t.join();
cout << value; // 输出 10,不是 11!
}
执行过程:
value = 10(主线程)
│
├── 拷贝到线程内部:value_copy = 10
│
│ 线程执行 add_one(value_copy)
│ value_copy 变成 11
│
│ 但主线程的 value 仍然是 10
│
▼
输出:10
正确写法:
cpp
// ✅ 用 std::ref 显式传递引用
std::thread t(add_one, std::ref(value));
t.join();
cout << value; // 输出 11 ✓
但此时就要面对和 pthread 同样的生命周期问题:
cpp
// ❌ 危险!悬空引用
std::thread create_thread() {
int value = 10;
std::thread t(add_one, std::ref(value));
return t;
// value 在这里被销毁!
// 线程还在运行,但引用已经悬空了!
}
create_thread()
│
├── int value = 10(栈上)
│
├── std::thread t(..., std::ref(value))
│ │
│ └── 内部存储:指向 value 的引用
│
├── return t;
│ │
│ └── value 被销毁!
│
▼
线程执行 add_one(悬空引用) ──► 未定义行为!
3.2 传指针:同样要注意指向对象的生命周期
cpp
// ❌ 危险
for (int i = 0; i < 3; ++i) {
// 传的是 i 的指针!虽然 std::thread 会拷贝这个指针值,
// 但指针指向的 i 是同一个变量!
std::thread t(run_client_ptr, &i);
threads.push_back(std::move(t));
}
主线程 i 的地址: 0x7ffd00100
│
├── 线程1 内部存储:ptr = 0x7ffd00100(指针被拷贝了,但指向同一个 i)
├── 线程2 内部存储:ptr = 0x7ffd00100(同上)
├── 线程3 内部存储:ptr = 0x7ffd00100(同上)
│
│ 主线程继续修改 i ──► 三个线程通过指针读到的值不确定!
正确写法:
cpp
// ✅ 按值传,让 std::thread 拷贝值
std::thread t(run_client, i);
3.3 Lambda 捕获同理
cpp
// ❌ 危险:按引用捕获局部变量
for (int i = 0; i < 3; ++i) {
std::thread t([&i]() { // &i = 引用捕获
cout << i << endl; // i 可能被主线程修改
});
threads.push_back(std::move(t));
}
// ✅ 安全:按值捕获
for (int i = 0; i < 3; ++i) {
std::thread t([i]() { // i = 值捕获(拷贝)
cout << i << endl; // 每个线程有自己的 i 副本
});
threads.push_back(std::move(t));
}
四、总结:一张表说清楚
┌──────────────────────────────────────────────────────────────────┐
│ pthread 传参 vs std::thread 传参 │
├──────────────────────┬───────────────────┬───────────────────────┤
│ 场景 │ pthread │ std::thread │
├──────────────────────┼───────────────────┼───────────────────────┤
│ 传 int 值 │ 必须 new+指针 │ 直接传,自动拷贝 ✓ │
│ 循环变量 i │ &i 会出错 │ 直接传 i,安全 ✓ │
│ 传结构体 │ 必须 new+指针 │ 直接传,自动拷贝 ✓ │
│ 传引用 │ 天然就是指针 │ 必须 std::ref() │
│ 传指针 │ 天然就是指针 │ 指针被拷贝,指向不变 │
│ Lambda 捕获 │ 不支持 Lambda │ 按值捕获 = 安全 │
│ 手动释放内存 │ 必须 delete │ 不需要 ✓ │
│ 忘记释放 = 内存泄漏 │ 容易出错 │ 不存在此问题 ✓ │
│ 类型安全 │ void* 无类型检查 │ 编译期类型检查 ✓ │
└──────────────────────┴───────────────────┴───────────────────────┘
五、结论
✅ std::thread 不需要担心 pthread 那个"子线程读到被主线程修改的值"的问题
原因:
pthread:
传的是指针 ──► 主线程和新线程共享同一块内存
主线程改了,新线程就看到变化
std::thread:
传的是值 ──► std::thread 构造函数会做 decay-copy
新线程拥有独立的副本
主线程怎么改都不影响新线程
⚠️ 但仍然需要注意
| 场景 | 是否安全 | 原因 |
|---|---|---|
std::thread(f, i) |
✅ 安全 | 自动拷贝 i 的值 |
std::thread(f, &i) |
⚠️ 小心 | 指针被拷贝,但仍指向同一个 i |
std::thread(f, std::ref(i)) |
⚠️ 小心 | 传了引用,要保证 i 的生命周期 |
[&i](){...} Lambda |
⚠️ 小心 | 引用捕获,i 可能被提前销毁 |
[i](){...} Lambda |
✅ 安全 | 值捕获,Lambda 内部有独立副本 |
一句话总结 :std::thread 默认按值拷贝传参,天然解决了 pthread 的"循环变量被篡改"问题。只有当你主动选择 传引用/传指针/Lambda 引用捕获时,才需要关心生命周期------而这不是 std::thread 的问题,而是引用/指针本身的问题。
一、实现目标
上一版使用 POSIX pthread:
cpp
pthread_create();
pthread_join();
pthread_detach();
pthread_mutex_lock();
pthread_mutex_unlock();
这一版改用 C++ 标准库:
cpp
std::thread;
std::thread::join();
std::thread::detach();
std::mutex;
std::lock_guard;
std::this_thread::get_id();
std::this_thread::sleep_for();
需要先明确:
std::thread只能替换线程管理接口,不能替换 Linux Socket API。
因此下面这些网络函数仍然来自 POSIX:
cpp
socket();
bind();
listen();
accept();
connect();
send();
recv();
close();
C++ 标准库规定了 std::thread、join()、detach()、std::mutex 等统一接口,但底层原生线程句柄属于实现定义内容。
整体替换关系如下:
text
pthread C++ 标准库
──────────────────────────────────────────────
pthread_create() → std::thread
pthread_join() → thread.join()
pthread_detach() → thread.detach()
pthread_self() → std::this_thread::get_id()
pthread_mutex_t → std::mutex
pthread_mutex_lock() → mutex.lock()
pthread_mutex_unlock() → mutex.unlock()
手工加锁和解锁 → std::lock_guard
sleep()/usleep() → std::this_thread::sleep_for()
二、C++ 标准库多线程服务端
2.1 服务端并发模型
服务端仍然采用"一连接一线程"模型:
text
主线程
│
├── accept() ──► cfd=4 ──► std::thread 工作线程1
│
├── accept() ──► cfd=5 ──► std::thread 工作线程2
│
└── accept() ──► cfd=6 ──► std::thread 工作线程3
主线程只负责:
text
socket
│
bind
│
listen
│
accept
│
创建 std::thread
│
继续 accept
每个工作线程负责:
text
接管 cfd
│
recv
│
处理业务
│
send
│
close(cfd)
│
线程结束
2.2 为什么增加 UniqueFd
原来的代码需要手工关闭文件描述符:
cpp
close(cfd);
如果中间有多个错误返回路径:
cpp
if (error1) {
close(cfd);
return;
}
if (error2) {
close(cfd);
return;
}
很容易在某个分支中遗漏 close()。
因此可以使用 RAII 封装:
cpp
class UniqueFd {
public:
explicit UniqueFd(int fd = -1) noexcept
: fd_(fd) {
}
~UniqueFd() {
reset();
}
private:
int fd_;
};
对象离开作用域时,析构函数自动关闭文件描述符:
text
handle_client() 开始
│
▼
UniqueFd cfd(raw_cfd)
│
▼
执行 recv/send
│
▼
函数 return 或正常结束
│
▼
UniqueFd 析构
│
▼
close(cfd)
这样可以减少资源泄漏。
三、完整的 std::thread 服务端代码
cpp
// server_std_thread.cpp
#include <arpa/inet.h>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <netinet/in.h>
#include <sstream>
#include <string>
#include <system_error>
#include <thread>
#include <sys/socket.h>
#include <unistd.h>
// ============================================================
// 文件描述符 RAII 封装
// ============================================================
class UniqueFd {
public:
explicit UniqueFd(int fd = -1) noexcept
: fd_(fd) {
}
~UniqueFd() {
reset();
}
// 文件描述符不能被普通复制。
UniqueFd(const UniqueFd&) = delete;
UniqueFd& operator=(const UniqueFd&) = delete;
// 允许移动所有权。
UniqueFd(UniqueFd&& other) noexcept
: fd_(other.release()) {
}
UniqueFd& operator=(UniqueFd&& other) noexcept {
if (this != &other) {
reset(other.release());
}
return *this;
}
int get() const noexcept {
return fd_;
}
explicit operator bool() const noexcept {
return fd_ != -1;
}
int release() noexcept {
int old_fd = fd_;
fd_ = -1;
return old_fd;
}
void reset(int new_fd = -1) noexcept {
if (fd_ != -1) {
::close(fd_);
}
fd_ = new_fd;
}
private:
int fd_;
};
// 保护服务端日志。
std::mutex g_log_mutex;
std::string get_timestamp();
void log_message(const std::string& message);
ssize_t send_all(
int fd,
const void* data,
std::size_t len
);
ssize_t recv_line(
int fd,
std::string& out,
std::size_t max_len = 4096
);
void handle_client(
int raw_cfd,
sockaddr_in client_addr
);
// ============================================================
// 主函数
// ============================================================
int main() {
// --------------------------------------------------------
// 1. 创建监听套接字
// --------------------------------------------------------
UniqueFd lfd(
::socket(
AF_INET,
SOCK_STREAM,
0
)
);
if (!lfd) {
std::cerr
<< "socket() failed: "
<< std::strerror(errno)
<< '\n';
return 1;
}
// --------------------------------------------------------
// 2. 设置地址复用
// --------------------------------------------------------
int opt = 1;
if (::setsockopt(
lfd.get(),
SOL_SOCKET,
SO_REUSEADDR,
&opt,
sizeof(opt)) == -1) {
std::cerr
<< "setsockopt() failed: "
<< std::strerror(errno)
<< '\n';
return 1;
}
// --------------------------------------------------------
// 3. 设置服务器地址
// --------------------------------------------------------
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(8888);
// --------------------------------------------------------
// 4. 绑定
// --------------------------------------------------------
if (::bind(
lfd.get(),
reinterpret_cast<sockaddr*>(&server_addr),
sizeof(server_addr)) == -1) {
std::cerr
<< "bind() failed: "
<< std::strerror(errno)
<< '\n';
return 1;
}
// --------------------------------------------------------
// 5. 监听
// --------------------------------------------------------
if (::listen(lfd.get(), 128) == -1) {
std::cerr
<< "listen() failed: "
<< std::strerror(errno)
<< '\n';
return 1;
}
std::cout
<< "std::thread server listening on "
<< "0.0.0.0:8888"
<< ", PID="
<< ::getpid()
<< '\n';
// --------------------------------------------------------
// 6. 循环接收客户端
// --------------------------------------------------------
while (true) {
sockaddr_in client_addr{};
// accept() 可能修改长度值,
// 因此每轮都要重新初始化。
socklen_t client_len =
sizeof(client_addr);
int cfd = ::accept(
lfd.get(),
reinterpret_cast<sockaddr*>(&client_addr),
&client_len
);
if (cfd == -1) {
if (errno == EINTR ||
errno == ECONNABORTED) {
continue;
}
std::cerr
<< "accept() failed: "
<< std::strerror(errno)
<< '\n';
continue;
}
// ----------------------------------------------------
// 7. 创建工作线程
// ----------------------------------------------------
try {
std::thread worker(
[cfd, client_addr]() {
handle_client(
cfd,
client_addr
);
}
);
// 一连接一线程服务端不需要等待每一个工作线程,
// 因此将线程设置为分离状态。
worker.detach();
// detach() 之后:
//
// 1. worker 对象不再代表该执行线程;
// 2. 工作线程仍然继续运行;
// 3. 工作线程结束后资源自动释放;
// 4. 不能再调用 worker.join()。
} catch (const std::system_error& e) {
// std::thread 创建线程失败时可能抛出
// std::system_error。
std::cerr
<< "std::thread creation failed: "
<< e.what()
<< '\n';
// 创建失败,cfd 没有交给工作线程,
// 必须由主线程关闭。
::close(cfd);
}
}
return 0;
}
// ============================================================
// 工作线程处理函数
// ============================================================
void handle_client(
int raw_cfd,
sockaddr_in client_addr
) {
// 工作线程取得 cfd 所有权。
//
// 函数结束时,UniqueFd 自动 close(cfd)。
UniqueFd cfd(raw_cfd);
char ip[INET_ADDRSTRLEN]{};
if (::inet_ntop(
AF_INET,
&client_addr.sin_addr,
ip,
sizeof(ip)) == nullptr) {
std::strncpy(
ip,
"<invalid-ip>",
sizeof(ip) - 1
);
}
std::uint16_t port =
ntohs(client_addr.sin_port);
// std::thread::id 是 C++ 标准库线程标识,
// 不保证等于 Linux 内核 TID。
std::thread::id thread_id =
std::this_thread::get_id();
{
std::ostringstream oss;
oss
<< '\n'
<< '['
<< get_timestamp()
<< "] NEW CONNECTION\n"
<< " Process PID : "
<< ::getpid()
<< '\n'
<< " std::thread ID : "
<< thread_id
<< '\n'
<< " Client : "
<< ip
<< ':'
<< port
<< '\n'
<< " cfd : "
<< cfd.get()
<< "\n\n";
log_message(oss.str());
}
// --------------------------------------------------------
// 读取客户端请求
// --------------------------------------------------------
std::string request;
ssize_t received =
recv_line(
cfd.get(),
request
);
if (received > 0) {
{
std::ostringstream oss;
oss
<< '['
<< get_timestamp()
<< "] [thread "
<< thread_id
<< "] received "
<< received
<< " bytes: "
<< request
<< '\n';
log_message(oss.str());
}
// 模拟耗时业务。
std::this_thread::sleep_for(
std::chrono::seconds(1)
);
const std::string response =
"Hello from std::thread server\n";
ssize_t sent = send_all(
cfd.get(),
response.data(),
response.size()
);
std::ostringstream oss;
if (sent == -1) {
oss
<< '['
<< get_timestamp()
<< "] [thread "
<< thread_id
<< "] send failed: "
<< std::strerror(errno)
<< '\n';
} else {
oss
<< '['
<< get_timestamp()
<< "] [thread "
<< thread_id
<< "] sent "
<< sent
<< " bytes\n";
}
log_message(oss.str());
} else if (received == 0) {
std::ostringstream oss;
oss
<< '['
<< get_timestamp()
<< "] [thread "
<< thread_id
<< "] client closed without data\n";
log_message(oss.str());
} else {
std::ostringstream oss;
oss
<< '['
<< get_timestamp()
<< "] [thread "
<< thread_id
<< "] recv failed: "
<< std::strerror(errno)
<< '\n';
log_message(oss.str());
}
{
std::ostringstream oss;
oss
<< '['
<< get_timestamp()
<< "] [thread "
<< thread_id
<< "] closing cfd "
<< cfd.get()
<< '\n';
log_message(oss.str());
}
// 不需要手工 close(cfd.get())。
//
// handle_client() 返回后:
//
// UniqueFd::~UniqueFd()
// │
// ▼
// close(cfd)
}
// ============================================================
// 完整发送
// ============================================================
ssize_t send_all(
int fd,
const void* data,
std::size_t len
) {
const char* p =
static_cast<const char*>(data);
std::size_t total = 0;
while (total < len) {
ssize_t n = ::send(
fd,
p + total,
len - total,
MSG_NOSIGNAL
);
if (n > 0) {
total +=
static_cast<std::size_t>(n);
continue;
}
if (n == -1 && errno == EINTR) {
continue;
}
return -1;
}
return static_cast<ssize_t>(total);
}
// ============================================================
// 按行接收
// ============================================================
ssize_t recv_line(
int fd,
std::string& out,
std::size_t max_len
) {
out.clear();
while (out.size() < max_len) {
char ch = '\0';
ssize_t n = ::recv(
fd,
&ch,
1,
0
);
if (n > 0) {
if (ch == '\n') {
return static_cast<ssize_t>(
out.size()
);
}
out.push_back(ch);
continue;
}
if (n == 0) {
return static_cast<ssize_t>(
out.size()
);
}
if (errno == EINTR) {
continue;
}
return -1;
}
errno = EMSGSIZE;
return -1;
}
// ============================================================
// 日志输出
// ============================================================
void log_message(
const std::string& message
) {
// lock_guard 构造时自动加锁,
// 析构时自动解锁。
std::lock_guard<std::mutex> lock(
g_log_mutex
);
std::cout
<< message
<< std::flush;
}
// ============================================================
// 获取毫秒时间戳
// ============================================================
std::string get_timestamp() {
auto now =
std::chrono::system_clock::now();
std::time_t current =
std::chrono::system_clock::to_time_t(
now
);
auto ms =
std::chrono::duration_cast<
std::chrono::milliseconds
>(
now.time_since_epoch()
) % 1000;
std::tm local_tm{};
// Linux 可重入版本。
localtime_r(
¤t,
&local_tm
);
std::ostringstream oss;
oss
<< std::put_time(
&local_tm,
"%H:%M:%S"
)
<< '.'
<< std::setfill('0')
<< std::setw(3)
<< ms.count();
return oss.str();
}
四、C++ 标准库多线程客户端
客户端创建三个 std::thread:
text
客户端主线程
│
├── std::thread(run_client, 0)
├── std::thread(run_client, 1)
└── std::thread(run_client, 2)
│
▼
分别建立三个 TCP 连接
客户端不能直接 detach() 后退出,因为主线程退出将导致整个进程结束。
因此客户端使用:
cpp
thread.join();
等待所有客户端线程完成。
五、完整的 std::thread 客户端代码
cpp
// client_std_thread.cpp
#include <arpa/inet.h>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <iostream>
#include <mutex>
#include <netinet/in.h>
#include <sstream>
#include <string>
#include <system_error>
#include <thread>
#include <vector>
#include <sys/socket.h>
#include <unistd.h>
// ============================================================
// 文件描述符 RAII
// ============================================================
class UniqueFd {
public:
explicit UniqueFd(int fd = -1) noexcept
: fd_(fd) {
}
~UniqueFd() {
reset();
}
UniqueFd(const UniqueFd&) = delete;
UniqueFd& operator=(const UniqueFd&) = delete;
UniqueFd(UniqueFd&& other) noexcept
: fd_(other.release()) {
}
UniqueFd& operator=(UniqueFd&& other) noexcept {
if (this != &other) {
reset(other.release());
}
return *this;
}
int get() const noexcept {
return fd_;
}
explicit operator bool() const noexcept {
return fd_ != -1;
}
int release() noexcept {
int old_fd = fd_;
fd_ = -1;
return old_fd;
}
void reset(int new_fd = -1) noexcept {
if (fd_ != -1) {
::close(fd_);
}
fd_ = new_fd;
}
private:
int fd_;
};
std::mutex g_log_mutex;
void log_message(
const std::string& message
);
ssize_t send_all(
int fd,
const void* data,
std::size_t len
);
ssize_t recv_line(
int fd,
std::string& out,
std::size_t max_len = 4096
);
void run_client(int index);
// ============================================================
// 主函数
// ============================================================
int main() {
constexpr int kClientNum = 3;
std::cout
<< "=== Concurrent std::thread clients: "
<< kClientNum
<< " ===\n";
// 保存所有客户端线程对象。
std::vector<std::thread> threads;
// 提前预留空间,避免反复扩容。
threads.reserve(kClientNum);
// --------------------------------------------------------
// 创建线程
// --------------------------------------------------------
for (int i = 0; i < kClientNum; ++i) {
try {
// 参数 i 按值传递。
//
// 每个线程分别得到自己的:
//
// 0
// 1
// 2
//
// 不需要 void*,不需要 new/delete,
// 也不需要强制类型转换。
threads.emplace_back(
run_client,
i
);
} catch (const std::system_error& e) {
std::cerr
<< "Creating client thread "
<< i
<< " failed: "
<< e.what()
<< '\n';
}
}
// --------------------------------------------------------
// 等待线程结束
// --------------------------------------------------------
for (std::thread& thread : threads) {
if (thread.joinable()) {
thread.join();
}
}
std::cout
<< "=== All clients finished ===\n";
return 0;
}
// ============================================================
// 客户端线程函数
// ============================================================
void run_client(int index) {
// 制造少量错峰,方便观察并发。
std::this_thread::sleep_for(
std::chrono::milliseconds(
(index % 2) * 200
)
);
// --------------------------------------------------------
// 1. 创建套接字
// --------------------------------------------------------
UniqueFd fd(
::socket(
AF_INET,
SOCK_STREAM,
0
)
);
if (!fd) {
std::ostringstream oss;
oss
<< "Client "
<< index
<< " socket() failed: "
<< std::strerror(errno)
<< '\n';
log_message(oss.str());
return;
}
// --------------------------------------------------------
// 2. 设置服务器地址
// --------------------------------------------------------
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
if (::inet_pton(
AF_INET,
"127.0.0.1",
&server_addr.sin_addr) != 1) {
log_message(
"inet_pton() failed\n"
);
return;
}
// --------------------------------------------------------
// 3. 连接服务器
// --------------------------------------------------------
if (::connect(
fd.get(),
reinterpret_cast<sockaddr*>(
&server_addr
),
sizeof(server_addr)) == -1) {
std::ostringstream oss;
oss
<< "Client "
<< index
<< " connect() failed: "
<< std::strerror(errno)
<< '\n';
log_message(oss.str());
return;
}
std::thread::id thread_id =
std::this_thread::get_id();
{
std::ostringstream oss;
oss
<< "Client "
<< index
<< " connected: PID="
<< ::getpid()
<< ", std::thread ID="
<< thread_id
<< ", fd="
<< fd.get()
<< '\n';
log_message(oss.str());
}
// --------------------------------------------------------
// 4. 发送请求
// --------------------------------------------------------
std::string request =
"Hello from std::thread client "
+ std::to_string(index)
+ "\n";
if (send_all(
fd.get(),
request.data(),
request.size()) == -1) {
std::ostringstream oss;
oss
<< "Client "
<< index
<< " send failed: "
<< std::strerror(errno)
<< '\n';
log_message(oss.str());
return;
}
{
std::ostringstream oss;
oss
<< "Client "
<< index
<< " sent request\n";
log_message(oss.str());
}
// --------------------------------------------------------
// 5. 接收响应
// --------------------------------------------------------
std::string response;
ssize_t n = recv_line(
fd.get(),
response
);
std::ostringstream oss;
if (n > 0) {
oss
<< "Client "
<< index
<< " received: "
<< response
<< '\n';
} else if (n == 0) {
oss
<< "Client "
<< index
<< " peer closed without response\n";
} else {
oss
<< "Client "
<< index
<< " recv failed: "
<< std::strerror(errno)
<< '\n';
}
log_message(oss.str());
// run_client() 返回时:
//
// UniqueFd::~UniqueFd()
// │
// ▼
// close(fd)
}
// ============================================================
// 完整发送
// ============================================================
ssize_t send_all(
int fd,
const void* data,
std::size_t len
) {
const char* p =
static_cast<const char*>(data);
std::size_t total = 0;
while (total < len) {
ssize_t n = ::send(
fd,
p + total,
len - total,
MSG_NOSIGNAL
);
if (n > 0) {
total +=
static_cast<std::size_t>(n);
continue;
}
if (n == -1 && errno == EINTR) {
continue;
}
return -1;
}
return static_cast<ssize_t>(total);
}
// ============================================================
// 按行接收
// ============================================================
ssize_t recv_line(
int fd,
std::string& out,
std::size_t max_len
) {
out.clear();
while (out.size() < max_len) {
char ch = '\0';
ssize_t n = ::recv(
fd,
&ch,
1,
0
);
if (n > 0) {
if (ch == '\n') {
return static_cast<ssize_t>(
out.size()
);
}
out.push_back(ch);
continue;
}
if (n == 0) {
return static_cast<ssize_t>(
out.size()
);
}
if (errno == EINTR) {
continue;
}
return -1;
}
errno = EMSGSIZE;
return -1;
}
// ============================================================
// 线程安全日志
// ============================================================
void log_message(
const std::string& message
) {
std::lock_guard<std::mutex> lock(
g_log_mutex
);
std::cout
<< message
<< std::flush;
}
六、编译和运行
6.1 编译服务端
bash
g++ -std=c++17 \
-Wall \
-Wextra \
-Wpedantic \
-pthread \
server_std_thread.cpp \
-o server_std_thread
6.2 编译客户端
bash
g++ -std=c++17 \
-Wall \
-Wextra \
-Wpedantic \
-pthread \
client_std_thread.cpp \
-o client_std_thread
虽然代码中没有直接调用 pthread_create(),在 GNU/Linux 上使用 <thread>、<mutex> 等并发组件时仍应使用 -pthread。GCC 文档说明,该选项既可能影响编译阶段的预处理配置,也负责链接线程支持,因此应同时用于编译和链接。
不要只写:
bash
g++ server_std_thread.cpp -o server_std_thread
推荐统一写成:
bash
g++ -std=c++17 -pthread source.cpp -o program
6.3 运行服务端
bash
./server_std_thread
可能输出:
text
std::thread server listening on 0.0.0.0:8888, PID=2321
6.4 运行客户端
在另一个终端执行:
bash
./client_std_thread
经过实际编译运行,客户端输出形式如下:
text
=== Concurrent std::thread clients: 3 ===
Client 0 connected: PID=2323, std::thread ID=140467622688448, fd=3
Client 0 sent request
Client 2 connected: PID=2323, std::thread ID=140467605903040, fd=4
Client 2 sent request
Client 1 connected: PID=2323, std::thread ID=140467614295744, fd=5
Client 1 sent request
Client 0 received: Hello from std::thread server
Client 2 received: Hello from std::thread server
Client 1 received: Hello from std::thread server
=== All clients finished ===
三个客户端线程拥有相同 PID:
text
PID = 2323
但拥有不同的 std::thread::id:
text
140467622688448
140467605903040
140467614295744
服务端分别创建三个工作线程处理连接:
text
主线程
│
├── cfd=4 ──► 工作线程A
├── cfd=5 ──► 工作线程B
└── cfd=6 ──► 工作线程C
七、std::thread 如何传递参数
7.1 pthread 参数传递
pthread 的线程入口必须符合固定签名:
cpp
void* thread_function(void* arg);
创建线程:
cpp
int* index = new int(i);
pthread_create(
&tid,
nullptr,
run_client,
index
);
线程中还要恢复类型:
cpp
void* run_client(void* arg) {
int index =
*static_cast<int*>(arg);
delete static_cast<int*>(arg);
return nullptr;
}
调用关系:
text
int*
│
▼
转换成 void*
│
▼
传给 pthread
│
▼
在线程中转换回 int*
│
▼
读取参数
│
▼
delete
7.2 std::thread 参数传递
使用 std::thread 可以直接写:
cpp
std::thread thread(
run_client,
i
);
线程函数也可以直接声明为:
cpp
void run_client(int index);
完整过程:
text
i
│
├── 复制参数
│
▼
std::thread
│
▼
调用 run_client(i)
不再需要:
text
void*
强制类型转换
new
delete
固定的线程入口函数签名
std::thread 构造函数会保存可调用对象和参数,并在新线程中调用它们;创建新线程失败时会抛出 std::system_error。
7.3 默认是按值保存参数
例如:
cpp
int value = 10;
std::thread thread(
print_value,
value
);
新线程通常获得 value 的值副本。
如果线程函数需要引用原对象,必须显式使用:
cpp
std::ref(value)
例如:
cpp
void add_one(int& value) {
++value;
}
int value = 10;
std::thread thread(
add_one,
std::ref(value)
);
thread.join();
否则直接传入:
cpp
std::thread thread(
add_one,
value
);
无法按照普通左值引用参数调用。
不过使用引用时必须保证:
text
原对象生命周期
│
└── 必须覆盖工作线程的使用期间
错误示例:
cpp
std::thread create_thread() {
int value = 10;
std::thread thread(
[&value]() {
// 可能访问已经销毁的 value。
std::cout << value;
}
);
return thread;
}
当函数返回后:
text
局部变量 value 被销毁
│
线程仍然持有 &value
│
▼
悬空引用
八、std::mutex 与 pthread_mutex_t
8.1 pthread 写法
cpp
pthread_mutex_t mutex =
PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
std::cout << message;
pthread_mutex_unlock(&mutex);
存在一个问题:
cpp
pthread_mutex_lock(&mutex);
some_function(); // 可能抛异常
pthread_mutex_unlock(&mutex);
如果 some_function() 抛出异常,解锁语句不会执行:
text
mutex 加锁
│
▼
抛出异常
│
▼
跳过 unlock
│
▼
其他线程永久等待
8.2 C++ RAII 写法
cpp
std::mutex mutex;
{
std::lock_guard<std::mutex> lock(mutex);
std::cout << message;
}
执行过程:
text
进入作用域
│
lock_guard 构造
│
mutex.lock()
│
执行受保护代码
│
离开作用域
│
lock_guard 析构
│
mutex.unlock()
即使发生异常,栈展开也会调用 lock_guard 析构函数。
std::lock_guard 持有互斥对象引用,在构造和析构过程中管理锁;它本身不可复制。
因此推荐写法是:
cpp
void log_message(
const std::string& message
) {
std::lock_guard<std::mutex> lock(
g_log_mutex
);
std::cout << message;
}
而不是手工调用:
cpp
g_log_mutex.lock();
std::cout << message;
g_log_mutex.unlock();
九、join() 和 detach() 的区别
9.1 join()
cpp
std::thread thread(
run_client,
0
);
thread.join();
执行过程:
text
主线程 工作线程
│ │
创建 thread │
│─────────────────────────────►│
│ │ 执行 run_client()
调用 join() │
│ 阻塞等待 │
│ │
│ │ 线程结束
│◄─────────────────────────────│
join() 返回
适用于:
-
客户端测试程序;
-
主线程需要等待结果;
-
线程结束后才能继续;
-
程序退出前需要确保任务完成。
线程执行结束与成功返回的 join() 之间形成同步关系。
9.2 detach()
cpp
std::thread worker(
handle_client,
cfd,
client_addr
);
worker.detach();
执行过程:
text
主线程 工作线程
│ │
创建 worker │
│─────────────────────────────►│
调用 detach() │
│ │
继续 accept() │ 继续处理客户端
│ │
│ │ 线程独立结束
分离后:
-
std::thread对象不再代表该执行线程; -
不能调用
join(); -
工作线程继续运行;
-
工作线程结束后,其线程资源由运行库回收。
标准将"没有线程对象代表该线程"的状态定义为分离线程。
9.3 为什么客户端使用 join()
客户端主线程必须等待三个客户端线程完成:
cpp
for (std::thread& thread : threads) {
thread.join();
}
否则:
text
主线程创建三个工作线程
│
▼
主线程立即 return
│
▼
进程结束
│
▼
其他线程也被终止
9.4 为什么服务端使用 detach()
服务端主线程不能每创建一个线程就立即执行:
cpp
worker.join();
否则流程会变成:
text
accept 客户端1
│
创建工作线程1
│
join 等待线程1结束
│
accept 客户端2
这实际上会把服务器重新变成串行处理:
text
客户端1处理完
│
▼
客户端2才能进入
│
▼
客户端3才能进入
因此一连接一线程服务端一般会:
cpp
worker.detach();
不过 detach() 也有缺点:
-
主线程无法等待工作线程;
-
不容易获取返回值;
-
不容易执行统一停止;
-
进程关闭时难以保证所有任务已经完成;
-
工作线程不能访问生命周期已经结束的对象。
生产服务通常更适合使用线程池或受控的线程集合,而不是无限制创建并分离线程。
十、std::thread 最危险的规则:析构前必须处理
下面的代码有严重问题:
cpp
void test() {
std::thread thread(
[]() {
std::this_thread::sleep_for(
std::chrono::seconds(1)
);
}
);
// 没有 join()
// 也没有 detach()
}
当 test() 返回时:
text
thread 对象准备析构
│
▼
thread.joinable() == true
│
▼
调用 std::terminate()
│
▼
整个进程终止
C++ 标准明确规定:如果 std::thread 析构时仍处于 joinable() 状态,将调用 std::terminate()。
因此每一个成功创建的 std::thread 最终都必须满足以下二选一:
cpp
thread.join();
或者:
cpp
thread.detach();
常见安全写法:
cpp
if (thread.joinable()) {
thread.join();
}
十一、线程创建失败的处理差异
11.1 pthread
pthread 函数直接返回错误码:
cpp
pthread_t tid;
int ret = pthread_create(
&tid,
nullptr,
run_client,
arg
);
if (ret != 0) {
std::cerr
<< std::strerror(ret)
<< '\n';
}
注意:
cpp
std::strerror(ret)
而不是:
cpp
std::strerror(errno)
11.2 std::thread
std::thread 创建失败时使用异常机制:
cpp
try {
std::thread thread(
run_client,
index
);
thread.detach();
} catch (const std::system_error& e) {
std::cerr
<< e.what()
<< '\n';
}
标准规定,在无法启动新线程时,std::thread 构造函数抛出 std::system_error。
两种错误模型如下:
text
pthread:
调用 pthread_create()
│
▼
检查 ret
│
├── ret == 0:成功
└── ret != 0:失败
std::thread:
构造 std::thread
│
├── 成功:得到线程对象
│
└── 失败:抛出 std::system_error
十二、pthread_t、std::thread::id 和 Linux TID
这三个概念不能简单视为同一个值。
12.1 pthread_t
cpp
pthread_t tid =
pthread_self();
这是 POSIX 线程库的线程标识类型。
12.2 std::thread::id
cpp
std::thread::id id =
std::this_thread::get_id();
这是 C++ 标准库定义的线程标识。
可以输出:
cpp
std::cout
<< std::this_thread::get_id()
<< '\n';
但是不应假设它:
-
一定是整数;
-
一定等于
pthread_t; -
一定等于 Linux 内核 TID;
-
一定可以长期保存并跨线程生命周期比较。
12.3 Linux 内核 TID
Linux 内核调度实体具有自己的线程 ID。
如确实需要 Linux TID,可以使用 Linux 特定接口:
cpp
#include <sys/syscall.h>
#include <unistd.h>
long tid =
syscall(SYS_gettid);
但是这样会降低可移植性。
通用 C++ 代码优先使用:
cpp
std::this_thread::get_id();
Linux 调试、strace、perf 或 /proc 分析场景可以使用内核 TID。
十三、文件描述符共享规则没有改变
改用 std::thread 后,线程的文件描述符共享规则完全没有变化。
所有线程仍然共享同一个进程级文件描述符表:
text
服务端进程
┌─────────────────────────────────────┐
│ 文件描述符表 │
│ │
│ fd=3 ──► 监听 socket │
│ fd=4 ──► 客户端1 socket │
│ fd=5 ──► 客户端2 socket │
│ │
│ 主线程 │
│ 工作线程1 │
│ 工作线程2 │
│ │
│ 全部共享上面的 fd 表 │
└─────────────────────────────────────┘
POSIX accept() 会为已接受的连接分配并返回一个新的文件描述符。
因此下面的代码仍然错误:
cpp
int cfd = accept(...);
std::thread worker(
handle_client,
cfd,
client_addr
);
worker.detach();
// 错误:
close(cfd);
原因是:
text
主线程 close(cfd)
│
▼
共享文件描述符表中的 cfd 被关闭
│
▼
工作线程中的同一 cfd 也失效
正确的所有权转移过程:
text
accept() 返回 cfd
│
▼
主线程暂时拥有 cfd
│
std::thread 创建成功
│
▼
所有权转移给工作线程
│
▼
主线程不再使用 cfd
│
▼
工作线程最终 close(cfd)
需要特别注意:
cpp
[cfd]() {
handle_client(cfd);
}
虽然 Lambda 按值捕获了 cfd,但只是复制了整数编号:
text
主线程变量 cfd = 4
工作线程副本 cfd = 4
它并没有调用:
cpp
dup(cfd);
也没有创建新的底层 socket。
两个整数仍然指向同一个进程文件描述符表项。
十四、pthread 与 std::thread 代码对照
14.1 创建线程
pthread
cpp
pthread_t tid;
int ret = pthread_create(
&tid,
nullptr,
do_connect,
info
);
if (ret != 0) {
// 错误处理
}
std::thread
cpp
try {
std::thread worker(
handle_client,
cfd,
client_addr
);
} catch (const std::system_error& e) {
// 错误处理
}
14.2 等待线程
pthread
cpp
pthread_join(
tid,
nullptr
);
std::thread
cpp
thread.join();
14.3 分离线程
pthread
cpp
pthread_detach(tid);
std::thread
cpp
thread.detach();
14.4 获取线程 ID
pthread
cpp
pthread_t tid =
pthread_self();
std::thread
cpp
std::thread::id tid =
std::this_thread::get_id();
14.5 休眠
POSIX
cpp
sleep(1);
或者:
cpp
usleep(200000);
C++ 标准库
cpp
std::this_thread::sleep_for(
std::chrono::seconds(1)
);
或者:
cpp
std::this_thread::sleep_for(
std::chrono::milliseconds(200)
);
标准库方式具有更清晰的时间单位:
text
seconds
milliseconds
microseconds
nanoseconds
不容易混淆:
text
1 是一秒还是一毫秒?
200000 是多少时间?
14.6 加锁
pthread
cpp
pthread_mutex_lock(&mutex);
std::cout << message;
pthread_mutex_unlock(&mutex);
C++ 标准库
cpp
std::lock_guard<std::mutex> lock(
mutex
);
std::cout << message;
14.7 线程参数
pthread
cpp
void* do_work(void* arg);
C++ 标准库
cpp
void do_work(
int index,
std::string message
);
创建时直接传参:
cpp
std::thread thread(
do_work,
10,
"hello"
);
十五、pthread 与 std::thread 的完整差异
| 对比项 | pthread | C++ std::thread |
|---|---|---|
| 所属标准 | POSIX | ISO C++ |
| 头文件 | <pthread.h> |
<thread> |
| 创建线程 | pthread_create() |
构造 std::thread |
| 线程函数 | 必须是兼容 void* (*)(void*) 的入口 |
任意满足调用条件的可调用对象 |
| 参数传递 | 通过 void* |
类型安全的模板参数 |
| 返回值 | 通过 void* 或共享数据 |
线程函数返回值默认被忽略 |
| 错误处理 | 返回错误码 | 通常抛出 std::system_error |
| 等待 | pthread_join() |
join() |
| 分离 | pthread_detach() |
detach() |
| 互斥锁 | pthread_mutex_t |
std::mutex |
| 自动锁管理 | 需要额外封装 | std::lock_guard、std::unique_lock |
| 休眠 | sleep()、usleep() |
std::this_thread::sleep_for() |
| 线程 ID | pthread_t |
std::thread::id |
| 可复制性 | pthread_t 通常可以作为句柄值保存 |
std::thread 不可复制,只可移动 |
| 生命周期规则 | 手工管理 | 析构时若仍 joinable 会终止程序 |
| 可移植性 | POSIX 系统 | 支持 C++ 线程库的平台 |
| 原生接口 | 本身就是 POSIX 原生接口 | 可通过 native_handle() 获取实现定义句柄 |
| 代码风格 | C 风格接口 | RAII、模板和类型安全接口 |
十六、性能是否有差异
不应简单认为:
text
std::thread 比 pthread 更快
或者:
text
pthread 比 std::thread 更快
std::thread 是标准 C++ 抽象,标准并不规定它必须使用 pthread 实现;其 native_handle_type 也属于实现定义类型。
在 GNU/Linux 的常见 GCC/libstdc++ 环境中,标准线程功能通过平台线程支持运行,编译时仍使用 -pthread。
从程序设计角度看,两者创建的都是真正的系统线程,因此更重要的性能因素是:
-
创建了多少线程;
-
每个线程的栈大小;
-
上下文切换次数;
-
锁竞争;
-
数据共享方式;
-
Socket 是否阻塞;
-
每个连接的业务耗时;
-
是否使用线程池;
-
是否使用
epoll; -
是否频繁分配内存。
接口层面的写法通常不是主要瓶颈。
十七、std::thread 的主要优势
17.1 类型安全
pthread:
cpp
void* arg
需要:
cpp
static_cast<ClientInfo*>(arg);
std::thread:
cpp
std::thread(
handle_client,
cfd,
client_addr
);
编译器可以检查参数类型是否匹配。
17.2 支持 Lambda
可以直接捕获连接信息:
cpp
std::thread(
[cfd, client_addr]() {
handle_client(
cfd,
client_addr
);
}
).detach();
不需要专门定义:
cpp
struct ClientInfo;
也不需要:
cpp
new ClientInfo;
delete info;
17.3 与 RAII 自然结合
cpp
std::lock_guard<std::mutex> lock(mutex);
UniqueFd fd(raw_fd);
std::vector<std::thread> threads;
资源生命周期由对象管理:
text
对象构造
│
▼
取得资源
│
▼
使用资源
│
▼
对象析构
│
▼
释放资源
17.4 更适合现代 C++
可以和下列组件直接组合:
cpp
std::thread
std::mutex
std::condition_variable
std::future
std::promise
std::async
std::atomic
std::jthread
std::stop_token
十八、pthread 的主要优势
pthread 仍然有自己的适用场景。
18.1 更直接的 POSIX 控制
pthread 可以使用更多平台级属性:
cpp
pthread_attr_setstacksize();
pthread_attr_setdetachstate();
pthread_setaffinity_np();
pthread_setschedparam();
pthread_sigmask();
其中一些功能没有完全对应的标准 C++ 接口。
18.2 更容易与 C 项目集成
如果项目本身使用 C:
c
void* worker(void* arg);
pthread 是直接选择。
18.3 更明确的平台语义
如果代码只运行在 Linux,并且需要:
-
CPU 亲和性;
-
实时调度策略;
-
精确线程栈配置;
-
POSIX 信号控制;
-
特定 pthread 扩展;
直接使用 pthread 通常更自然。
18.4 可以通过 native_handle() 混合使用
std::thread 提供:
cpp
thread.native_handle();
在 Linux 的具体标准库实现中,可以利用原生句柄调用平台接口。
例如概念上:
cpp
std::thread worker(...);
auto native =
worker.native_handle();
// 使用平台特定接口操作 native。
但 native_handle_type 是实现定义的,因此这部分代码不再是完全可移植的标准 C++。
十九、三种并发模型总对比
| 对比项 | 多进程 fork() |
pthread | std::thread |
|---|---|---|---|
| 并发实体 | 进程 | 线程 | 线程 |
| 地址空间 | 独立 | 共享 | 共享 |
| 文件描述符表 | fork 后独立表副本 | 共享 | 共享 |
| 创建方式 | fork() |
pthread_create() |
构造 std::thread |
| 参数传递 | 地址空间副本或 IPC | void* |
类型安全参数 |
| 回收方式 | waitpid() |
pthread_join()/detach |
join()/detach() |
| 僵尸问题 | 可能产生僵尸进程 | 无传统僵尸进程 | 无传统僵尸进程 |
| 错误处理 | -1 和 errno |
错误码 | 异常 |
| 同步接口 | 进程间同步机制 | pthread mutex | std::mutex 等 |
| 隔离性 | 强 | 弱 | 弱 |
| 单任务崩溃影响 | 通常局限在子进程 | 可能终止整个进程 | 可能终止整个进程 |
| 代码风格 | POSIX 系统编程 | C 风格线程接口 | 现代 C++ RAII |
| 可移植性 | Unix/Linux | POSIX 系统 | C++ 线程库平台 |
| 类型安全 | 较高 | 较低 | 较高 |
| 适合场景 | 强隔离任务 | Linux 底层线程控制 | 普通现代 C++ 并发程序 |
二十、C++20 的 std::jthread
如果使用 C++20,还可以考虑:
cpp
std::jthread
它与 std::thread 的主要区别是:
-
析构时可以自动请求停止;
-
析构时自动等待线程结束;
-
支持
std::stop_token; -
减少忘记
join()导致std::terminate()的风险。
标准将 std::jthread 描述为具有停止令牌、停止请求和自动等待能力的线程类。
简单示例:
cpp
#include <chrono>
#include <iostream>
#include <stop_token>
#include <thread>
void worker(std::stop_token token) {
while (!token.stop_requested()) {
std::cout << "working\n";
std::this_thread::sleep_for(
std::chrono::milliseconds(500)
);
}
std::cout << "worker stopped\n";
}
int main() {
std::jthread thread(worker);
std::this_thread::sleep_for(
std::chrono::seconds(2)
);
thread.request_stop();
// 不需要显式调用 join()。
}
不过对于当前服务端的分离线程模型,std::jthread 不能直接解决所有问题。
因为:
text
主线程需要持续 accept
│
▼
每个连接创建一个线程
│
▼
如果立即销毁 jthread
│
▼
析构会等待线程结束
│
▼
服务端重新串行化
因此真正的改进方向不是简单地把:
cpp
std::thread
替换成:
cpp
std::jthread
而是使用:
text
固定线程池
+
任务队列
+
条件变量
+
统一停止控制
二十一、最终结论
pthread 版本:
text
更接近 Linux/POSIX 底层
│
├── void* 参数
├── 返回错误码
├── 手工管理锁
└── 更容易操作线程属性
std::thread 版本:
text
更接近现代 C++ 编程方式
│
├── 类型安全参数
├── 支持函数对象和 Lambda
├── std::mutex
├── std::lock_guard
├── RAII
└── 更好的跨平台接口
在当前 TCP 服务端中,两者的核心并发模型没有变化:
text
主线程负责 accept()
工作线程负责客户端通信
所有线程共享地址空间
所有线程共享文件描述符表
每个 cfd 只交给一个工作线程管理
真正发生变化的是线程接口:
text
pthread_create()
↓
std::thread
pthread_join()
↓
thread.join()
pthread_detach()
↓
thread.detach()
pthread_mutex_t
↓
std::mutex
pthread_mutex_lock/unlock
↓
std::lock_guard
对于现代 C++ 项目,优先使用:
cpp
std::thread
std::mutex
std::lock_guard
std::condition_variable
std::atomic
只有在需要 Linux/POSIX 特定线程能力时,再使用:
cpp
pthread
或者通过:
cpp
std::thread::native_handle()
访问平台原生接口。
当前"一连接一线程"版本适合理解线程创建和 Socket 所有权,但生产服务端更推荐:
text
accept 主线程
│
▼
线程安全任务队列
│
▼
固定数量工作线程
即:
text
线程池模型
这样可以避免客户端数量不断增加时无限制创建线程。