Linux多线程服务器端编程
2019-06-04 20:11
3227 查看
目录
Linux多线程服务器端编程
- 源码链接。
- muduo的编译安装.
- 陈硕的编译教程。
- bazel编译文件不能有中文路径。
- 安装到指定目录: /usrdata/usingdata/studying-coding/server-development/server-muduo/build/release-install-cpp11/lib/libmuduo_base.a.
线程安全的对象生命期管理
- 利用shared_ptr和weak_ptr避免对象析构时存在的竞争条件(race conditon).
- 当一个对象被多个线程同时看到,那么对象的销毁时机就会变得模糊不清,可能出现多种竞争条件(race condition).
- 用RAII(Resource Acquire Is Initialization, 资源申请即初始化)封装互斥量的创建和销毁, MutexLock封装临界区(critical section), 资源管理类。
- MutexLockGuard封装临界区的进入和退出,即加锁和解锁,MutexLockGuard一般是个栈上对象,它的作用域刚好等于临界区域。
- 不可拷贝类. 把copy构造函数和复制操作符声明为私有函数并不声明。
- 在C++11中使用delete关键字,muduo采用了这种方式。
namespace muduo { class noncopyable { public: noncopyable(const noncopyable&) = delete; void operator=(const noncopyable&) = delete; protected: noncopyable() = default; ~noncopyable() = default; }; } // namespace muduo
-
不要在构造函数中注册任何回调;(利用二段式构造(构造函数+initialization()),或直接调用register_函数)
对象的销毁线程比较难
- 单线程对象析构要注意避免空悬指针和野指针。多线线程每个成员函数的临界区域不得重叠,而且成员函数用来保护临界区的互斥器本身必须是有效的。
- 在析构函数中直接调用互斥器进行多线程的同步是不可取的,没有完全达到线程安全的效果。
- 作为数据成员的mutex不能保护析构, 因为成员的生命周期最多与对象一样长,而析构动作可以发生在对象死亡之后。(调用基类析构函数时,派生类的析构函数已经被调用)
- 析构函数本身不需要保护,因为只有别的线程都访问不到这个对象时,析构才是安全的。
- 如果要锁住相同类型的多个对象,为了保证始终按相同的顺序加锁,可以比较mutex对象的地址,始终先加锁地址较小的mutex.(防止死锁)
- 判断一个指针是不是合法指针没有高效的办法,这是C/C++指针问题的根源。
- 调用正在析构对象的任何非静态成员函数都是不安全的,更何况是虚函数。
- 指向对象的原始指针(raw pointer)最好不要暴露给别的线程。--- 一般用智能指针
- 解决空悬指针的办法是,引入一层间接性。(handle/body惯用技法)
- shared_ptr指针源码分析. shared_ptr控制对象的生命期,是强引用,只要有一个指向x对象的shared_ptr存在,该x对象就不会析构,当指向对象x的最后一个shared_ptr析构或reset()调用时,x保证会被销毁。
- weak_ptr不控制对象的生命期,但它知道对象是否还或者; 如果对象还活着,weak_ptr可以提升为有效的shared_ptr;
- 如果对象已经死了,提升失败,返回一个空的shared_ptr;
- 提升函数lock()行为是线程安全的。
-
如果不小心多进行了拷贝或赋值就会意外延长对象的生命周期。
线程同步精要
- 线程同步的四原则: 首要原则是尽量最低限度地共享对象,减少需要同步的场合。
- 其次使用高级的并发编程构建(TaskQueue, Producer-Consumer Queue, CountDownLatch(倒计时)).
- 最后不得已必须使用底层同步原语(promitives)时,只用非递归的互斥器和条件变量,慎用读写锁,不要用信号量。 使用非递归(non-recursive)互斥量可以把程序错误尽早地暴露出来。
gdb ./self_deadlock core--- 调试定位死锁。
-
条件变量的学名叫作管程(monitor);
-
主线程等待多个子线程完成初始化;
-
如果需要等待一段已知的时间,应该往event loop里注册一个timer,然后在timer的回调函数里接着干活,因为线程是个珍贵的资源,不能轻易浪费(阻塞也是浪费)。
借shared_ptr实现写时拷贝(copy-on-write)
- 写时如果引用计数大于1,该如何处理?
- 用普通的mutex替换读写锁。
- 大多数情况下更新都是在原来数据上进行的,拷贝的比例还不到1%.
多线程服务器的适用场合与常用编程模型
- 进程(process)是操作系统里最重要的两个概念之一(另一个是文件),一个进程就是内存中正在运行的程序。
- 每个进程有自己独立的地址空间(adress space), 在同一个进程还是不在同一个进程是系统功能划分的重要决策。
- 把一个进程比喻成一个人,周期性的心跳判断对方是否还活着。 容错 --- 万一有人突然死了。
- 扩容 --- 新人中途加进来。
- 负载均衡 --- 把甲的活挪给乙做。
- 退休 --- 甲要修复Bug, 先别派新任务,等他做完手上的活就把他重启。
-
多进程可以高效地共享代码段,但是不能共享数据。
单线程服务器的常用编程模型
- “nonblocking IO + IO multiplexing(非阻塞IO + IO 多路复用)”, 即Reactor模式(反应堆模式)。 lighttpd, 单线程服务器(Nginx与之类似,每个工作进程有一个事件循环(event loop)).
- libevent, libev.
- ACE, Poco C++ Libaries.
- Java NIO, 包括Aache Mina 和 Netty.
- POE(Perl).
- Twisted(Python).
-
以事件驱动和事件回调的方式实现业务逻辑。
多线程服务器的常用编程模型
- 每个请求创建一个线程,使用阻塞式IO操作(可伸展性不佳)。
- 使用线程池。 阻塞的任务队列(blocking queue TaskQueue)。
- Intel Threading Building Blocks的concurrent_queue性能比较好。
- 线程池(thread pool)用来做计算, 可以用任务队列或者是生产者消费者队列实现。
-
每个IO线程有一个event loop(或者叫Reactor)用于处理读写和定时事件。
Leader/Follower等高级模式。
-
匿名管道(pipe);
用来异步唤醒select(或等价的poll或epoll_wait)调用。
-
共享内存是消息协议,a进行填好一块内存让b进程来读,基本上是停等方式(stop wait).
-
TCP是双向的,管道pipe是单向的(进程间双向通信需要打开两个文件描述符,父子进程才能用pipe).
-
互斥器(mutex);
分布式系统中使用TCP长连接通信
- 分布式系统是由运行在多台机器上的多个进程组成的,进程间采用TCP长连接通信(建立连接后不会立即关闭)。
- 在实现每一类服务器进程时,在必要时可以通过多线程提高性能。
- 对整个分布式系统,要做到能scale out, 即享受增加机器带来的好处。
- TCP长连接的好处:
容易定位分布式系统中服务之间的依赖关系。 ---
netstat -tpna | grep :port
客户端用netstat或lsof找到那个进程发起的连接。
netstat -tn观察Recv-Q和Send-Q的变化情况。
-
提高响应速度,让IO和计算相互重叠,降低latency.
C++多线程系统编程精要
- 多线程编程面临的最大思维方式的转变有两点: 当前线程可能随时会被切换出去,或者说被抢占(preempt)了。
- 多线程程序中,事件的发生顺序不再有全局同喜的先后关系。
-
Linux系统本身是可以被抢占的(preemptive).
-
全局对象不能创建线程。
-
Thread的析构不会等待线程结束。
-
__thread变量是每个线程有一份独立实体,各个线程的变量值都互不干扰。
-
每个文件描述符只由一个线程操作。
-
父进程的内存锁: mlock,mlockall.
-
fork之后,除了当前线程之外,其他线程都消失了。
高效的多线程日志
- 日志可以分为两类: 诊断日志(diagnostic log), 用于故障诊断和追踪(trace), 也可用于性能分析。 每条日志都要有对应的时间戳。
- 生产者-消费者模型: 对生产者(前端)而言,要尽量做到低延迟、低CPU开销、无阻塞;对消费者(后端)而言,要做到足够大的吞吐量,并占用较少的资源。
- 整个程序最好使用相同的日志库(库程序和主程序)。 --- 日志库最好是一个单例(singleton).
日志功能的需求
- 日志消息有多种级别(level): TRACE, DEBUG, INFO, WARN, ERROR,FATAL等。
- 日志消息的格式可配置(layout)。
- 日志消息可能有多个目的地(appender),如文件、socket,SMTP等。
- 可以设置运行时过滤器(filter),控制不同组件的日志消息的级别和目的地。
- 日志的输出级别需要在运行时进行动态调整(不需要重新编译,也不要重新启动进程)。
- muduo库只要调用muduo::Logger::setLogLevel()就能即时生效。
- 分布式系统中,日志的目的地(destination)只有一个: 本地文件。
-
因为诊断日志的功能之一就是诊断网络故障:
链接断开(网卡或交换机故障);
- 网络暂时不通(若干秒之内没有心跳消息);
- 网络拥塞(消息延迟明显加大)等。
- 也应该避免往网络文件系统(NFS)上写日志。
- 日志回滚(rolling): 回滚(rolling)通常具有两个条件: 文件大小(如写满1GB就换下一个文件);
- 时间(如每天零点新建一个日志文件,不论前一个文件有没有写满)。
每条内存中的日志消息都带有cookie(或者叫哨兵值/sentry),其值为某个函数地址,通过core dump查找cookie就能找到尚未来得及写入磁盘的消息。
-
时间戳字符串中的日期和时间两部分是缓存的,一秒内的多条日志只需要重新格式化微妙部分。
多线程异步日志
- 多线程写多个文件也不一定会提速,所以尽量一个进程的多线程写一个文件。 用一个背景线程负责收集日志消息,并写入日志文件,其他线程只管往这个日志线程发送日志消息,这称为"异步消息"("非阻塞日志")。
-
准备两块buffer: A和B, 前端负责往A填数据(日志消息),后端负责将buffer B的数据写入文件;
- 这么做的好处是: 新建日志消息的时候不必等待磁盘文件操作,也避免每条新日志消息都触发(唤醒)后端日志线程。
- 即便buffer A未填满,日志库也会每3秒执行一次交换写入操作。
muduo网络库简介
- 高级语言(Java, Python等)Socket库并没有对Sockets API提供更高层的封装,直接调用很容易掉入到陷阱中;网络库的价值在于能方便地处理并发连接。
- muduo使用了较新的系统调用(主要是timefd和eventfd),要求linux内核的版本大于2.6.28.
- muduo是基于Reactor模式的网络库,其核心是个时间循环EventLoop, 用于响应计时器和IO事件。
- muduo采用基于对象而非面向对象的设计风格,其事件回调接口多以boost::function+boost::bind表达。
- muduo主要掌握关键的5个类: Buffer, EventLoop, TcpConnection, TcpClient, TcpSever.
.
. - 一个文件描述符(file descriptor)只能由一个线程读写。
- muduo支持非并发阻塞TCP网络编程,它的核心是每个IO线程一个事件循环,把IO事件分发到回调函数上。减少网络编程中的偶发复杂性(accidental complexity).
- muduo擅长的领域是TCP长连接(建立连接后一直收发、处理数据)。
TCP网络编程最本质的是处理三个半事件:
- 连接的建立: 服务端成功接受(accept)新连接和客户端成功发起(connect)连接。
- 连接的断开: 包括主动断开(close、shutdown)和被动断开(read(2) 返回0)。
- 消息到达,文件描述符可读: 对它的处理决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计等)。
- 消息发送完毕,这算半个: 发送完毕是指数据写入操作系统的缓冲区,将由TCP协议栈负责数据的发送与重传,不代表对方已经收到了数据。
在一个端口上提供服务,并且要发挥多核处理器的计算能力
- 高性能httpd(httpd是一个开源软件,且一般用作web服务器来使用)普遍采用的是单线程Reactor方式。
- 推荐的C++多线程服务端编程模式: one loop per thread + threadpool. event loop 用作non-blocking IO和定时器。
- threadpool用来做计算,具体可以是任务队列或生产者消费者队列。
muduo编程示例
- daytime是短连接协议,在发送完当前时间后,由服务端主动断开连接。
- 非阻塞网络编程必须在用户态使用接收缓冲区。
- TcpConnection对象表示一次TCP连接,连接断开后不能重建,重试后连接的会是另一个TcpConnection对象。
- Chargen协议很特殊,它只发送数据,不接收数据,而且,它发送数据的速度不能快过客户端接收的速度。
- 在非阻塞网络编程种,发送消息通常是由网络库完成,用户代码不会直接调用write或send等系统调用。
- TCP是一个全双工协议,同一个文件描述符即可读又可写,shutdownWrite()关闭了"写"方向的连接,保留了"读方向"的连接,这称为TCP半关闭(half-close).
如果直接close(socket_fd), 那么sock_fd就不能读或写了。
.
-
分包指的是在发生一个消息(message)或一帧(frame)数据时,通过一定的处理,让接收方能从字节流种识别并截取(还原)一个个消息。
-
消息长度固定。
-
IO线程只阻塞在IO multiplexing(复用)函数上(如select/poll/epoll_wait等)。
-
为了与传统的poll兼容,在文件描述符较少,活动文件描述符比例较高时,epoll不见得比poll更高效。
- 必要时可以在进程中切换Poller.
- 水平触发(level trigger)编程更加容易,不可能发生漏掉事件的bug.
- 读写的时候不必等候出现EAGAIN, 可以节省系统调用次数,降低延迟。
-
在栈上准备一个65536字节的额外缓存(extrabuf), 利用readv来读取数据,iovec有两块,第一块指向muduo的Buffer中的可写字节,另一块指向extrabuf。
数据不多,直接存到Buffer中,如果较多,剩余的放到extrabuf中进行缓存,然后再存到Buffer中。
FILE是一个在stdio.h中预先定义的一个文件类型.
typedef struct{ short level; /*缓冲区“满/空”的程度*/ unsigned flags; /*文件状态标志字*/ char fd; unsigned char hold; short bsize; /*缓冲区大小*/ unsigned char *buffer; /*数据缓冲区的位置*/ unsigned char *curp; /*当前读写位置指针*/ unsigned istemp; short token; }FILE;
-
boost::any可以表示任意类型,所以boost::any用不了多态的特性。
-
Buffer内部是一个std::vector,它是一块连续的内存,参考netty的ChannelBuffer(prependable是微创新)。
如果readIndex太靠右,就不会重新分配内存,而是把已有数据移动到前面,腾出writable空间。
-
实现分段连续的zero copy buffer再配合gather scatter IO(mbuf方案, Linux的sk_buff方案),基本思路是不要求数据在内存中是连续的,而是用链表把数据连接到一起。
一种自动反射消息类型的Google Protobuf网络传输方案
- Google Protocol Buffers(简称Protobuf)是一款非常优秀的库,它定义了一种紧凑的可扩展二进制消息格式,特别适合网络数据传输。
拿到Message*指针,不用知道它的具体类型,就能创建和其他类型一样的具体Message type的对象。 - 通过DescriptorPool可以根据type name查到Descriptor*, 再调用DescriptorPool::findMessageTypeByName(const string& type_name)即可。
-
用DescriptorPool::generated_pool()找到一个DescriptorPool对象(它包含了编译时所连接的全部Protobuf Message types).
-
返回的是动态对象,调用方需要释放它,可以使用智能指针管理资源。(消息分发器dispatcher)
-
adler32校验算法,计算量小,速度比较快,强度和CRC-32差不多。
-
codec(编解码器)的基本功能是TCP分包: 确定每条消息的长度,为消息划分界限。
-
计时只使用gettimeofday(2)来获取当前时间。 --- 精度为1微妙。
-
NTP协议进行时间校准。
-
timing wheel只用检查第一个桶中的连接。
-
可以增加多个Subscriber而不用修改Subscriber(分布式的观察者模式Observer pattern).
·sub<topic>\r\n, 表示订阅, 以后该topic有任何更新都会发给这个TCP连接。 Hub会把上最近的消息发给此Subscriber.
unsub<topic>\r\n, 表示退订.
pub<topic>\r\n<content>\r\n, 表示往发送消息,内容为.
-
连接服务器把多个客户连接汇聚为一个内部TCP连接,起到数据串并转换的作用,后端(backend)的逻辑服务器专心处理业务。
-
当client connection到达或断开时,向backend发出通知。
-
Sockets API来实现TcpRelay,需要splice系统调用。
短址服务
- muduo HTTP服务器可以处理简单的HTTP请求,也可以用来实现一个简单的短URL转发服务。
一种真正高效的优化手段是修改Linux内核,例如Google的SO_REUSEPORT内核补丁。
- muduo的Channel class类,可以把其他一些现成的网络库融入muduo的event loop中。 Channel class是IO事件回调的分发器(dispatcher), 它在handleEvent()中根据事件的具体类型分别回调ReadCallback, WriteCallback等。
- 每个Channel对象服务于一个文件描述符,但并不拥有fd, 在析构函数中也不会close(fd).
- Channel与EventLoop的内部交互有两个函数: EventLoop::updateChannel(Channel*);
- EventLoop::removeChannel(Channel*).
- 客户需要在Channel析构前自己调用Channel::remove().
POSIX操作系统总是选用当前最小可用的文件描述符。
muduo库设计与实现
- EventLoop的析构函数会记住本对象所属的线程(threadId_), 创建了EventLoop的线程是IO线程。 其主要同能时运行事件循环EventLoop::loop().
- EventLoop对象的生命期通常和其所属的线程一样长,不必是heap对象。
-
TCPNoDelay的作用是禁用Nagle算法,避免连续发包出现延迟,对编写低延迟网络服务很重要。
分布式系统工程实践
- 分布式系统设计以进程为基本单位.
- 不要把时间浪费在解决错误的问题,应集中精力应付更本质的业务问题。
- 只用TCP为进程间通信,因为进程一退出,连接与端口自动关闭;而且无论连接的哪一方断连,都可以重建TCP连接,恢复通信。
- 分布式系统中心跳协议的设计: 心跳除了说明应用程序还活着(进程还在,网络畅通), 更重要的是表明应用程序还能正常工作。
- TCP keepalive由操作系统负责探查,即便进程死锁或阻塞,操作系统也会如常收发TCP keepalive消息。对方无法得知这一异常。
- 一般是服务端向客户端发送心跳。
- Sender和Receiver的计时器是独立的。
- 心跳协议的内在矛盾: 高置信度与低反应时间不可兼得。
- timeout的选择要能容忍网络消息延时波动和定时器的波动。
发送周期和检查周期均为\(T{_C}\), 通常可取\(timeout=2T{_C}\).
-
要在工作线程中发送,不要单独起一个心跳线程。
-
设置SO_REUSEADDR, 为了快速重启。
-
每种service都内置了HTTP状态页面。
-
可扩展消息格式的第一条原则是避免协议的版本号。
-
proto文件就像C/C++动态库的头文件,其中定义的消息就是库(分布式服务)的接口,一单发布就不能做有损二进制兼容性的修改。
-
自动化测试的必要性:
自动化测试的作用是把程序已经事项的features以test case的形式固话下来,将来任何代码改动如果破坏了现有功能需求就会触发测试failure.
-
测试进程间交互。
-
HDFS有四个角色参与其中: NameNode(保存元数据)、DataNode(存储节点,多个)、Secondary NameNode(定期写check point)、Client(客户,系统的使用者)。
-
发复不间断发送请求,向被测程序加压,用C++写一个负载生成器。
分布式系统部署、监控与进程管理的几重境界
- 以Host指代服务器硬件。
- 境界1: 全手工操作,过家家级别,系统时灵时不灵,可以跑跑测试,发发parper.
- 境界2: 使用零散的自动化脚本和第三方组件
- 公司的开发中心放在实现核心业务,天健新功能方面,暂时还顾不上高效的运维。
- host的IP地址由DHCP配置,公司的软硬件配置比较统一。
- 使用cron、at、logrotate、rrdtool等标准的Linux工具来将部分运维任务自动化. QA签署后部署(md5), md5sum检查拷贝之后的文件是否与源文件相同。
- Monit开源工具进行监控(内存、CPU、磁盘空间、网络带宽等)。
netstart-tpn | grep port(端口号)
查询哪些用到了程序。
- 境界3: 自制机制管理系统,几种化配置
- 境界4: 机群管理与nameing service结合:
- naming service的功能是把一个service_name解析成list of ip:port,比方说,查询"sudo_solver",返回host1:9981、host2:9981、host3:9981.
- naming_servive与DNS最大的不同在于它能把新的地址信息推送给客户端。
- gethostbyname()和getaddrinfo()解析DNS是阻塞的(除非使用UDNS等异步DNS库),在大规模分布式系统中DNS的作用不大,宁愿花时间实现一个naming service,并为它编写name resolve library.
C++编译链接模型精要
- C++语言的三大约束: 与C兼容、零开销(zero overhead)原则、值语义。
- 查看编译时打开的文件命令:
strace -f -e open cpp hello.cc -o /dev/null 2>&1 |grep -v ENONT|awk {'print $3'}
- C++也继承了单遍编译的约束,Java编译器不受单遍编译的约束,调整成员函数的顺序不会影响代码语义。
- 按照C++模板的局限话规则,编译期会为每一个用到的类模板成员函数具现化一份实例。
- 在现在的C++实现中,虚函数的动态调用(动态绑定、运行期决议)是通过虚函数表(vtable)进行的,每个多态class都应该有一根vtable. 定义或继承了虚函数的对象中会有一个隐含成员:指向vtable的指针,即vptr。
- 在构造和析构对象的时候,编译期生成的代码会修改这个vptr成员,这就要用到vtable的定义(使用其地址)。
相关文章推荐
- linux基础编程 套接字socket 完整的服务器端多线程socket程序
- 赖勇浩:推荐《Linux 多线程服务器端编程》
- linux基础编程 套接字socket 完整的服务器端多线程socket程序
- TCP/IP网络编程 基于Linux编程_4 --多线程服务器端的实现
- TCP/IP网络编程 基于Linux编程_4 --多线程服务器端的实现
- linux基础编程 套接字socket 完整的服务器端多线程socket程序【转】
- {网络编程}和{多线程}应用:基于TCP协议【实现多个客户端发送文件给一个服务器端】--练习
- Linux编程练习 --多线程1--线程创建
- Linux下多线程编程简介(二)
- Linux网络编程【三】:TCP服务器多进程和多线程(http访问)版本
- Linux下的多线程编程
- Linux操作系统下的多线程编程详细解析----条件变量pthread_cond_t那些事儿
- 【原创】《Linux高级程序设计》杨宗德著 - Linux多线程编程 - 线程概念及创建线程 分类: Linux --- 应用程序设计 2014-11-19 17:31 82人阅读 评论(0) 收藏
- Linux C 多线程编程 互斥锁
- Linux下的基于Pthread的多线程Socket编程
- 浅谈 linux 多线程编程和 windows 多线程编程的异同
- Linux 网络编程四(socket多线程升级版)
- 浅谈 linux 多线程编程和 windows 多线程编程的异同
- Linux操作系统下的多线程编程详细解析----条件变量
- Linux多线程编程---线程间同步(互斥锁、条件变量、信号量和读写锁)