一、什么是线程&进程
1、进程 (Process)
是资源的集合。其实就是程序(qq进程)。对于操作系统来说一个任务就是一个进程,例如打开浏览器就启动了一个浏览器进程,打开word就启动了一个word进程。
多进程多用于处理CPU密集型任务,例如排序、计算都是消耗cpu的
2、线程 (Thread)
是程序里面最小的执行单元。比如打开word,可以同时打字、拼写检查等,这些进程中的“子任务”就是线程。
多线程多用于处理IO密集型任务频繁写入读出,cpu负责调度,消耗的是磁盘空间
3、线程是包含在一个进程里面的,一个进程可以有多个线程
4、一个进程里面默认有一个线程
5、主线程与子线程,一个程序默认有一个主线程,由主线程来启动子线程
二、多线程
1、一个简单的多线程,threading.Thread(target=方法)
import threading,time def run(): time.sleep(3) print('hello') # for i in range(5): #串行,运行需要15s # run() for i in range(5):#多线程,并行运行,3s t = threading.Thread(target=run)#实例化了一个线程 t.start()
下面举一个下载网页的例子,列举多线程的函数如何传参,需要用args
import threading,time,requests urls = { '58':'http://www.58.com/', 'haozu':'https://www.haozu.com/bj', }def down_html(file_name,url): req = requests.get(url).content #content返回二进制结果 open(file_name+'.html','wb').write(req) #需要加上.html # 1、串行 # start_time = time.time() # for k,v in urls.items(): # down_html(k,v) # end_time = time.time() # run_time = end_time-start_time # print('串行下载总共花了%s秒'%run_time) #2.31秒 #2、并行 start_time = time.time() for k,v in urls.items(): t = threading.Thread(target=down_html,args=(k,v))#多线程的函数如果传参的话,必须得用args t.start() end_time = time.time() run_time = end_time-start_time print('并行下载总共花了%s秒'%run_time) #0.003秒
从这个下载网页的例子中看到,并行下载的时间远远短于串行,但事实真的是这样么?
我们看到并行运行时是先打印出时间,但是程序未运行结束。实际上打印的时间是主线程结束时间,主线程结束后子线程还未结束,所以程序未结束运行。所以0.003s这个时间是主线程运行的时间,而不是并行下载的时间。如果想看到并行下载的时间,就需要引入线程等待。
2、线程等待,t.join()
import threading,time,requests urls = {'58':'http://www.58.com/','haozu':'https://www.haozu.com/bj',} def down_html(file_name,url): req = requests.get(url).content #content返回二进制结果 open(file_name+'.html','wb').write(req) #需要加上.html start_time = time.time() threads = [] for k,v in urls.items(): #2个线程 t = threading.Thread(target=down_html,args=(k,v)) t.start() threads.append(t) #实际有3个线程,进程里面默认有一个线程,这个线程叫做主线程 for t in threads:#主线程循环等待2个子线程执行结束 t.join()#循环等待 end_time = time.time() run_time = end_time-start_time print('串行下载总共花了%s秒'%run_time) #0.56秒
有了线程等待,主线程就会等到子线程全部执行结束再结束,这样统计出的才是真正的并行下载时间。
这里又有了一个新的问题,如果我们想看到每个线程运行的时间怎么办呢,需要在down_html函数中打印。但若需要获得down_html函数返回值时,需要特殊处理,因为多线程调用函数时,函数的返回值是获取不到的。
3、多线程调用函数时,如何获取函数返回值
只能在函数外定义字典或者list来存储返回值
import threading,time,requests urls = {'58':'http://www.58.com/','haozu':'https://www.haozu.com/bj',} data = {}#多线程调用函数时,函数返回值获取不到,只能在函数外定义字典或者list来存储返回值 def down_html(file_name,url): start_time = time.time() req = requests.get(url).content open(file_name+'.html','wb').write(req) end_time = time.time() run_time = end_time - start_time print(run_time, url) # 打印每个进程的耗时 data[url] = run_time # 定义字典存储返回值 start_time = time.time() threads = [] for k,v in urls.items(): t = threading.Thread(target=down_html,args=(k,v)) t.start() threads.append(t) for t in threads:#主线程循环等待2个子线程执行结束 t.join() end_time = time.time() run_time = end_time-start_time print('串行下载总共花了%s秒'%run_time) #0.56秒
4、为什么python的多线程不能利用多核CPU,但是在写代码的时候,多线程的确在并发,而且还比单线程快
电脑cpu有几核,那么只能同时运行几个线程。但是python的多线程,只能利用一个cpu的核心。因为Python的解释器使用了GIL的一个叫全局解释器锁,它不能利用多核CPU,只能运行在一个cpu上面,但是运行程序的时候,看起来好像还是在一起运行的,是因为操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。这个叫做上下文切换。
python只有一个GIL,运行python时,就要拿到这个锁才能执行,在遇到I/O 操作时会释放这把锁。如果是纯计算的程序,没有 I/O 操作,解释器会每隔100次操作就释放这把锁,让别的线程有机会 执行(这个次数可以通sys.setcheckinterval来调整)同一时间只会有一个获得GIL线程在跑,其他线程都处于等待状态。
1、如果是CPU密集型代码(循环、计算等),由于计算工作量多和大,计算很快就会达到100,然后触发GIL的释放与在竞争,多个线程来回切换损耗资源,所以在多线程遇到CPU密集型代码时,单线程会比较快;
2、如果是I\O密集型代码(文件处理、网络爬虫),开启多线程实际上是并发(不是并行),IO操作会进行IO等待,线程A等待时,自动切换到线程B,这样就提升了效率。
三、线程锁
python2中在多个线程同时修改一个数据的时候,可能会把数据覆盖,因此需要加线程锁 threading.Lock()。但python3里面不加锁也无所谓,默认会自动帮你加锁。
import threading num = 1 lock = threading.Lock()#实例化一把锁 #Python3默认会加锁,python2需要加锁,不然可能会覆盖以前的数据 def run(): global num lock.acquire() # 加锁 num += 1 lock.release() # 解锁 for i in range(10): t = threading.Thread(target=run) t.start() print(num)
四、守护线程
setDaemon(True),只要主线程结束,那么子线程立即结束,不管子线程有没有运行完成
import threading,time def run(): time.sleep(2) print('hello') for i in range(5): t = threading.Thread(target=run) t.setDaemon(True)#把子线程设置成为守护线程,主线程死了,守护线程也死 t.start() print('Done,运行完成') time.sleep(3)#加上这句,子线程就能执行完成了
五、多进程
一个简单的多线程,multiprocessing.Process(target=run,args=(3,))
import threading,multiprocessing def my(): print('hello') def run(num): for i in range(num): t = threading.Thread(target=my) t.start() #多进程可以利用多核cpu if __name__ == '__main__':#必须加这个才能启动多进程 processes = [] for i in range(2): p = multiprocessing.Process(target=run,args=(3,))#启动一个进程 # args只有一个参数一定后面要加逗号 p.start() processes.append(p) [p.join() for p in processes] #与线程用法一致