重写的顿悟:lambda 类型擦除、条件变量的真相与 sendto 里的 bind
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
这一篇比较特别------它不是按日期笔记整理的知识链,而是 8 月 29 号的两篇"感悟"笔记。两篇都是重写代码之后写下的顿悟时刻,一篇关于线程池,一篇关于 UDP 聊天室。感悟虽然短,但每一条都踩在前面十几篇博客的关节点上,值得单独成篇。
一、线程池重写感悟:lambda、function 与类型擦除
1. 回调函数为什么能"包容"lambda
重写线程池时最核心的发现是这个组合拳:std::function<void()> + lambda。
先回忆 lambda 的本质:lambda 表达式其实是一个类 ,它的类型就是类内 operator() 函数的类型,调用 lambda 执行的就是这个 operator() 内部的代码。
问题来了:每个 lambda 的类型都不一样(编译器为每个 lambda 生成独一无二的类名),那 std::function 是怎么统一接收它们的?
答案就是类型擦除:
- lambda 的返回值可以有 ,但不能有参数 ------参数用捕获替代;
- 类型确定(无参 + 指定返回值)+
function,才能做到类型擦除; - 什么样的 lambda 都可以类型擦除(只要签名匹配);但有参数的不好擦除------传参不适合我们这套设计,所以需要捕获。
还有一条隐秘规则:没有捕获参数的 lambda,可以隐式转换为 operator() 类型的普通函数指针 ------但这个转化需要 lambda 类和 function 缺一不可的配合才能在工作里跑通。捕获的变量本质就是当参数用的。
这就是线程池任务的最终形态:function<void()>,一个"带返回值约定、无参数、全部依赖捕获"的万能任务类型。上层用 lambda 注册业务,线程池只管调度执行------回调 = 底层调用上层。
2. pthread_cond_wait 为什么必须传锁------重写后的深层理解
之前线程同步那篇讲过"wait 必须传锁",这次重写把它彻底想透了:
因为条件变量的内核等待队列,本身也是临界资源!
而且注意苏醒之后的时序:线程被唤醒后,并不是直接从 wait 里出来执行,而是先去抢锁------抢到了,才会真正从 wait 里返回执行代码。所以 wait 内部完成的是"放锁 → 入队 → 睡 → 醒 → 抢锁"的完整闭环,锁不是 wait 的附属品,而是它整个生命周期的参与者。
顺带发现:线程池的线程和 enqueue 还要竞争一把锁------任务队列这把锁是消费端(线程池的线程)和生产端(enqueue)共同保护的临界区,两边谁都不能绕过它。
3. 一个血的教训:主线程一定不要先退
重写时踩的最后一个坑:主线程把任务派完就 return 了,进程直接退出,线程池里的线程还没来得及干活。所以------主线程一定要最后 join 等待,不能先退。
4. 封装的相似性:Thread 和 Threadpool 是一个模子
重写两遍之后发现,Thread 类的封装和 Threadpool 的封装非常相似:
- 都是包装函数 的思路:
void* (*)(void*)类型的执行回调(routine); - Thread 的封装和 Threadpool 的封装里,都有一个 routine 函数在执行回调------前者是"一个执行流的启动器",后者是"一堆执行流的调度器"。
这印证了一个设计直觉:线程池没有发明新东西,它就是线程封装 + 生产者消费者模型的组装品。
二、UDP 聊天室感悟:那些藏在函数里的"隐藏行为"
第二篇感悟来自写完 UDP 聊天室之后,三个各值一句的发现:
1. sendto 函数里藏着一个 bind
上一篇讲过"客户端不显式 bind",这篇感悟给出了更精确的动态描述:
客户端不能显式调用 bind 绑定端口和 IP,所以一旦检测到 sendto 时还未绑定,就先绑定 socket 文件,再发送。
也就是说,sendto 自带 bind 的功能------"没绑过?那我先给你随机绑一个"。"客户端不 bind"不是一种妥协,而是一段设计好的惰性初始化逻辑。
2. 读写线程不用分先后,因为 recvfrom 会阻塞
聊天室客户端开了读线程(从标准输入获取数据、发送)和写线程(接收、打印)。这两个线程不需要规定启动顺序 ------因为 recvfrom 会阻塞线程:没消息时读线程挂着,写线程照常工作;反之亦然。阻塞不是 bug,是天然的同步分界线。
3. 保存用本机序列,传输用网络序列
服务端要给所有在线用户转发消息,就得把每个客户端的 ip+port 存下来。这里有一条精打细算的规则:
服务端保存 ip/port 的时候用本机序列,后面作为参数传入的时候,先变为网络序列。
翻译一下:存的时候按"人看的"(本机字节序)存------日志好打印、比较逻辑好写;发的时候按"网看的"(网络字节序)转------一次转换都不浪费,也不重复转换。
三、写在最后:感悟类笔记的价值
这两篇笔记没有引入任何新知识,但每一条都是把前面的知识"用了一遍之后长出来的理解":
- lambda 类型擦除 ← C 语言多态、函数指针表的 C++ 版;
- 条件变量传锁 ← 等待队列也是临界资源(临界资源保护的无穷递归,最终由原子指令兜底);
- sendto 隐式 bind ← 客户端/服务端不对称设计的动态视角;
- 字节序的存/用分离 ← 网络字节序规则的工程化落地。
知识是学出来的,理解是写出来的。 重写一遍代码能产生的顿悟,往往比再读十遍笔记更多------这可能就是这两篇以感叹号命名的笔记想传达的东西。
至此线程章彻底收官、网络编程第一步落地。下一篇继续网络线:TCP socket 编程。