Python多线程和多进程
线程是一个操作系统级别的概念,编程语言本身不创建线程,而是调用操作系统层提供的接口创建、控制、销毁线程实例。
他们所支持的线程底层实现和操作效果也是不尽相同的。不过一个操作系统支持的线程至少会有四种状态:就绪、执行、阻塞和终结。线程在四种状态下进行切换,都是要消耗不少的CPU计算能力的。
并且根据操作系统使用线程的进程的不一样,线程还分为用户线程和操作系统线程。操作系统线程(内核线程),是指操作系统内核为了完成硬件接口层操作,由操作系统内核创建的线程:例如I/O操作的内核线程,这些线程应用程序是不能干预的;用户线程,是指用户安装/管理的应用程序,为执行某一种操作,而由这个应用程序创建的线程。
线程在创建时,操作系统不会为这个线程分配独立的资源(除了必要的数据支撑)。一个应用程序(进程)下的所有线程,都是共享这个应用程序(进程)中的资源,例如这个应用程序的CPU资源、I/O资源、内存资源。
多线程是一门编程语言的重要操作。
GIL(全局解释器锁)存在于python解释器中,用来确保当前只有一个线程被执行,当一个线程获得GIL后,这个线程将被执行,在python2中,执行了一定数量的字节码,或者遇到IO操作会退出释放GIL,在python3.2以后,使用了新的GIL,新的GIL实现中用一个固定的超时时间来指示当前的线程放弃全局锁。在当前线程保持这个锁,且其他线程请求这个锁时,当前线程就会在5毫秒后被强制释放该锁。由下一个获得GIL的线程执行,这导致了纯Python代码使用多线程并不能提高运行速率,只是在遇到需要等待操作的时候,使用多线程会提升效率。
python多线程有两个模块,threading和thread,一般情况下使用threading,特殊情况才使用thread,下面是原因:
- threading模块对同步原语的支持更为完善和丰富。
- threading模块在主线程和子线程交互上更为友好。
- thread模块不支持守护线程。
thread模块中主线程退出的时候。所有的子线程不论是否还在工作,都会被强制结束。 - python3中已经不存在thread模块。
Python多线程支持两种方式来创建线程:一种是继承Thread类,重写他的run()方法,另一种是创建一个threading.Thread对象,在它的初始化函数中(init())中可将调用对象作为参数传输。
常用Threading模块的方法:
- threading.Thread(self, group=None, target=None, name=None,
args=(), kwargs=None, verbose=None))
target: 指定线程由 run () 方法调用的可调用对象。默认为 None, 意味着不调用任何内容。
name: 指定该线程的名称。 在默认情况下,创建一个唯一的名称。
args: target调用的实参,元组格式。默认为 (),即不传参。
daemon: 为False表示父线程在运行结束时需要等待子线程结束才能结束程序,为True则表示父线程在运行结束时,子线程无论是否还有任务未完成都会跟随父进程退出,结束程序。(python2和3可能有区别)
- start方法和run方法
start() 方法是启动一个子线程,线程名就是我们定义的name
run() 方法并不启动一个新线程,就是在主线程中调用了一个普通函数而已。
想启动多线程,就必须使用start()方法。
- threading.Timer(interval, function, args=[], kwargs={})
threading的派生类,可以用来定时任务,如果要重复执行某个任务,可以在function中再定义一个threading.time()
- threading.current_thread().name (或者threading.current_thread().getName())
线程名,只是一个标识符,可以使用getName()、setName()获取和运行时重命名。
- threading.current_thread().ident
线程ID,非0整数。线程启动后才会有ID,否则为None。线程退出,此ID依旧可以访问。此ID可以重复使用
- threading.current_thread().is_alive()
返回线程是否存活,布尔值,True或False。
- threading.active_count()
返回当前活跃的Thread对象数量。返回值和通过enumerate()返回的列表长度是相等的。
- join([timeout])方法
threading.Thread.join。主线程A中,创建了子线程B,并且在主线程A中调用了B.join(),那么,主线程A会在调用的地方等待,直到子线程B完成操作后,才可以接着往下执行,那么在调用这个线程时可以使用被调用线程的join方法。
里面的参数时可选的,代表线程运行的最大时间,即如果超过这个时间,不管这个此线程有没有执行完毕都会被回收,然后主线程或函数都会接着执行的。
join()的作用是,在子线程完成运行之前,这个子线程的父线程将一直被阻塞,设置为None则等待子进程执行完毕
- setDaemon()方法
主线程A中,创建了子线程B,并且在主线程A中调用了B.setDaemon(),这个的意思是,把主线程A设置为守护线程,这时候,要是主线程A执行结束了,就不管子线程B是否完成,一并和主线程A退出.这就是setDaemon方法的含义,这基本和join是相反的。此外,还有个要特别注意的:必须在start() 方法调用之前设置,如果不设置为守护线程,程序会被无限挂起。
在CPU密集型任务下,多进程更快,或者说效果更好;而IO密集型,多线程能有效提高效率。
python的两个多进程库
subprocess和multiprocess
区别:
subprocess:
subprocess 用来执行其他的可执行程序的,即执行外部命令。 他是os.fork() 和 os.execve() 的封装。 他启动的进程不会把父进程的模块加载一遍。使用subprocess的通信机制比较少,通过管道或者信号机制.
由于在windows中没有os.fork,所以创建进程采用的是createprocess()
Linux的进程创建是通过系统调用fork()
multiprocessing:
multiprocessing 用来执行python的函数,他启动的进程会重新加载父进程的代码。可以通过Queue、Array、Value等对象来通信。
subprocess 用来执行外部命令,是os.fork() 和 os.execve() 的封装,即先fork一个子进程,再运行新的外部程序,子进程不会把父进程的模块加载一遍;
而multiprocessing的原理是fork,fork()调用:调用1次,返回两次--操作系统自动把当前进程(父进程)复制了一份(子进程),然后,分别在父进程和子进程内返回,父进程返回子进程的pid,子进程返回0,即父进程和子进程都在运行。
对于外部调用来说,使用multiprocessing太占资源。
进程之间是数据隔离的,想通讯要用特殊的方式。
在windows中必须把Process()放到if name == "main"语句下
由于Windows没有fork,多处理模块启动一个新的Python进程并导入调用模块。
如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。
这是隐藏对Process()内部调用的原,使用if name == “__main __”,这个if语句中的语句将不会在导入时被调用。
由于Python运行过程中,multiprocess新创建进程后,进程会导入正在运行的文件,即在运行代码0.1的时候,代码在运行到mp.Process时,新的进程会重新读入改代码,对于没有if name=="main"保护的代码,新进程都认为是要再次运行的代码,这是子进程又一次运行mp.Process,但是在multiprocessing.Process的源码中是对子进程再次产生子进程是做了限制的,是不允许的,于是出现如上的错误提示。
进程之间的通讯可以通过Value和Array进行,Value是单个变量,Array是数组
from multiprocess import Value,Array
num = Value('d', 0.0)
arr = Array('i', range(10))
创建num和arr时,“d”和“i”参数由Array模块使用的typecodes创建:“d”表示一个双精度的浮点数,“i”表示一个有符号的整数,这些共享对象将被线程安全的处理。小写是有符号,大写是无符号。
print(num.value)
print(arr[0])
‘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