进程就是操作系统中执行的一个程序,操作系统以进程为单位分配存储空间,每个进程都有自己的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据,操作系统管理所有进程的执行,为它们合理的分配资源。进程可以通过fork或spawn的方式来创建新的进程来执行其他的任务,不过新的进程也有自己独立的内存空间,因此必须通过进程间通信机制(IPC,Inter-Process Communication)来实现数据共享,具体的方式包括管道、信号、套接字、共享内存区等。
一个进程可以拥有多个并发的执行线索,简单的说就是拥有多个可以获得CPU调度的执行单元,这就是所谓的线程。
由于线程在同一个进程下,它们可共享相同的上下文,因此相对于进程而言,线程间的信息共享和通信更加容易。当然在单核CPU系统中,真正的并发是不可能的,因为在某个时刻能够获得CPU的只有唯一的一个线程,多个线程共享了CPU的执行时间。
Python中的多进程
Unix和Linux操作系统上提供了fork()
系统调用来创建进程,调用fork()
函数的是父进程,创建出的是子进程,子进程是父进程的一个拷贝,但是子进程拥有自己的PID。fork()
函数非常特殊它会返回两次,父进程中可以通过fork()
函数的返回值得到子进程的PID,而子进程中的返回值永远都是0。
Python的os模块提供fork()
函数。由于Windows系统没有fork()
调用,因此要实现跨平台的多进程编程,可以使用multiprocessing模块的Process
类来创建子进程,而且该模块还提供了更高级的封装,例如批量启动进程的进程池(Pool
)、用于进程间通信的队列(Queue
)和管道(Pipe
)等。
from multiprocessing import Process from os import getpid from random import randint from time import time, sleep def download_task(filename): print('启动下载进程,进程号[%d].' % getpid()) print('开始下载%s...' % filename) time_to_download = randint(5, 10) sleep(time_to_download) print('%s下载完成! 耗费了%d秒' % (filename, time_to_download)) def main(): start = time() p1 = Process(target=download_task, args=('Python从入门到住院.pdf', )) p1.start() p2 = Process(target=download_task, args=('Peking Hot.avi', )) p2.start() p1.join() # 等待进程执行结束 p2.join() end = time() print('总共耗费了%.2f秒.' % (end - start)) if __name__ == '__main__': main()
# 用subprocess模块中的类和函数来创建和启动子进程,然后通过管道来和子进程通信
全局变量counter不起作用!!--> 原因:进程各自持有一份数据,默认无法共享数据 --> 用multiprocessing模块中的Queue
类,它是可以被多个进程共享的队列,底层是通过管道和信号量(semaphore)机制来实现的
Python中的多线程
Python解释器通过GIL(全局解释器锁)来防止多个线程同时执行本地字节码,这个锁对于CPython(Python解释器的官方实现)是必须的,因为CPython的内存管理并不是线程安全的。因为GIL的存在,Python的多线程并不能利用CPU的多核特性。
from random import randint from threading import Thread from time import time, sleep def download(filename): print('开始下载%s...' % filename) time_to_download = randint(5, 10) sleep(time_to_download) print('%s下载完成! 耗费了%d秒' % (filename, time_to_download)) def main(): start = time() t1 = Thread(target=download, args=('Python从入门到住院.pdf',)) t1.start() t2 = Thread(target=download, args=('Peking Hot.avi',)) t2.start() t1.join() # 逐个执行每个线程,执行完毕后main()继续往下执行. t2.join() end = time() print('总共耗费了%.3f秒' % (end - start)) if __name__ == '__main__': main()
继承Thread
类的方式来创建自定义的线程类,然后再创建线程对象并启动线程。
from random import randint from threading import Thread from time import time, sleep class DownloadTask(Thread): def __init__(self, filename): super().__init__() self._filename = filename
# 线程被cpu调度后自动执行线程对象的run方法 def run(self): print('开始下载%s...' % self._filename) time_to_download = randint(5, 10) sleep(time_to_download) print('%s下载完成! 耗费了%d秒' % (self._filename, time_to_download)) def main(): start = time() t1 = DownloadTask('Python从入门到住院.pdf') t1.start() t2 = DownloadTask('Peking Hot.avi') t2.start() t1.join() t2.join() end = time() print('总共耗费了%.2f秒.' % (end - start)) if __name__ == '__main__': main()
- setName / getName / start()
-
join() 逐个执行每个线程,执行完毕后主线程继续往下执行.
- setDaemon() 设置为后台线程或前台线程(默认)
- 如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止;
- 如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止;
多个线程共享进程的全局变量, 启用锁机制:
from time import sleep from threading import Thread, Lock class Account(object): def __init__(self): self._balance = 0 self._lock = Lock() def deposit(self, money): # 先获取锁才能执行后续的代码 self._lock.acquire() try: new_balance = self._balance + money sleep(0.01) self._balance = new_balance finally: # 在finally中执行释放锁的操作保证正常异常锁都能释放 self._lock.release() @property def balance(self): return self._balance class AddMoneyThread(Thread): def __init__(self, account, money): super().__init__() self._account = account self._money = money def run(self): self._account.deposit(self._money) def main(): account = Account() threads = [] for _ in range(100): t = AddMoneyThread(account, 1) threads.append(t) t.start()
# 等待子线程执行结束 for t in threads: t.join() print('账户余额为: ¥%d元' % account.balance) if __name__ == '__main__': main()
Python在threading模块中定义了几种线程锁类,分别是: https://www.liujiangblog.com/course/python/79
Lock 互斥锁 | lk = threading.Lock() |
acquire() / release() |
RLock 可重入锁 | 对象内部维护着一个Lock和一个counter对象 | |
Semaphore 信号 |
se= threading.BoundedSemaphore(5) |
允许一定数量的线程同时更改数据 acquire() / release() |
Event 事件 |
event = threading.Event() |
全局定义一个Flag,如果Flag=False,当程序执行wait()方法时就会阻塞,如果Flag=True,线程不再阻塞。
clear()方法会将事件的Flag设置为False。 set()方法会将Flag设置为True。 is_set()判断当前是否"绿灯放行"状态。 wait()方法将等待“红绿灯”信号。 |
Condition 条件 |
con = threading.Condition() |
acquire() / release() notify() 从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁(进入锁定池),其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常 |
Barrier “阻碍” |
acquire() / release() |
单线程+异步I/O
异步编程是通过调度程序从任务队列中挑选任务,调度程序以交叉形式执行这些任务,由于执行时间和顺序的不确定,因此需要通过钩子函数(回调函数)或Future
对象来获取任务执行的结果。Python 3通过asyncio
模块以及await
和async
关键字提供了对异步I/O的支持。
利用操作系统提供的异步I/O支持,就可用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型。
Nginx就是支持异步I/O的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。用Node.js开发的服务器端程序也使用了这种工作模式,这也是当下实现多任务编程的一种趋势。
在Python语言中,单线程+异步I/O的编程模型称为协程,可基于事件驱动编写高效的多任务程序。
- 极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销。
- 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好,所以执行效率比多线程高很多。
如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。