系统知识
计算机系统抽象组成: CPU + 存储器 + IO
时间片(timeslice)又称为 “量子”(quantum)或 “处理器片”(processor slice),是分时操作系统分配给每个正在运行的进程微观上的一段 CPU 时间(在抢占内核中是:从进程开始运行直到被抢占的时间)。
对于单核cpu同一时刻只能有一个任务运行。
1. 并发:交替执行(某时间段内的处理能力)
2. 并行:同时执行
线程(Thread):是操作系统最小的调度单位, 是一串指令的集合。
进程 (Process):是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
进程与线程
- 真正在cpu上运行的是线程。
- 线程共享内存空间;进程的内存是独立的。
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。进程的资源是独立的。
- 同一个进程的线程之间可以直接交流;两个进程想通信,必须通过一个中间代理来实现。
进程状态模型
多进程与多线程
线程
• 线程被称为轻量级进程(Lightweight Process,LWP),是cpu调度的基本单位。
• 组成:线程ID、当前指令指针(PC)、寄存器集合、堆栈组成。
• 在单个程序中同时运行多个线程完成不同的工作,称为多线程。
threading模块提供的常用类:
• Thread:创建线程
• Lock/RLock:互斥锁
Thread构造方法
构造方法: Thread(group=None, target=None, name=None, args=(), kwargs={})
- group: 线程组,目前还没有实现,库引用中提示必须是None;
- target: 要执行的方法;
- name: 线程名;
- args/kwargs: 要传入方法的参数。
Thread实例方法
- t.name:获取或设置线程的名称。
- t.getName()/setName(name): 获取/设置线程名。
- t.is_alive()、t.isAlive():判断线程是否为激活状态。返回线程是否在运行。正在运行指启动后、终止前。
- t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。
- t.run() :线程被cpu调度后自动执行线程对象的run方法。
- t.start(): 线程准备就绪,等待CPU调度,start会自动调用t.run()。
- t.join([timeout]): 阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的timeout(可选参数)。
- t.setDaemon(bool): 设置是后台线程(默认前台线程(False))。(在start之前设置)如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,主线程和后台线程均停止。如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止。
- t.isDaemon:判断是否为后台线程。
Lock
在多线程中使用lock可以让多个线程在共享资源的时候遵循一定的规则。
常见锁类型
• Lock()/RLock:普通锁(互斥锁)。解决资源争用,数据读取不一致等。
• Semaphore :信号量。最多允许同时N个线程执行内容。
• Event: 事件锁。根据状态位,决定是否通过事件。
• Condition: 条件锁。该机制会使得线程等待,只有满足某条件时,才释放n个线程。
Lock()/RLock
构造方法:Lock()。
实例方法:
• acquire([timeout]): 尝试获得锁定。使线程进入同步阻塞状态。
• release(): 释放锁。使用前线程必须已获得锁定,否则将抛出异常。
Semaphore
构造方法:Semaphore (N)。
实例方法:
• acquire([timeout]): 尝试获得锁定。使线程进入同步阻塞状态。
• release(): 释放锁。使用前线程必须已获得锁定,否则将抛出异常。
Event
- 事件机制:全局定义了一个“Flag”。
- 如果“Flag”的值为False,那么当程序执行wait方法时就会阻塞。
- 如果“Flag”值为True,那么wait方法时便不再阻塞。
- 这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有的排队中的线程。
- Event是线程间通信最间的机制之一。
- 一个线程发送一个event信号,其他的线程则等待这个信号。
用于主线程控制其他线程的执行。
实例方法:
• e.wait([timeout]) : 堵塞线程,直到Event对象内部标识位被设为True或超时(如果提供了参数timeout)。
• e.set() :将标识位设为Ture。
• e.clear() : 将标识伴设为False。
• e.isSet() :判断标识位是否为Ture。
Condition
实例方法:
• wait_for(func): 等待函数返回结果,如果结果为True-放行一个线程。
• wait、lock.notify(N): 一次性放行N个wait。
• acquire、release: 以上的wait和wait_for需要在锁中间使用。
GIL
Python GIL与多线程
• GIL全称Global Interpreter Lock(全局解释器锁)。
• GIL和Python语言没有任何关系,只是因为历史原因导致在官方推荐的解释器Cpython中遗留的问题(Jpython无此类问题)。
• 每个线程在执行的过程中都需要先获取GIL,保证同一时刻只有一个线程可以执行代码。
Python中多线程
GIL最基本的行为只有下面两个:
1. 当前执行的线程持有GIL。
2. 当线程遇到io阻塞时,会释放GIL。
由于GIL锁的限制,所以多线程不适合计算型任务,而更适合IO型任务。
进程与多进程
1. io密集型计算用多线程。
2. cpu密集型计算用多进程。
进程
概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位。
多个进程同时执行时,每个进程的执行都需要由操作系统按一定的算法(RR调度、优先数调度算法等)分配内存空间。
组成:进程控制块PCB、数据段、正文段。
基本状态:就绪状态、运行状态和阻塞状态。
创建:用户创建出来的所有进程都是由操作系统负责,新进程的创建都是由一个已经存在的进程执
行了一个用于创建进程的系统调用而创建的。
Linux中pid为0的进程,是所有进程的主进程。
如何创建子进程?
在python中,每一个运行的程序都有一个主进程,可以利用模块中封装的方法来创建子进程。
os.fork中就用来创建子进程的方法。
注意:这个os.fork()方法只有在unix系统中才会有,在window下没有。
os.fork:创建子进程
- 使用fork创建子进程后,操作系统会将当前的进程复制一份。
- 原来的进程称为父进程,新创建的进程称为子进程。
- 两个进程会各自互不干扰的执行下面的程序。
- 父进程与子进程的执行顺序与系统调度有关。
- 在子进程内,这个方法会返回0;在父进程内,这个方法会返回子进程的编号PID。
- os.fork的返回值:返回值为大于0时,此进程为父进程,且返回的数字为子进程的PID;当返回值为0时,此进程为子进程。如果返回值为负数则表明创建子进程失败。
- 父进程结束时,子进程并不会随父进程立刻结束。同样,父进程不会等待子进程执行完。
os.getpid():获取进程的进程号。
os.getppid():获取父进程的进程号。
Multiprocessing
由于windows没有fork调用,python提供了multiprocessing支持跨平台版本。
创建管理进程模块:
• Process(用于创建进程模块)
• Pool(用于创建管理进程池)
• Queue(用于进程通信,资源共享)
• Value,Array(用于进程通信,资源共享)
• Pipe(用于管道通信)
• Manager(用于资源共享)
Process 类
构造方法:Process([group [, target [, name [, args [, kwargs]]]]])。
• group: 线程组,目前还没有实现,库引用中提示必须是None;
• target: 要执行的方法;
• name: 进程名;
• args/kwargs: 要传入方法的参数。
实例方法:
• p.start():启动进程,并调用该子进程中的p.run()。
• p.run(): strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
• p.terminate(): 不管任务是否完成,立即停止工作进程。
• p.is_alive(): 如果p仍然运行,返回True。
• p.join([timeout]): 阻塞当前上下文环境的进程,直到调用此方法的进程终止或到达指定的timeout。
使用multiprocessing.Array共享数据
• 创建Array时,需要指定数据类型。
• 如:arr = Array('i', [11, 22, 33, 44])。
•'i'表示数据类型:“d”表示一个双精度的浮点数,“i”表示一个有符号的整数。
• 这些共享对象将被线程安全的处理,类型对应表:
"c": ctypes.c_char "u": ctypes.c_wchar "b": ctypes.c_byte "B": ctypes.c_ubyte
"h": ctypes.c_short "H": ctypes.c_ushort "i": ctypes.c_int "I": ctypes.c_uint
"l": ctypes.c_long "L": ctypes.c_ulong "f": ctypes.c_float "d": ctypes.c_double
使用multiprocessing.Manager共享数据
• 由Manager()返回的manager提供 list, dict, Namespace, Lock, RLock, Semaphore。
• BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array类型的支持。
• Manager比Array要好用一点,因为它可以同时保存多种类型的数据格式。
使用multiprocessing.Queue共享数据
• 消息队列:multiprocessing.Queue
Queue是对进程安全的队列,可以使用Queue实现对进程之间的数据传输;还有一个重要作用是作为缓存使用。
Queue(maxzize = 0) 创建一个队列对象,maxsize 表示队列中最多存放消息的数量。
实例方法:
• put(obj [, block=True[, timeout]]):调用队列对象的put()方法将obj插入到队列中。
• get([block=True[, timeout]]):get方法可以将队列中读取并删除一个元素。
• full():判断队列是否为满。
• empty():判断队列是否为空。
• qsize():获取队列中消息数量。
Queue不能再Pool进程池中使用,使用Multiprocessing.Manager类可以适用Pool类。
使用multiprocessing.Queue共享数据
• 不一致读。
• 为了防止和多线程一样的出现数据抢夺和脏数据的问题,同样需要设置进程锁。与threading类
似,在multiprocessing里也有同名的锁类RLock, Lock, Event, Condition, Semaphore,用法一样。
• 当创建进程时(非使用时),共享数据会被拿到子进程中,当进程中执行完毕后,再赋值给原值。
多进程资源消耗
一般我们是通过动态创建子进程(或子线程)来实现并发服务器的,但是会存在这样一些缺点:
1. 动态创建进程(或线程)比较耗费时间,这将导致较慢的服务器响应。
2. 动态创建的子进程通常只用来为一个客户服务,这样导致了系统上产生大量的细微进程(或
线程)。进程和线程间的切换将消耗大量CPU时间。
3. 动态创建的子进程是当前进程的完整映像,当前进程必须谨慎的管理其分配的文件描述符和
堆内存等系统资源,否则子进程可能复制这些资源,从而使系统的可用资源急剧下降,进而
影响服务器的性能。
Pool 进程池
进程池的作用:有效的降低频繁创建销毁线程所带来的额外开销。
进程池的原理:
• 进程池都是采用预创建的技术,在应用启动之初便预先创建一定数目的进程。
• 应用在运行的过程中,需要时可以从这些进程所组成的进程池里申请分配一个空闲的进程,来执
行一定的任务,任务完成后,并不是将进程销毁,而是将它返还给进程池,由线程池自行管理。
• 如果进程池中预先分配的线程已经全部分配完毕,但此时又有新的任务请求,则进程池会动态的
创建新的进程去适应这个请求。
• 某些时段应用并不需要执行很多的任务,导致了进程池中的线程大多处于空闲的状态,为了节省
系统资源,进程池就需要动态的销毁其中的一部分空闲进程。
• 进程需要一个管理者,按照一定的要求去动态的维护其中进程的数目。
Pool 主进程管理进程的机制:
• 最简单、最常用的算法是随机算法和Round Robin(轮流算法)。
• 主进程和所有子进程通过一个共享的工作队列来实现同步:子进程都睡眠在该工作队列上,当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程将获得新任务的“接管权”,它可以从工作队列中取出任务并执行之,而其他子进程将继续睡眠在工作队列上。
• 当选择好子进程后,主线程程还需要使用某种通知机制来告诉目标子进程有新任务需要处理,并传递必要的数据。我们可以把这些数据定义为全局,那么它们本身就是被所有进程共享的。对于进程池而言,最简单的方式是,在父进程和子进程之间预先建立好一条管道,然后通过管道来实现所有的进程间通信。
Pool 进程池的应用场景
• 需要大量的进程来完成任务,且完成任务的时间比较短。
• 但对于长时间的任务,比如一个Telnet连接请求,进程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
Pool 类
构造方法
• Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])。
• processes :使用的工作进程的数量,如果processes是None那么使用 os.cpu_count()返回的数量。
• initializer: 如果initializer是None,那么每一个工作进程在开始的时候会调用initializer(*initargs)。
• maxtasksperchild:工作进程退出之前可以完成的任务数,完成后用一个新的工作进程来替代原进程,来让闲置的资源被释放。maxtasksperchild默认是None,意味着只要Pool存在工作进程就会一直存活。
实例方法
• apply_async(func[, args[, kwds[, callback]]]) 它是非阻塞。
• apply(func[, args[, kwds]])是阻塞的。
• close() 关闭pool,使其不在接受新的任务。
• terminate() 关闭pool,结束工作进程,不在处理未完成的任务。
• join() 主进程阻塞,等待子进程的退出, join方法要在close或terminate之后使用。这样是因为被终止的进程需要被父进程调用wait(join等价与wait),否则进程会成为僵尸进程。
注意:
- 使用Pool创建进程池对象,同时进程池中进程已经启动。
- 向进程池对象中添加事件,事件排队执行。
- 如果主进程退出,则进程池中所有进程都退出。
协程
概念
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
协程,又称微线程,纤程,英文名Coroutine。
协程的作用:
1、在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。
2、但这一过程并不是函数调用(没有调用语句)。
协程的缺点:
无法利用多核资源。
协程与线程比较
• 线程有自己的上下文,切换受系统控制;而协程有自己的上下文,但是其切换由用户控制,由当前协程切换到其他协程由当前协程来控制。
• 无需原子操作锁定及同步的开销,所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何切换到另一个线程的动作。
• 协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。
• 与多线程比协程有极高的执行效率,不需要多线程的锁机制。
• 协程以后主要用在网络爬虫和网络请求,开辟一个协程大概需要5k空间,开辟一个线程需要512k空间, 开辟一个进程占用资源最多。
• 强调非阻塞异步并发的一般都是使用协程。
python对协程的支持
python2.x协程应用:
• yield
• gevent
python3.x协程应用:
• asynico + yield from(python3.4)
• asynico + await(python3.5)
• gevent
• Python3.4以后引入了asyncio模块,可以很好的支持协程。
asyncio协程
• asyncio是一个使用async / await语法编写并发代码的库。
• asyncio用作多个Python异步框架的基础,这些框架提供高性能的网络和Web服务器,数据库连接库,分布式任务队列等。
• asyncio通常非常适合IO绑定和高级结构化网络代码。
asyncio的几个概念
• event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数(协程)注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。
• coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
• future 对象: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别。
• task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。Task 对象是 Future 的子类,它将 coroutine 和 Future 联系在一起,将coroutine 封装成一个 Future 对象。
• async/await 关键字:python3.5 用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。其作用在一定程度上类似于yield。
asyncio提供了一组高级 API:
• 同时运行Python协同程序并完全控制它们的执行;
• 执行网络IO和IPC ;
• 控制子过程 ;
• 通过队列分配任务;
• 同步并发代码;
此外,还有一些用于库和框架开发人员的低级 API :
• 创建和管理事件循环,提供异步的 hronous API networking,运行subprocesses,处理等;OSsignals。
• 使用传输实现有效的协议 ;
• 使用 async / await语法桥接基于回调的库和代码。
官方文档:asyncio — Asynchronous I/O — Python 3.12.7 documentation