现在的位置: 首页 > 综合 > 正文

Linux多线程服务端编程(笔记3_4)

2013年10月17日 ⁄ 综合 ⁄ 共 7438字 ⁄ 字号 评论关闭

    每个进程都有自己的独立地址空间,线程的特点是共享地址空间从而可以高效的共享数据。
    select和poll用来,支持Unix中I/O复用的功能,在Unix中I/O模型可以分为以一几种:阻塞IO应用进程产生一个system call ,如果内核没有数据准备好,则会一直wait,处于阻塞,当内核数据准备好之后,将会把数据从内核再拷贝到应用进程,这一copy过程也处于阻塞状态;非阻塞I/O,就意味着当应用进程产生一个system call的时候,不管内核的数据是否准备好,都会立即返回。而后,再一次发起call,这是一个轮询的过程。当内核数据准备好之后,便可以正常进行响应。这一过程是非阻塞的。而当数据从内核copy到应用进程的过程,仍然是阻塞,应为要保证数据完整与一致;I/O复用,一个或多个
system call 阻塞于select 或是 poll,而不是阻塞与真正的调用。当内核有数据准备好的时候,会通知select或是poll,接下来,会发起真正的system call,也就是图片中的recvfrom。之后,便会正常copy数据到应用进程。值得注意的是,I/O复用产生了两次system call,一次select(poll),一次recvfrom。因此,如果进程只是处理单一描述字(descriptor)的话,使用I/O复用不但不会有好的效果,而且还会有额外的系统开销,所以,I/O复用一般都用于处理多个描述字(descriptors)的情况下;信号驱动I/O,当有描述字准备好后,内核会产生信号来通知应用进程。信号驱动模型不同于上述三种,对于应用进程而言,它在等待接受数据过程中,处于被通知状态。这一过程,相当于一个异步操作。但是,对于内核copy数据到应用进程这一过程,应用进程仍然处于阻塞的状态
     select函数该函数允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或是多个事件发生或经历一段指定的时间后才唤醒它。我们调用select告知内核对哪些描述字(就读、写或异常条件)感兴趣以及等待多长时间。我们感兴趣的描述字不局限于套接口,任何描述字都可以使用select来测试。
poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。
    任务队列:能够把封装了数据和操作的任务在多线程间传递的线程安全的先入先出的队列
    BlockingQueue<T>是多线程编程的利器,用阻塞队列实现任务队列
    线程池就是以一个或多个线程[循环执行]多个应用逻辑的线程集合,应该是循环执行多个逻辑单元.也就是有一批要执行的任务,这些任务被独立为多个不同的执行单元。一般而言,线程池有以下几个部分:1.完成主要任务的一个或多个线程.2.用于调度管理的管理线程.3.要求执行的任务队列.线程池属于对象池.所有对象池都具有一个非常重要的共性,就是为了最大程度复用对象,为了减少创建和切换线程的额外开销,利用已经的线程多次循环执行多个任务从而提高系统的处理能力。一个线程池至少应该具有以下几个方面的功能:1.提供一个任务接口以便用户加入任务,2.工作线程,把用来执行用户任务的线程称为工作线程,工作线程就是不断从队列中获取任务对象并执行对象上的业务方法,3需要有一个对工作线程的调度线程,完成以下几个功能:1.生成需要的工作线程.由于创建线程需要一定的开销,一定要注意所创建的所有线程不能超一个设定的最大值.建议最大值不要超25,2.动态自适应调整集合中线程数.当有太多的线程处于闲置状态时(队列中没有任务),应该按一定比例销毁闲置了一定时的线程,如果队列中任务队列积压太多而工作线程总数没有超最大线程数时应该及时创建工作线程直至达到是大值,3.需要一个专门的后台线程定时扫描队列中任务与正在工作的线程总数,闲置的线程总数.
    c++多线程服务端编程模式:one(event ) loop per thread+thread pool,event loop用作IO多路复用,thread pool用来做计算具体可以是任务队列或生产者消费者队列
    阻抗匹配:cpu和IO都能高效的工作,程序里具体使用几个loop,线程池的大小都需要根据应用设定
    event loop:所有的事件处理函数,以及timer执行的函数,会排在一个queue结构中,利用一个无穷回圈,不断从queue中取出函数来执行。
    callback在形式上,其实就是把函数A传给函数,然后在适当的时机呼叫传入的函数A
    进程间通信选用TCP可以跨主机具有伸缩性,TCP是文件操作描述符
    eventfd用来实现,进程(线程)间 的 等待/通知(wait/notify) 机制,是一个比 pipe 更高效的线程间事件通知机制,一方面它比 pipe 少用一个 file descriper,节省了资源;另一方面,eventfd 的缓冲区管理也简单得多,建一个类似管道的东西,但比管道更简单,它的读和写缓冲区只有8个字节,它会通过eventfd创建一个描述符fd,用于线程或进程间通信。简单来说,就是进程A被write一个n,那么进程B可以通过read读到这个n,当然在使用过程中,n是A和B之间协商的一个有意义的数字,eventfd()创建一个“eventfd对象”,这个对象能被用户空间应用用作一个事件等待/响应机制,靠内核去响应用户空间应用事件。这个对象包含一个由内核保持的无符号64位整型计数器。这个计数器由参数initval说明的值来初始化。
    Reacotor模式:Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的时间发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。某程序需要某个操作但是暂时得不到则注册一个回调函数等事件发生了通知回调函数处理数据。Proactor是基于异步I/O,Reactor是同步I/O(一般是I/O复用)。以读操作为例:1. 应用程序注册读就绪事件和相关联的事件处理器2. 事件分离器等待事件的发生3. 当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器4.
事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
    I/O复用模型:调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。用于多个文件描述符
    必须使用单线程:程序可能fork;限制程序的cpu占用率(辅助程序)
    event loop(时间循环):poll查看事件是否到来,若有事件则执行事件回调函数。事件循环有个显著缺点:非抢占的,可能使得事件优先级反转
    如果用很少的cpu负载就能让IO跑满或者用很少的IO流量就能让CPU跑满那么多线程没啥用处,无论是IO限制还是CPU限制的环境中单线程都占优势
多核机器上提供服务可用的模式有:运行一个单线程的进程;运行一个多线程的进程pthread_create;运行多个单线程的进程fork(简单将第一个中的进程运行多份;主进程+work进程);运行多个多线程的进程
    多线程的适用场景:提高响应速度,让IO和计算相互重叠降低延迟,提高平均响应性能
    一个多线程服务程序中的线程大致分为:IO线程,主循环是IO多路复用阻塞在select系统调用上;计算线程主循环是阻塞队列,阻塞在条件变量上,一般位于线程池中;第三方库所用的线程
    one loop per thread模型并发连接数远低于基于事件的IO多路复用事件循环模型
    多线程让IO和计算相互重叠:把IO操作通过阻塞队列交给别的线程去做自己不必等待,通过将数据传给阻塞队列降低了延迟
    Memcached 是一个高性能的分布式内存对象缓存系统,它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提供动态、数据库驱动网站的速度。
    分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)
