one loop per thread = 一个线程,只跑一个 EventLoop

muduo 的 Thread 类 = 专门用来创建这种 “带 loop 的线程”

Thread类是对线程的一个轻量级封装

Thread.h

1
2
3
4
5
6
7
8
9
10
#pragma once

#include "noncopyable.h"

#include <atomic>
#include <functional>
#include <unistd.h> //pid_t
#include <memory>
#include <thread>
#include <string>
1
2
3
4
5
class Thread : noncopyable
{
public:
using ThreadFunc = std::function<void()>;
}
  • 一个线程对象只能代表一个线程,不能复制

  • 定义线程要执行的函数类型,无参数、无返回值的函数

私有成员变量

1
2
3
4
5
6
7
8
private:
//bool started_;
//bool joined_; 用joinable()替代
std::unique_ptr<std::thread> thread_;
pid_t tid_;
std::string name_;
ThreadFunc func_;
static std::atomic<int> numCreated_;
  • 这个started_可以不要,started_ 的作用 = 记录是否调用过 start,智能指针自带这个功能。
  • numCreated_是一个全局的、线程安全的线程编号计数器
    • static:所有 Thread 对象共用这一个变量,全局唯一
    • atomic原子变量,多线程同时创建线程也不会计数错乱。

线程变量(thread_)为什么要用只能指针?

  • 为什么用指针:线程不是构造时就创建,而是 start () 时才创建,用指针才能动态创建。

    • Thread t(func); 此时还没有线程
    • 调用 t.start(); 才真正创建线程
    • 不用指针构造 Thread 时就必须创建线程,控制不了时机。
  • 为什么用只能指针不用裸指针:自动管理生命周期

  • 为什么用unique_ptr没用shared_ptrstd::thread 本身就不能拷贝,不能共享,用shared_ptr 去包一个本来就不能共享的东西,逻辑上就多此一举。且unique_ptr没用引用计数,内存更小。(但这里用shared_ptr也不算错,只是语义不适合)

内部函数

1
2
private:
void setDefaultName();

你创建线程时不传名字,它就自动帮你起一个。

成员函数

1
2
3
//用「const 左值引用」绑定「临时对象」,既实现了参数的默认值,又兼顾了性能和安全性。
Thread(ThreadFunc, const std::string& name = std::string());
~Thread();

const std::string& name

  • 线程名字

  • const &好处:

    • 不拷贝字符串
    • 能接收临时字符串(如 "ThreadTest"
    • 安全、高效

= std::string()

  • 默认参数
  • 如果你不传名字,就自动用空字符串
  • 然后类内部会调用 setDefaultName() 自动生成 Thread1、Thread2...
1
void start();

启动线程

1
void join();

阻塞等待线程跑完

1
2
pid_t tid() const { return tid_; }
const std::string& name() const { return name_; }
  1. 返回 & 引用 = 不复制字符串
  • 如果返回 std::string

    → 会复制一份字符串
    → 浪费性能、浪费内存

  • 如果返回 const std::string&

    → 直接返回类内部的那个字符串本身

    → 0 拷贝、0 开销、最快

  1. 加 const = 外面绝对改不了!
  • 线程名字只能在内部设置
  • 外面只能读,不能改
  • const 就是强制保护
1
static int numCreated() { return numCreated_; }

查看全局创建了多少个线程

Thread.cc

1
2
3
4
#include "Thread.h"
#include "CurrentThread.h"

#include <future>

头文件依赖

1
std::atomic_int Thread::numCreated_{0};

静态变量初始化

1
2
3
4
5
6
7
8
9
10
11
void Thread::setDefaultName()
{
int num = ++numCreated_;

if(name_.empty())
{
char buf[32] = { 0 };
snprintf(buf, sizeof buf, "Thread%d", num);
name_ = buf;
}
}
  • 如果 name_ 是空 → 生成名字

  • 如果 name_ 不为空 → 保留你传的名字

1
2
3
4
5
6
7
Thread::Thread(ThreadFunc func, const std::string& name)
:name_(name)
, tid_(0)
, func_(func)
{
setDefaultName();
}
  • thread_不在这初始化,start()时才真正创建线程,初始化thread_。

  • thread_是智能指针,会自动初始化为nullptr

1
2
3
4
5
6
7
Thread::~Thread()
{
if(thread_ && thread_->joinable())
{
thread_->detach();
}
}
  • thread_智能指针是否不为空,保证线程对象真的创建了

  • thread_->joinable():线程是否join()

  • detach () = 分离线程,不会阻塞

  • ==核心设计:==这里detach()不用害怕生命周期问题(一旦启动完成,Thread 对象就可以随时销毁),是因为start()fut.get()同步等待的设计,它用 等待 保证:访问 this 的阶段,this 一定存活之后就再也不访问 this

1
2
3
4
5
6
7
void Thread::join()
{
if(thread_ && thread_->joinable())
{
thread_->join();
}
}
  • joinable()替代原来joined_标记。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 启动线程,并保证 tid_ 赋值完成才返回
void Thread::start() {

// 1. 创建 promise,用于线程间传递 系统TID
std::promise<pid_t> prom;

// 2. 获取 future,用来等待值的到来
std::future<pid_t> fut = prom.get_future();

// 3. 创建线程,lambda 捕获 this 和 prom
thread_ = std::make_unique<std::thread>([this, &prom]() {
pid_t sys_tid = CurrentThread::tid();

// 把 TID 传给主线程,并唤醒等待的主线程
prom.set_value(sys_tid);

// 执行用户传入的线程业务函数
func_();
});

tid_ = fut.get();
}

原来的代码用 信号量 sem(C 语言接口,不是 C++ 标准)

1
2
3
4
sem_t sem;
sem_init(&sem, 0, 0);
sem_wait(&sem); // 主线程等
sem_post(&sem); // 子线程通知

现在用 std::promise + std::future

1
2
3
sem_init      =  创建 promise
sem_wait = fut.get()
sem_post = prom.set_value()
  1. 创建同步工具
1
2
std::promise<pid_t> prom;
std::future<pid_t> fut = prom.get_future();
  • promise:子线程用,用来发数据、发通知
  • future:主线程用,用来等数据、等通知
  1. 创建线程
1
thread_ = std::make_unique<std::thread>([this, &prom]() {
  • 启动新线程
  • 捕获 prom,让子线程能发通知给主线程
  1. 子线程获取 TID 并发消息
1
2
pid_t sys_tid = CurrentThread::tid();
prom.set_value(sys_tid);
  • 获取系统 TID
  • 把值传给主线程 + 唤醒主线程
  1. 主线程阻塞等待
1
tid_ = fut.get();
  • 等子线程发消息
  • 拿到 TID,赋值给 tid_
  • 然后 start()才返回

源码地址

Thread.h:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/Thread,h

Thread.cc:https://gitee.com/lpzdinghai/lpzmuduo/blob/master/Thread.cc