一、进程、线程、协程的相关概念
1、进程、线程、协程定义
(1)进程是系统进行资源分配和调度的独立单位
(2)线程是进程的实体,是CPU调度和分派的基本单位
(3)协程也是线程,称微线程,自带CPU上下文,是比线程更小的执行单元
2、进程和线程的区别
一个程序至少有一个进程,一个进程至少有一个线程。线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率;线程不能够独立执行,必须依存在进程中。
3、进程和线程的优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
4、协程相关
将把一个进程比作生活中的饭店,则保证饭店正常运行的服务员就是线程,服务好每桌客人就是线程要完成的任务。
当用多线程完成任务时,采取以下模式:每来一桌的客人,该桌子就安排一个服务员,即有多少桌客人就得对应多少个服务员。
而当我们用协程来完成任务时,模式如下:就安排一个服务员,来吃饭得有一个点餐和等菜的过程,当A在点菜,就去B服务,B叫了菜在等待,我就去C,当C也在等菜并且A点菜点完了,赶紧到A来服务… …依次类推
从上面的例子可以看出,想要使用协程,那么我们的任务必须有等待。当我们要完成的任务有耗时任务,属于IO密集型任务时,我们使用协程来执行任务会节省很多的资源(一个服务员和多个服务员的区别), 并且可以极大的利用到系统的资源。
二、多进程例子
1、Unix/Linux操作系统->fork()系统调用
fork()非常特殊。普通的函数调用,调用一次,返回一次。但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程
import os
print('Process (%s) start...' % os.getpid())
pid = os.fork()
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
输出结果为:
Process (163320) start...
I (163320) just created a child process (163337).
I am child process (163337) and my parent is 163320.
2、multiprocessing模块
由于fork()只支持Unix/Linux系统,无法在Windows系统下运行。所以fork()不支持跨平台操作。由于python是跨平台的,提供了multiprocessing模块,该模块可以支持跨平台的多进程操作。multiprocessing模块提供了一个Process类来代表一个进程对象。
import os
from multiprocessing import Process
# 子进程要执行的代码
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start() # 启动子进程
p.join() #等待子进程结束
print('Child process end.')
输出为:
Parent process 163928.
Child process will start.
Run child process test (163945)...
Child process end.
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
3、Pool模块
如果需要启动大量的子进程,可以用进程池的方式批量创建子进程,multiprocessing中的Pool提供了创建进程池的方法。
from multiprocessing import Pool
import os, time, random
def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task, args=(i,))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')
输出为:
Parent process 165248.
Waiting for all subprocesses done...
Run task 0 (165265)...
Run task 1 (165266)...
Run task 2 (165267)...
Run task 3 (165268)...
Task 3 runs 0.01 seconds.
Run task 4 (165268)...
Task 2 runs 0.50 seconds.
Task 0 runs 1.19 seconds.
Task 4 runs 1.20 seconds.
Task 1 runs 2.50 seconds.
All subprocesses done.
对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。由于Pool的默认大小是CPU的核数,至少提交大于核数的子进程才能看到上面的等待效果。
4、子进程
子进程往往并不是自身,而是一个外部进程。在创建子进程后,还需要控制子进程的输入和输出。subprocess模块可以非常方便地启动一个子进程,然后控制起输入和输出。
import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
输出结果为:
$ nslookup www.python.org
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
www.python.org canonical name = dualstack.python.map.fastly.net.
Name: dualstack.python.map.fastly.net
Address: 146.75.112.223
Name: dualstack.python.map.fastly.net
Address: 2a04:4e42:1a::223
exit code: 0
如果子进程还需要输入,则可以通过communicate()方法输入。
5、进程间通信
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。以Queue为例,见下方代码
from multiprocessing import Process, Queue
import os, time, random
def write(q):
print('Process to write: %s' % os.getpid())
for value in ['a','b','c']:
print('put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
def read(q):
print('Process to read: %s' % os.getpid())
while True:
value = q.get(True)
print('get %s from Queue.' % value)
if __name__ == "__main__" :
q = Queue()
pw = Process(target=write,args=(q,))
pr = Process(target=read,args=(q,))
pw.start()
pr.start()
pw.join()
pr.join()
pr.terminate()
三、多线程例子
1、threading模块
当我们面临多任务的时候,可以使用多个进程完成,但同时也可以在一个进程内由多个线程完成。线程是操作系统直接支持的执行单元。Python中的线程是真正的Posix Thread,而不是模拟出来的线程
Python的标准库中提供了两个模块:_thread和threading,_thread是低级模块,threading是对_thread封装的高级模块。因此我们在使用时只需用threading模块。
启动一个线程和启动一个进程的过程基本一样,均是将函数传入进程(Process)模块或者线程(Thread)模块,并通过传入函数的方式对线程或进程进行实例化,最后调用start()开始执行,以下方代码为例
import time, threading
def loop():
print('thread %s is running...'% threading.current_thread().name)
n = 0
while n < 5:
n = n + 1
print('thread %s >>> %s' % (threading.current_thread().name,n))
time.sleep(1)
print('thread %s ended.' % threading.current_thread().name)
if __name__ == "__main__" :
print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop,name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
输出结果为:
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
从上方代码可以看出,任何进程都会默认启动一个线程,将该线程成为主线程,主线程中又可启动新的线程。threading.current_thread().name永远返回当前线程的实例。
2、Lock
多线程和多进程最大的区别就是多线程共享一个进程的内存,所有变量由所有线程共享,任何一个变量都有可能被任何一个线程修改,因此在多进程编程中,往往要注意多个线程对同一个变量修改的问题(原因为线程之间是交替运行,一个线程运行过程中可能中断,但在另一个线程中却在修改)。而多进程中,即使执行同一个代码,同一个变量也会拷贝在各自进程中的内存,互不影响。
为了解决线程之间可能会修改同一变量的问题,确保多线程程序可以正常运行,应该给线程中运行的函数加锁(Lock),即在运行加锁的线程时,其他的进程不可执行进程中的函数,只有当锁被释放之后,获得该锁的线程才能修改。无论有多少线程,锁只有一个,且同一时刻只有一个线程持有该锁,所以不会造成修改的冲突。python中创建锁是通过threading.Lock()实现。以下为代码示例:
import threading
balance = 0
lock = threading.Lock()
def change_it(n):
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(2000000):
lock.acquire()
try:
change_it(n)
finally:
lock.release()
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
lock.acquire()为线程获得锁,当线程用完之后一定要释放锁,通过lock.release()释放,否则线程将是死线程。可以用try…finally来确保锁一定会被释放。在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
3、