关键词:
前言:相比于Go语言这种原生支持并发、自动垃圾回收的服务端“天选之子”,C++的多线程编程显得臃肿、困难。但是在C++服务器编程当中,多线程是一道绕不开门槛,是提高应用程序响应和性能的重要利器,能够隐藏诸如I/O这样耗时的操作延迟。特别是C++11引入了std::thread之后,C++对并发的支持显得异常强大。这篇博客做一个入门级的总结,以便日后讲解服务端编程的知识。
目录
决定线程创建后的状态:join阻塞进程和detach守护线程
管理线程API :yield、get_id、sleep_for、sleep_until
线程与进程
这是一道后端工程师必会的面试题,八股文入门级。
拥有资源
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属于进程的资源。
调度
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
Go语言并发-借鉴解读
Go语言并发编程
Go语言层面支持协程,将并发业务逻辑从异步转为同步,大幅提高开发效率;
在c++中,做并发编程目前主流的方案是事件驱动(单线程/多线程/多进程模型等),而事件驱动就需要一个IO多路复用的分发器(select/epoll),这样,就造成了业务逻辑的断开,在代码层面为异步模型,比如:
1).先是一段业务代码
2).调用IO(业务断裂)
3).IO完成后的后续处理逻辑;
而go中的协程的支持让这样的开发工作就轻松多了,按照同步的方式顺序写业务逻辑,遇到IO也没关系,一个线程中可以创建成上百万个协程,这个协程阻塞了就跑下一个,不需要应用代码层面来负责IO后续调度的处理;
比起自己用C/C++去封装底层或调用libevent之类的库,Go的优势是将事件机制封装成了CSP模式,编程变得方便了,但是需要付出goroutine调度的开销;
Go语言主动垃圾回收
毫无疑问这个好用,有了垃圾回收,不需要开发者自行控制内存的释放,这样可避免一堆问题(重复释放、忘记释放内存、访问已释放的内存等);
当然,c++11引入的智能指针(unique_ptr等)如果在程序中应用的普遍,也可以达到类似垃圾回收的目的;
GC带来的问题也是有的,会造成STW,会有程序停止调度的卡顿;
Go1.5的GC利用各种手段大大缩减了STW的时间。Go语言官方保证,在50毫秒的Go程序运行时间中因GC导致的调度停顿至多只有10毫秒。
Go语言原子函数和互斥锁处理共享资源竞争问题
参考我之前的博客:Golang——原子函数和互斥锁处理共享资源竞争问题
Go语言通道缓冲区探索
参考我之前的博客:Go语言——多线程相关的一点思考
C++的角度看Go语言
参考我之前的博客:从C++的角度看Go语言
成员函数API手册
下面是官网的成员函数API连接,作为工具使用。重点关注最常用的join和detach方法。
Construct thread (public member function ) 构造函数
Thread destructor (public member function ) 析构函数
Move-assign thread (public member function ) 赋值重载
Get thread id (public member function ) 获取线程id
Check if joinable (public member function ) 判断线程是否可以加入等待
Join thread (public member function ) 加入等待
Detach thread (public member function ) 分离线程
Swap threads (public member function ) 线程交换
Get native handle (public member function ) 获取线程句柄
Detect hardware concurrency (public static member function ) 检测硬件并发特性
创建线程:hello world!
创建线程的方式就是构造一个thread对象,并指定入口函数。与普通对象不一样的是,此时编译器便会为我们创建一个新的操作系统线程,并在新的线程中执行我们的入口函数。
用多线程开启一个hello world,源代码如下:
#include <iostream>
#include <thread>
using namespace std;
void hello ()
std::cout << "Hello world" <<endl;
int main ()
std::thread t(hello);
t.join();
return 0;
编译方法:
g++ -std=c++17 hello_word.cpp -o hello_word -lpthread
不知道如何使用g++编译的,可以参考本系列的第六篇博客:
记得加上 编译选项 -lpthread,不然会报错:
hello_word.cpp:(.text._ZNSt6threadC2IRFvvEJEEEOT_DpOT0_[_ZNSt6threadC5IRFvvEJEEEOT_DpOT0_]+0x2f): undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status
用lambda表达式描述多线程
lambda也是C++ 11引入的新特性之一,是C++实现闭包的重要手段之一,本系列会专门写一篇文章介绍一下lambda这个新特性。
我们用lambda实现上面的hello world:
#include <iostream>
#include <thread>
using namespace std;
int main()
thread t([]
cout << "Hello World from lambda thread." << endl;
);
t.join();
return 0;
传递参数给入口函数
将需要传递的参数写在构造thread方法的第二个参数即可。举例如下:
#include <iostream>
#include <thread>
#include <string>
using namespace std;
void func (string url)
cout << "welcome to cite " << url << endl;
int main ()
thread t(func, "www.pornhub.com");
t.join();
return 0;
决定线程创建后的状态:join阻塞进程和detach守护线程
刚才我们讲了线程是如何创建并指定参数的,下面重点讲一下thread的两种方法。
join | 等待线程完成其执行 | 调用此接口时,当前线程会一直阻塞,直到目标线程执行完成(当然,很可能目标线程在此处调用之前就已经执行完成了,不过这不要紧)。因此,如果目标线程的任务非常耗时,你就要考虑好是否需要在主线程上等待它了,因此这很可能会导致主线程卡住。 |
detach | 允许线程独立执行 | detach是让目标线程成为守护线程(daemon threads)。一旦detach之后,目标线程将独立执行,即便其对应的thread对象销毁也不影响线程的执行。并且,你无法再与之通信。 |
一旦启动线程之后,我们必须决定是要等待直接它结束(通过join),还是让它独立运行(通过detach),我们必须二者选其一。如果在thread对象销毁的时候我们还没有做决定,则thread对象在析构函数出将调用std::terminate()从而导致我们的进程异常退出。
管理线程API :yield、get_id、sleep_for、sleep_until
API | 功能 | 说明 |
yield | 让出处理器,重新调度处理各个线程 | 通常用在自己的主要任务已经完成的时候,此时希望让出处理器给其他任务使用 |
get_id | 返回当前线程id | 返回当前线程的id,可以以此来标识不同的线程。 |
sleep_for | 使当前线程执行停止指定时间段 | 让当前线程停止一段时间。 |
sleep_until | 使当前线程停止直到指定时间点 | 和sleep_for类似,但是是以具体的时间点为参数。 |
常用场景:初始化任务过程中的一次调用
在服务器编程的过程当中,启动的过程当中经常需要对多个任务进行初始化,但是多个线程之间共享资源,所以只需要初始化一次即可。利用call_once翻转flag的特性,多个线程都会使用init函数进行初始化,但是只会有一个线程真正执行它,具体是哪一个线程进行初始化,我们并不关心,这是一个典型的一次调用的场景。
其中call_once会在进阶部分讲授,期待一下:
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
void init ()
cout << "Now is initing……" << endl;
// do initing……
void worker (once_flag* flag)
call_once(*flag, init);
int main ()
once_flag flag;
thread t1(worker, &flag);
thread t2(worker, &flag);
thread t3(worker, &flag);
t1.join();
t2.join();
t3.join();
return 0;
编译运行:
g++ -std=c++11 once_init.cpp -o once_init -lpthread
./once_init
Now is initing……
进阶实验:加上每个id观察线程执行先后顺序
这是一个非常有趣的现象,在上面的例子当中,由于三个线程是处于无约束的竞争状态,所以如果加上打印id的条件,打印出来的结果会是无序的,但是能保证只会初始化一次,具体代码和实验现象如下:
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
void init ()
cout << "Now is initing……" << endl;
// do initing……
void worker (once_flag* flag, int id)
call_once(*flag, init);
cout << "id is " << id << endl;
int main ()
once_flag flag;
thread t1(worker, &flag, 1);
thread t2(worker, &flag, 2);
thread t3(worker, &flag, 3);
t1.join();
t2.join();
t3.join();
return 0;
我执行了三次,每一次的结果都不相同:
网络编程中的多线程实例
以上一讲TCP编程为例,我们在发送端sender中的main函数里面,开了一个线程进行send。
参见:https://xduwq.blog.csdn.net/article/details/118557515
源代码如下:
#include "Acceptor.h"
#include "InetAddress.h"
#include "TcpStream.h"
#include <thread>
#include <unistd.h>
void sender(const char* filename, TcpStreamPtr stream)
FILE* fp = fopen(filename, "rb");
if (!fp)
return;
printf("Sleeping 10 seconds.\\n");
sleep(10);
printf("Start sending file %s\\n", filename);
char buf[8192];
size_t nr = 0;
while ( (nr = fread(buf, 1, sizeof buf, fp)) > 0)
stream->sendAll(buf, nr);
fclose(fp);
printf("Finish sending file %s\\n", filename);
// Safe close connection
printf("Shutdown write and read until EOF\\n");
stream->shutdownWrite();
while ( (nr = stream->receiveSome(buf, sizeof buf)) > 0)
// do nothing
printf("All done.\\n");
// TcpStream destructs here, close the TCP socket.
int main(int argc, char* argv[])
if (argc < 3)
printf("Usage:\\n %s filename port\\n", argv[0]);
return 0;
int port = atoi(argv[2]);
Acceptor acceptor((InetAddress(port)));
printf("Accepting... Ctrl-C to exit\\n");
int count = 0;
while (true)
TcpStreamPtr tcpStream = acceptor.accept();
printf("accepted no. %d client\\n", ++count);
std::thread thr(sender, argv[1], std::move(tcpStream));
thr.detach();
写在后面的话
这一讲仅仅是C++多线程编程的皮毛,后面会继续在本系列更新线程的所有权、异常环境下的等待、共享数据问题、同步并发操作、内存模型、原子模型、有锁/无锁的并发、服务端编程常用并发模型、中断线程、多线程调试等等,敬请期待。
参考:
- 《C++并发编程实战》
- 《Linux高性能服务器编程》
- 《Linux多线程服务端编程》
- https://www.cnblogs.com/mmc9527/p/10427924.html
- https://mp.weixin.qq.com/s/SDVlU8DGWiDfCaHThnutFQ
手把手写c++服务器(34):高并发高吞吐io秘密武器——epoll池化技术两万字长文(代码片段)
本系列文章导航: 手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:前文手把手写C++服务器(31):服务器性能提升关键——IO复用技术【两万字长文】介绍了IO复用技术,其中重点比较了... 查看详情
手把手写c++服务器(21):linuxsocket网络编程入门基础(代码片段)
本系列文章导航:手把手写C++服务器(0):专栏文章-汇总导航【更新中】前言:刚开始写C++服务器的时候,我们进行网络编程肯定是使用socketAPI,等熟练之后,会根据我们自己的需要,封装... 查看详情
手把手写c++服务器:专栏文章-汇总导航更新中
手把手写C++服务器(1):网络编程常见误区手把手写C++服务器(2):C/C++编译链接模型、函数重载隐患、头文件使用规范手把手写C++服务器(3):C++编译常见问题、编译优化方法、C++库发... 查看详情
手把手写c++服务器(36):手撕代码——高并发高qps技术基石之非阻塞recv万字长文(代码片段)
本系列文章导航: 手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:没有什么东西是永恒,没有什么方案是万能,阻塞模式和非阻塞模式各有利弊。创建socket是默认阻塞的。但是在高并发多Q... 查看详情
手把手写c++服务器(22):linuxsocket网络编程进阶第一弹(代码片段)
前言:前面一篇文章手把手写C++服务器(21):Linuxsocket网络编程入门基础,讲解了如何建立socket连接、如何转换/使用socket地址、如何绑定/监听/发起/接受/断开/终止/关闭连接。socket博大精深,进阶会多写几弹&... 查看详情
手把手写c++服务器(22):linuxsocket网络编程进阶第一弹(代码片段)
前言:前面一篇文章手把手写C++服务器(21):Linuxsocket网络编程入门基础,讲解了如何建立socket连接、如何转换/使用socket地址、如何绑定/监听/发起/接受/断开/终止/关闭连接。socket博大精深,进阶会多写几弹&... 查看详情
手把手写c++服务器(25):万物皆可文件之socketfd(代码片段)
本系列文章导航:手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:大家一定听说过在Linux当中,万物皆是文件,任何客观的存在都是以文件形式呈现。前面讲socket编程的时候(手把手写C+&... 查看详情
手把手写c++服务器(35):手撕代码——高并发高qps技术基石之非阻塞send万字长文(代码片段)
本系列文章导航: 手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:创建socket是默认阻塞的。但是在高并发多QPS的场景中,阻塞模式会极大程度上影响并发性,使之并发名存实亡。而send函... 查看详情
手把手写c++服务器(33):linux常用命令合集
本系列文章导航: 手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:服务端编程的过程当中,各种常用的命令行也会大量使用;熟悉常用Linux命令不仅仅是运维的基本要求,也是一个主程的基本门槛。这里... 查看详情
手把手写c++服务器(38):面试必背!linux网络socket编程必会十问!(代码片段)
本系列文章导航: 手把手写C++服务器(0):专栏文章-汇总导航【更新中】目录1、说一下客户端和服务端socket建立连接和关闭连接的过程2、如何将一个socket设置成非阻塞模式3、什么是socket三大属性?4、阻塞模... 查看详情
手把手写c++服务器(31):服务器性能提升关键——io复用技术两万字长文
本系列文章导航:手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:Linux中素有“万物皆文件,一切皆IO”的说法。前面几讲手撕了CGI网关服务器、echo回显服务器、discard服务的代码,但是这几个一次只能监听... 查看详情
手把手写c++服务器(15):网络编程入门第一个tcp项目(代码片段)
前言:前面一篇博客讲述了第一个UDP项目,这篇博客来讲一讲TCP。TCP建立容易,销毁困难;TCP接收容易,发送困难。我们都知道TCP是一个可靠的协议,但是真的不会丢包吗?如何安全地关闭TCP连接?... 查看详情
手把手写c++服务器(19):序列化数据网络传输解决方案(代码片段)
前言:数据传输是服务器编程必须要面临的问题之一,原始数据传输是非常脆弱的,序列化传输是业界常用的方法,其中谷歌的PB方案广受欢迎,常用作项目中主要的解决方案,值得C++服务器编程者... 查看详情
手把手写c++服务器(26):常用i/o操作创建文件描述符
本系列文章导航:手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:通过上一篇文章(手把手写C++服务器(25):万物皆可文件之socketfd),Linux万物皆文件的一定深入人心。如何操作这些文件?I/O函数将震撼登场!第一... 查看详情
手把手写c++服务器(17):自测!tcp协议面试经典十连问(代码片段)
前言:前面一篇文章《手把手写C++服务器(15):网络编程入门第一个TCP项目》介绍了一个简单入门级的TCP项目,这一篇文章重点讲一讲面试常见的TCP协议相关的十个问题,都是后端开发程序员必知必会的经典... 查看详情
手把手写c++服务器(17):自测!tcp协议面试经典十连问(代码片段)
前言:前面一篇文章《手把手写C++服务器(15):网络编程入门第一个TCP项目》介绍了一个简单入门级的TCP项目,这一篇文章重点讲一讲面试常见的TCP协议相关的十个问题,都是后端开发程序员必知必会的经典... 查看详情
手把手写c++服务器(28):手撕cgi通用网关接口服务器代码(代码片段)
本系列文章导航:手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:前文《手把手写C++服务器(26):常用I/O操作、创建文件描述符》《手把手写C++服务器(27):五大文件描述符零拷... 查看详情
手把手写c++服务器(27):五大文件描述符零拷贝控制总结(代码片段)
本系列文章导航:手把手写C++服务器(0):专栏文章-汇总导航【更新中】 前言:前文《手把手写C++服务器(26):常用I/O操作、创建文件描述符》、《手把手写C++服务器(25):万物皆可文件之sock... 查看详情