Rector只是告诉调用者什么时候事件到来,但是需要进行什么操作,需要调用者自己处理。Preactor不是当事件到来时通知,而是针对此事件对应的操作完成时,通知调用者,一般通知方式都是异步回调。
    基本线程原语:create,join,mutex,cond_t三大件;pthead_once不如全局变量;读写锁和信号量(与条件变量相同)慎用;pthread_cancel/kill表明设计出了问题
    内存能见度:一个线程对某个共享变量的修改何时能被其他线程看见
    系统调用的对于用户态程序来说是原子的所逼不必担心线程安全性,但是要注意系统调用对于内核状态的改变可能影响其他线程。
    immutable类:类的实例是不可被修改的。实例的信息是在创建的时候提供,并且在整个生命周期中都不可改变。 
    没有合适的内存模型是无法多线程的,线程安全的函数调用组合一起不一定线程安全,将类设计成immutable可以提供线程安全的接口但是却不能总这样做,标准容器和string不是线程安全的,泛型算法只要保证输入区间是线程安全则安全(无状态纯函数),iostream不是线程安全的可以改用printf达到安全性和输出原子性但是等于用了全局锁(任何时刻只能有一个线程调用printf,因此高效的日志需要特殊设计)
    pthread_slef返回pthread_t类型是不确定的,且只有pthread_equal能比较两个线程标示符是否相等,这就一系列问题:没法在日志中打印pthread_t,pthread_t无法作为键值,Mutex无法有效判断当前线程是否持有本锁,pthread_t只在进程内有效很容易重复唯一性不能保证。因此pthread_t不适合作为线程标示符
    gettid()返回pid_t是个小整数,具有全局唯一性可以替代pthread_t,但是每次都系统调用获取pid_t,可以使用__thread变量来缓存gettid()的返回值在本线程第一次系统调用的时候就缓存这个值。若程序执行了fork则会进程这个缓存值,这时候要注册一个回调函数清空这个缓存值
线程创建的规则:程序不应该在未提前告知的前提下创建自己的背景线程,尽量用相同的方式创建线程,在进入main函数之前不应该启动线程(无论如何全局对象的构造都是顺序进行且在主线程中,若启动了其它线程访问未经初始化的全局对象或者更改全局对象就危险了),线程的创建最好能在初始化阶段全部完成。thread class,ThreadManager singleton class的实现
    异步回调:线程A需要线程B执行某种方法method但是B不能马上响应A,A将method函数指针传给B,B执行完自己的任务后回调method(这过程中A不阻塞并且继续执行自己的往后的任务)
    一个服务程序的线程数目应该与当前负载无关而与机器cpu相关,特别是有实时性的要求时线程数目不应该超过cpu数目。最好在程序的初始化阶段创建全部工作线程在程序运行期间不再创建或销毁线程(利用线程池和事件循环技术)
    线程销毁的几种方式:线程主函数返回(自然死亡);线程主函数跑出异常(非正常死亡);pthread_exit(自杀);其它线程调用pthread_cancel(他杀)。正常退出只有自然死亡一种清形,其它的不管是自杀还是他杀都没有机会清理资源,更没有机会释放已持有的锁
    joinable:线程结束的时候,线程相关的资源(线程的句柄、栈等等)依然存在,没有被自动释放。等待其它线程去释放这些资源,或者主进程结束时自动回收。joinunable:伴随着线程的结束,线程相关的资源也被自动地释放。所以“joinable”的线程,如果在程序运行过程中结束退出,要注意它的资源有可能并没有被释放掉。很多时候因为没释放的资源比较少,我们难以察觉。但是如果该线程反复的创建、结束,那么最后会耗尽系统资源。那么为了解决这个问题,我们可以使用以下两种手段,安全的结束线程:线程创建的时候将其属性指定为“joinunable”(detach)就可以了对于“joinable”的线程,我们可以在其结束(exit)前调用“pthread_detach”函数
    如果确实要强行终止一个耗时很长的计算任务而又不想在计算期间周期性的检查某个全局退出标志,那么可以考虑把那部分代码(计算部分)fork为新的进程,这样杀掉一个进程比杀本进程内的线程安全多了
    如果能做到程序中线程的创建最好能在初始化阶段全部完成则线程是不必销毁的,这就避免了线程退出的各种困难(资源释放,对象生命期等)
