程序的任务类型
CPU密集型
计算类:数值比较,数据计算,逻辑处理等。
特点是:需要进行大量的计算,消耗CPU资源。
选择的线程数 = CPU核心数 : CPU密集型任务虽然可以用多任务处理,但是任务越多,任务之间切换的时间就越多,CPU的执行效率反而会变低。所以要高效地利用CPU,任务的并行数应该等于CPU的核心数,避免任务在CPU之间频繁切换。
IO密集型
涉及大量的网络或磁盘的输入输出操作:数据库连接,网络通信。
特点是:CPU资源消耗少,任务的大部分时间都是在等待IO操作的完成。(IO的速度远远低于CPU和内存的速度)。
服务模型
不管是CPU密集型任务还是IO密集型任务,要提高服务器的处理能力,可以从软件和硬件两个层面做文章。
软件层面
单个任务处理能力有限,此时可以通过启动多个功能完全相同的服务实例,借此来提高服务整体处理性能。
多服务实例的实现主流技术有三种:多进程、多线程、多协程。
除了多实例的方式,还有IO多路复用、异步IO等技术。
多进程服务模型
进程概念
多进程模型
常见的进程间的通信方式
管道
父子进程、循环队列、每条数据只允许读一次,队列,先进先出。
读空和写满情况下,相关的进程会进入等待队列。
半双工通信,数据只能单向流动。
命名管道
不局限于亲缘关系的进程间的通信,只要命名管道提供一个路径名与之关联,以文件形式存在于文件系统中。
这样即使不存在亲缘关系的进程,只要可以访问该路径也能互相通信。
可靠、单向、双向数据通信。
信号量
信号可以在任何时候发给某一个进程,无需知道该进程的状态。如果该进程不是运行态,内核会暂时保存信号,当进程恢复执行后传递给它。
如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
信号在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:
- 硬件来源:用户按键输入
Ctrl+C
退出、硬件异常如无效的存储访问等。 - 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。
消息队列
是存放在内核中的消息链表,每个消息队列由消息队列标识符表示,只有在内核重启或主动删除时,该消息队列才会被删除。
克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限的缺点。
另外,某个进程往一个消息队列写入消息之前,并不需要另外读进程在该队列上等待消息的到达。
共享内存
一个进程把地址空间的一段,映射到能被其他进程所访问的内存。
一个进程创建,多个进程访问,进程可以直接读写这一块内存而不需要进行数据拷贝,从而大大提高效率。
共享内存往往与其他通信机制,如信号量配合使用,来实现进程间的同步和互斥通信。
套接字 Socket
进程间通信的一种方式。
熟悉的TCP/IP协议栈,是建立在socket通信上的,TCP/IP构建了当前的互联网通信网络。
套接字是一种通信机制,通过这种机制,既可以在本机进程间通信,也可以跨网络通信。因为,套接字通过网络接口将数据发送到本机的不同进程或远程计算机的进程。
多线程服务模型
线程概念
多线程模型
一个进程内的多个线程可以共享进程的全部系统资源。进程内创建的多个线程都可以访问进程内的全局变量。
但多线程访问公共资源会带来同步和互斥的问题,不同线程访问资源的先后顺序会互相影响,如果不做好同步和互斥会产生预期之外的结果,甚至死锁。
多线程同步
一个线程的执行需要等待另外一个线程的通知。如果没有得到另外一个线程的通知就必须等待,知道消息到达时才被唤醒。—— 即有很强的执行先后的关系。
多线程互斥
多线程对共享资源访问的排他性(使用某一个共享资源的时候,任何时刻最多只允许一个线程获得对这个共享资源的使用权,当这个共享资源被其中一个线程占有时,其他未获得资源的线程必须等待,知道占用资源的线程释放了线程)。
举例: (线程)学生、老师 —-> 共享资源(投影仪)
多线程同步和互斥方法
互斥锁
互斥锁作用是对临界区加以保护,使得任意时刻只有一个线程能够执行临界区的代码,实现了多线程对临界资源的互斥访问。
条件变量
条件变量是用来等待而不是用来上锁的。条件变量会自动阻塞一个线程,知道某些特殊情况发生为止。
适合多个线程等待某个条件的发生。如果不使用条件变量,那么每个线程就不断尝试互斥锁并检测条件是否发生,浪费系统资源。
读写锁
互斥锁要么是加锁状态,要么是不枷锁状态,而且一次只有一个线程对其进行加锁。
读写锁可以有3种状态,读加锁状态,写加锁状态和不加锁状态。
一次只能有一个线程占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。
(读锁和写锁互斥,读锁之间不互斥)
读写锁适合于对数据结构的读次数比写次数多得多的情况,且读写锁比互斥锁具有更高的并行性。
自旋锁**
互斥锁在得不到锁的时候,当前线程会进入休眠状态,引发任务的上下文切换。任务切换会涉及到一系列耗时的操作,因此互斥锁一旦遇到阻塞切换代价是十分昂贵的。
自旋锁的阻塞不会引起线程的上下文切换,当锁被其他线程占有时,获取锁的线程会进入自选,不断检测自旋锁的状态,直到得到锁。所谓的自旋就是循环等待的意思。
自旋锁适用于临界区代码比较短,锁的持有时间比较短的场景,否则会让其他线程一直等待造成饥饿现象。
信号量**
本质是一个非负的整数计数器,用来控制对公共资源的访问。
信号量是一种特殊类型的变量,可以被增加或者减少。可以根据操作信号量值的结果判断是否对公共资源具有访问的权限。当信号量值大于0时,则可以访问,否则将阻塞。
对信号量的访问被保证是原子操作,在一个多线程的程序里也是如此。
信号量的类型
- 二进制信号量
只有0和1两种取值。适用于临界代码每次只能被一个执行线程运行,就要用到二进制信号量。
- 计数信号量
可以有更大的取值范围。适用于临界代码允许有限数目的线程去执行,就要用到计数信号量。