7.C++标准多线程实现 TCP通信

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::threadjoin()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(
        &current,
        &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::mutexpthread_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_tstd::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 调试、straceperf/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_guardstd::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()
僵尸问题 可能产生僵尸进程 无传统僵尸进程 无传统僵尸进程
错误处理 -1errno 错误码 异常
同步接口 进程间同步机制 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 复制代码
线程池模型

这样可以避免客户端数量不断增加时无限制创建线程。

相关推荐
chen_ke_hao5 小时前
K8s 高可用集群部署
java·docker·kubernetes
CodeStats5 小时前
《源纹天书》第一百八十一章至第一百八十五章:跨界bug·双世界联合debug
java·源纹天书
小则又沐风a5 小时前
库的原理:目标文件,ELF格式,程序加载
java·linux·前端
江屿风5 小时前
【C++笔记】List流食般投喂
开发语言·c++·笔记·list
我是唐青枫5 小时前
Java Neo4j 实战指南:从图模型、Cypher 到 Spring Boot 关系查询
java·spring boot·neo4j
Lucis__8 小时前
基于Cache替换算法的LRU缓存实现
数据结构·c++·算法·缓存·lru
Cx330❀14 小时前
【MySQL基础】一文吃透“表的约束”:从 Null/Default 到主外键的终极安全法则
linux·服务器·数据库·c++·mysql·安全
Java搬码工14 小时前
CompletableFuture 完整详解 + 全场景使用案例
java
许心月14 小时前
Java学习资料网站汇总
java