python学习笔记(九)——线程与进程
一、线程
Python 中为我们提供了两个模块来创建线程。
- _thread
- threading
thread 模块已被废弃。用户可以使用 threading 模块代替。所以,在 Python 中不能再使用"thread" 模块。为了兼容性,Python 将 thread 重命名为 “_thread”。
相对 _thread 模块来说, threading 模块更加高级也更加常用。
多线程
创建多线程步骤:
- 导入线程库
- 创建任务函数
- 创建线程对象
- 启动线程
实例:
import threading,time
def task():
for i in range(1,11):
time.sleep(1)
print(threading.current_thread().name,end="")
print(":output >%d"%i)
print(time.ctime())
# 创建两个线程
pt1=threading.Thread( target=task )
pt2=threading.Thread( target=task )
# 输出线程名
print(threading.current_thread().name)
# 启动线程
pt1.start()
pt2.start()
pt1.join()
pt2.join()
print(time.ctime())
输出:
Sat Mar 14 16:07:12 2020
MainThread
Thread-2:output >1
Thread-1:output >1
Thread-1Thread-2:output >2
:output >2
Thread-1Thread-2:output >3
:output >3
Thread-1Thread-2:output >4
:output >4
Thread-1:output >5
Thread-2:output >5
Thread-1Thread-2:output >6
:output >6
Thread-2:output >7
Thread-1:output >7
Thread-2Thread-1:output >8
:output >8
Thread-2:output >9
Thread-1:output >9
Thread-1:output >10
Thread-2:output >10
Sat Mar 14 16:07:22 2020
可以看到一共用了10秒时间,两个线程都完成了自己的任务。
使用 threading.Thread( )
创建线程时,可以传入六个参数,比如线程名 name,任务函数参数 args等。
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):
其中,如name、daemon等参数除了在创建线程时指定以外,还可以通过对象调用函数设置。 pt.setName()、pt.setDaemon()。
线程同步
在多线程中会常会发生多个线程同时访问同一个资源的情况,造成线程不安全。而我们要求线程对临界资源的操作是必须是原子操作,因此我们可以通过线程锁来实现线程安全。
使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法进行加锁和解锁。
lock=threading.RLock() # 创建一个锁🔒
lock.acquire() # 加锁
lock.release() # 解锁
把每次只允许一个线程操作的数据,放到 acquire 和 release 方法之间进行操作。
全局解释器锁GIL
在非python环境中,单核情况下,同时只能有一一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。
GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一 一个。拿不到通行证的线程,就不允许进入CPU执行。GIL 只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一-时间只能有- -个线程拿到数据。而在pypy和ipython中是没有GIL的。
所以,在Python中,可以使用多线程,但不要指望能有效利用多核。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响
二、进程
使用多进程同样可以完成多线程的工作,我们将之前的多线程程序改成多进程程序。
import multiprocessing
import time
def task():
for i in range(1,11):
time.sleep(1)
print(multiprocessing.current_process().name,__name__,end="")
print(":output >%d"%i)
if __name__=="__main__":
print(time.ctime())
p1=multiprocessing.Process(target=task)
p2=multiprocessing.Process(target=task)
p1.start()
p2.start()
p1.join()
p2.join()
print(time.ctime())
输出:
Sat Mar 14 17:25:31 2020
Process-1 __mp_main__:output >1
Process-2 __mp_main__:output >1
Process-1 __mp_main__:output >2
Process-2 __mp_main__:output >2
Process-1 __mp_main__:output >3
Process-2 __mp_main__:output >3
Process-1 __mp_main__:output >4
Process-2 __mp_main__:output >4
Process-1 __mp_main__:output >5
Process-2 __mp_main__:output >5
Process-1 __mp_main__:output >6
Process-2 __mp_main__:output >6
Process-1 __mp_main__:output >7
Process-2 __mp_main__:output >7
Process-1 __mp_main__:output >8
Process-2 __mp_main__:output >8
Process-1 __mp_main__:output >9
Process-2 __mp_main__:output >9
Process-1 __mp_main__:output >10
Process-2 __mp_main__:output >10
Sat Mar 14 17:25:41 2020
进程池
通过进程池 pool 可以批量的创建大量的进程。
import multiprocessing
import os
import time
def task(i):
print("线程%d 执行: pid=%s"%(i,os.getpid()))
time.sleep(1)
return i
def back(n):
print("任务%d已完成"%n)
if __name__=="__main__":
mypool=multiprocessing.Pool(5) # 线程池大小
for i in range(1,21): # 20个线程任务
mypool.apply_async(func=task,args=(i,))
mypool.close()
mypool.join()
apply_async()
会等待线程池中有空闲时依次执行线程任务。其中,callback 参数是回调函数。
进程池中回调函数callback作用是:进程池中任何一个任务一旦处理完了,就立即告知主进程,主进程则调用一个函数去处理该结果,它可以用来接收进程任务的返回值,判断其执行的结果。
输出结果:
线程1 执行: pid=12372
线程2 执行: pid=11236
线程3 执行: pid=15144
线程4 执行: pid=8332
线程5 执行: pid=16732
线程6 执行: pid=12372
任务1已完成
线程7 执行: pid=11236
任务2已完成
线程8 执行: pid=15144
任务3已完成
线程9 执行: pid=8332
任务4已完成
线程10 执行: pid=16732
任务5已完成
线程11 执行: pid=12372
任务6已完成
线程12 执行: pid=11236
任务7已完成
线程13 执行: pid=15144
任务8已完成
线程14 执行: pid=8332
任务9已完成
线程15 执行: pid=16732
任务10已完成
线程16 执行: pid=12372
任务11已完成
线程17 执行: pid=11236
任务12已完成
线程18 执行: pid=15144
任务13已完成
线程19 执行: pid=8332
任务14已完成
线程20 执行: pid=16732
任务15已完成
任务16已完成
任务17已完成
任务18已完成
任务19已完成
任务20已完成
需要注意的是,在使用线程池时应注意将pool.close()
先于 pool.join()
调用。因为 join() 会让主进程阻塞等待子进程全部执行完之后再执行,close() 的作用是关闭pool,使其不在接受新的(主进程)任务。所以,两个顺序可不能颠倒了。