要注意的是 pthread_cancel 调用并不等待线程终止,它只提出请求。线程在取消请求(pthread_cancel)发出后会继续运行,直到到达某个取消点(Cancellation Point)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。几个 POSIX 线程函数是取消点:pthread_join(3) pthread_cond_wait(3) pthread_cond_timedwait(3) pthread_testcancel(3) sem_wait(3) sigwait(3)以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点,线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。最经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中被外界取消,如果线程处于响应取消状态,且采用异步方式响应,或者在打开独占锁以前的运行路径上存在取消点,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。
    exit除了终止进程还会析构全局对象(注意不是局部对象)和已经构造完的函数静态对象,这会出现潜在的死锁
     用全局对象实现无状态策略在多线程中析构可能有危险。考虑用_exit()不会执行清理操作
     策略模式通常把一个系列的算法包装到一系列的策略类里面,作为一个抽象策略类的子类。用一句话来说,就是:“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
     POD对象,其二进制内容是可以随便复制的,在任何地方,只要其二进制内容在,就能还原出正确无误的POD对象。对于任何POD对象,都可以使用memset()函数或者其他类似的内存初始化函数平凡的类或结构定义如下:1.具有一个平凡的缺省构造器。(可以使用缺省构造器语法,如 SomeConstructor() = default;)2.具有一个平凡的拷贝构造器。(可以使用缺省构造器语法)3.具有一个平凡的拷贝赋值运算符。(可以使用缺省语法)4.具有一个非虚且平凡的析构器。
    __thread内置的线程局部存储设施,只能用于修饰POD类型不能修饰class,可以用于全局变量、函数内的静态变量,不能用于修饰函数的局部变量或者class的普通成员变量,__thread变量的初始化只能用编译期常量.__thread变量每个线程有一个独立实体(多份存在的意思),可以修饰那些“值可能变带有全局性但是又不值得用全局锁
护”的变量
     accept调用时,服务器端的程序会一直阻塞到有一个客户程序发出了连接. accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了
     多线程与IO:每个文件描述符只有一个线程操作,一个线程可以操作多个文件描述符但是不能操作别的线程拥有的文件描述符。同一个epoll fd的操作都放在一个线程中执行。有两个例外:对于磁盘文件可以多线程调用pwrite/pread读写同一个文件;UDP本身保证消息的原子性可以多线程同时读写同一个UDP文件描述符.不能关闭标准输出和标准错误文件描述符
     RAII包装文件描述符:由于linux内核非配文件描述符采用当前最小可用的方式导致可能出现多线程错误,用Socket封装文件描述符只要Socket对象还活着就不会有一样的描述符存在,而新的问题是对象生命周期的管理(shared_ptr)
程序不能单纯的持有TCP连接A的文件描述符,而应该持有封装A的TcpConnection对象,保证在处理request期间A的描述符不会被关闭。使用shared_ptr管理TcpConnection对象生命周期
     设法保证对象的构造和析构是成对的,否则几乎一定会有内存泄露,使用RAII封装手法把资源管理与对象生命期管理统一起来。但是遇见fork(对象构造了一次析构了多次)子程序会继承父进程的资源这种局面将会打破(对象恰巧封装某类资源没有被子进程继承),子进程不会继承:父进程的内存锁(不是pthread_mutex_t),文件锁,某些定时器,其它(见fork)
     多线程与fork:fork后的子进程只会可能父进程的主线程而父进程的其它线程丢失了,这就会造成错误(父进程的非主线程持有某个锁但是突然死亡未释放锁,子进程加锁必然死锁)。唯一安全的做法是:fork之后理解调用exec执行另一个程序彻底隔断父子进程之间的联系。
可重入是线程安全的子集,可重入是线程安全的充分非必要条件。可重入的函数一定是线程安全的,然过来则不成立
    asyc-signal-safe函数指被信号中断后能重入,因此不是每个线程安全的函数都能可重入的。信号分为两类:发送给某一线程和任一线程,特别是信号处理函数不能调用任何pthreads函数并且不能通过条件变量来通知其它线程。所以多线程中使用signal的第一原则是“不要使用signal”

抱歉!评论已关闭.