1. 进程
1.1队列
1.2生产者消费者模型
多进程是用来解决计算型的程序用
2.线程
2.1理论
- 线程是执行程序的
- 线程是计算机中CPU调度的最小单位
为什么要有线程- 为了节省操作系统的资源
- 在实现并发的时候,能够减少开启,销毁进程时间开销
- 效率差:线程比进程的开启和销毁消耗的时间长
2.2python中使用多线程
2.3特点
进程和线程区别:
- 1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
- 2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
- 3)调度和切换:线程上下文切换比进程上下文切换要快得多。
- 4)在多线程操作系统中,进程不是一个可执行的实体。
缺点:
解决缺点:多进程+多线程
线程不能被强制终止
2.4守护线程
2.5锁
2.5.1互斥锁
- 互斥锁最节省资源的
2.5.2递归锁
- 为了临时解决上线问题
2.5.3死锁
3.池
如果任务多的情况下,无限的开启进程、线程不仅会浪费非常多的时间来开启和销毁,还需要占用系统的调度资源。
如果我开启有限的线程、进程,来完成无限的任务,这样能够最大化的保证并发,且维护操作系统资源的协调。
- 任务数超过了CPU个数的2倍
- 进程的个数就不应该和任务数相等了
- 任务数超过了CPU个数的5倍
- 线程的个数就不应该和任务数相等了
现在用线程和进程池:
from concurrent.futures import ThreadPoolExecuter
from concurrent.futures import ProcessPoolExecuter
3.1进程池
3.2线程池
4.协程
- 一条线程分成几个任务执行,每个任务执行一会,在切到下一个任务。
- 切换任务是由程序来完成的,不是由操作系统控制的。
- 如果在执行任务的过程中是遇到阻塞才切换,是能够节约时间的
- 协程本质是一条线程
- 多个协程也是宏观上的并发
- 协程是数据安全的,不需要考虑锁
优点如下:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
总结协程特点:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
from gevent import monkey;monkey.patch_all() #from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前
或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
import gevent
import time
def eat():
print('eat food 1')
time.sleep(2)
print('eat food 2')
def play():
print('play 1')
time.sleep(1)
print('play 2')
g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)
gevent.joinall([g1,g2])
print('main')
5.IO模型
刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
等待数据准备 (Waiting for the data to be ready)
将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。阻塞 I/O(blocking IO)
非阻塞 I/O(nonblocking IO)
I/O 多路复用( IO multiplexing)
信号驱动 I/O( signal driven IO)
异步 I/O(asynchronous IO)
注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。
典型的读操作流程大概是这样:
>
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
非阻塞 I/O(nonblocking IO)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
5.1多路复用IO
6.线程队列
import queue
q = queue.Queue()
# 在线程之间数据安全,自带线程锁的数据容器
lq = queue.LifoQueue()
# 栈,先进后出
lq.put(1)
print(lq.get())
qp = queue.PriorityQueue() #优先级队列
qp.put(2)
qp.put(3)
qp.get() # 输出2