一、多任务(多线程)
多线程特点:
(1)线程的并发是利用cpu上下文的切换(是并发,不是并行)
(2)多线程执行的顺序是无序的
(3)多线程共享全局变量
(4)线程是继承在进程里的,没有进程就没有线程
(5)GIL全局解释器锁
(6)只要在进行耗时的IO操作的时候,能释放GIL,所以只要在IO密集型的代码里,用多线程就很合适
# 无序的,并发的 import threading import time def test1(n): time.sleep(1) print('task', n) for i in range(10): t = threading.Thread(target=test1,args=('t-%s' % i,)) t.start() -------------------------------------------------------------------- task t-9 task t-8 task t-7tasktask t-6 t-5 task t-4 task t-3 task t-2 task t-1 task t-0
#计算并发所用的时间 import threading import time def test1(n): time.sleep(1) print('task', n) def test2(n): time.sleep(1) print('task', n) start = time.time() l = [] t1 = threading.Thread(target=test1, args=(1,)) #args是一个元组,括号内如果是一个数值的话,需要再加一个,使其成为一个元组 t2 = threading.Thread(target=test1, args=(2,)) t1.start() t2.start() l.append(t1) l.append(t2) for i in l: i.join() end = time.time() print(end - start) -------------------------------------------------------------------- task 2 task 1 1.0170276165008545
GIL的全称是:Global Interpreter Lock,意思就是全局解释器锁,这个GIL并不是python的特性,他是只在Cpython解释器里引入的一个概念,而在其他的语言编写的解释器里就没有这个GIL例如:Jython,Pypy
为什么会有gil?:
随着电脑多核cpu的出现核cpu频率的提升,为了充分利用多核处理器,进行多线程的编程方式更为普及,随之而来的困难是线程之间数据的一致性和状态同步,而python也利用了多核,所以也逃不开这个困难,为了解决这个数据不能同步的问题,设计了gil全局解释器锁。
说到gil解释器锁,我们容易想到在多线程中共享全局变量的时候会有线程对全局变量进行的资源竞争,会对全局变量的修改产生不是我们想要的结果,而那个时候我们用到的是python中线程模块里面的互斥锁,哪样的话每次对全局变量进行操作的时候,只有一个线程能够拿到这个全局变量;看下面的代码:
import threading import time lock = threading.Lock() #Lock后面必须加括号threading.Lock() num = 0 def test1(): # time.sleep(1) global num # lock.acquire() for i in range(1000000): num += 1 # lock.release() def test2(): # time.sleep(1) global num # lock.acquire() for i in range(1000000): num += 1 # lock.release() t1 = threading.Thread(target=test1) #target=test1不能有括号,如果是target=test1()则是两个线程串行执行,先执行完一个后执行另一个 t2 = threading.Thread(target=test2) t1.start() t2.start() t1.join() t2.join() print(num) #multithreading ---------------------------------------------- 1707070 #不加GIL全局解释锁,结果是一个随机数
import threading import time lock = threading.Lock() #Lock后面必须加括号threading.Lock() num = 0 def test1(): # time.sleep(1) global num lock.acquire() for i in range(1000000): num += 1 lock.release() def test2(): # time.sleep(1) global num lock.acquire() for i in range(1000000): num += 1 lock.release() t1 = threading.Thread(target=test1) #target=test1不能有括号,如果是target=test1()则是两个线程串行执行,先执行完一个后执行另一个 t2 = threading.Thread(target=test2) t1.start() t2.start() t1.join() t2.join() print(num) #multithreading ------------------------------------------------------------------- 2000000 #加了GIL全局解释锁后的结果
二、多进程
一个程序运行起来之后,代码+用到的资源称之为进程,它是操作系统分配资源的基本单位,不仅可以通过线程完成多任务,进程也是可以的()
进程之间是相互独立的
cpu密集的时候适合用多进程
1、进程之间资源不共享
import multiprocessing num = 0 def test1(): global num for i in range(10): num += 1 def test2(): global num num += 22 if __name__ == '__main__': p1 = multiprocessing.Process(target=test1) p2 = multiprocessing.Process(target=test2) p1.start() p2.start() p1.join() #join()方法可以等子进程结束后再继续往下运行,通常用于进程间的同步 p2.join() print(num) #independent ----------------------------------------------------------------------- 0
2、多进程并发
import multiprocessing import time def test1(): for i in range(10): time.sleep(1) print('test1',i) def test2(): for i in range(10): time.sleep(1) print('test2',i) if __name__ == '__main__': p1 = multiprocessing.Process(target=test1) p2 = multiprocessing.Process(target=test2) p1.start() p2.start() #multi-core processor ----------------------------------------------------------- test1 0 test2 0 test1 1 test2 1 test1 2 test2 2 test2 3 test1 3 test2 4 test1 4 test2 5 test1 5 test2 6 test1 6 test2 7 test1 7 test1 8 test2 8 test2 9 test1 9
3、进程池并发(如果要启动大量的子进程,可以用进程池的方式批量创建子进程)
# import multiprocessing import time from multiprocessing import Pool #Pool中的P要大写的 def test1(): time.sleep(1) for i in range(10): print('test1',i) def test2(): time.sleep(1) for i in range(10): print('test2',i) if __name__ == '__main__': pool = Pool(5) #Pool的默认大小是CPU的核数,进程数可以手动更改 pool.apply_async(test1) pool.apply_async(test2) pool.close() pool.join() #调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的process了。 # pool.close() -------------------------------------------------------------------- test1 0 test1 1 test1 2 test1 3 test1 4 test1 5 test1 6 test1 7 test1 8 test1 9 test2 0 test2 1 test2 2 test2 3 test2 4 test2 5 test2 6 test2 7 test2 8 test2 9
4、子进程
很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。
subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。
import subprocess print('$ nslookup www.python.org') r = subprocess.call(['nslookup','www.python.org',]) print('Exit code:', r) -------------------------------------------------------------- $ nslookup www.python.org ������: UnKnown Address: 192.168.11.2 ����: dualstack.python.map.fastly.net Address: 151.101.108.223 Aliases: www.python.org.localdomain Exit code: 0
如果子进程还需要输入,则可以通过communicate()方法输入:
相当于在命令行执行命令nslookup,然后手动输入:
set q=mx
python.org
exit
import subprocess print('$ nslookup') p = subprocess.Popen(['nslookup'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE) output, err = p.communicate(b'set q=mx\npython.org\nexit\n',) print(output.decode('gbk')) print('Exit code:',p.returncode) ---------------------------------------------------------------------- $ nslookup 默认服务器: UnKnown Address: 192.168.11.2 > > 服务器: UnKnown Address: 192.168.11.2 python.org MX preference = 50, mail exchanger = mail.python.org (root) nameserver = l.root-servers.net (root) nameserver = c.root-servers.net (root) nameserver = f.root-servers.net (root) nameserver = h.root-servers.net (root) nameserver = i.root-servers.net (root) nameserver = m.root-servers.net (root) nameserver = b.root-servers.net (root) nameserver = k.root-servers.net (root) nameserver = j.root-servers.net (root) nameserver = d.root-servers.net (root) nameserver = e.root-servers.net (root) nameserver = g.root-servers.net (root) nameserver = a.root-servers.net a.root-servers.net internet address = 198.41.0.4 a.root-servers.net AAAA IPv6 address = 2001:503:ba3e::2:30 b.root-servers.net internet address = 199.9.14.201 b.root-servers.net AAAA IPv6 address = 2001:500:200::b c.root-servers.net internet address = 192.33.4.12 c.root-servers.net AAAA IPv6 address = 2001:500:2::c d.root-servers.net internet address = 199.7.91.13 d.root-servers.net AAAA IPv6 address = 2001:500:2d::d e.root-servers.net internet address = 192.203.230.10 e.root-servers.net AAAA IPv6 address = 2001:500:a8::e f.root-servers.net internet address = 192.5.5.241 > Exit code: 0
5、进程间通信
Process
之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing
模块包装了底层的机制,提供了Queue
、Pipes
等多种方式来交换数据。
我们以Queue
为例,在父进程中创建两个子进程,一个往Queue
里写数据,一个从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__': # 父进程创建Queue,并传给各个子进程: q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) # 启动子进程pw,写入: pw.start() # 启动子进程pr,读取: pr.start() # 等待pw结束: pw.join() # pr进程里是死循环,无法等待其结束,只能强行终止: pr.terminate() ---------------------------------------------------------------- Process to write: 2024 Put A to queue... Process to read: 4308 Get A from queue. Put B to queue... Get B from queue. Put C to queue... Get C from queue.
三、协程并发(gevent)
1、协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
2、优点:
(1)协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
(2)单线程内就可以实现并发的效果,最大限度的利用CPU
3、缺点:
(1)协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
(2)协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
4、协程的特点:
(1)必须在只有一个单线程里实现并发
(2)修改共享数据不需加锁
(3)用户程序里自己保存多个控制流的上下文栈
附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield,greenlet都无法实现,就用到了gevent模块)
5、使用生成器yield进行切换案例:
# 进程 启动多个进程 进程之间是由操作系统负责调用 # 线程 启动多个线程 真正被CPU执行的最小单位实际是线程 # 开启一个线程 创建一个线程 # 协程 # 本质上是一个线程 # 能够在多个任务之间切换来节省一些IO(输入输出)时间 # 协程中任务之间的切换也消耗时间,但是开销要远远小于进程线程之间的切换 # 实现并发的手段 def consumber(): while True: x = yield print("处理了数据:",x) def producer(): c = consumber() next(c) for i in range(10): print("生产了数据:",i) c.send(i) producer() ------------------------------------------------------------------- 生产了数据: 0 处理了数据: 0 生产了数据: 1 处理了数据: 1 生产了数据: 2 处理了数据: 2 生产了数据: 3 处理了数据: 3 生产了数据: 4 处理了数据: 4 生产了数据: 5 处理了数据: 5 生产了数据: 6 处理了数据: 6 生产了数据: 7 处理了数据: 7 生产了数据: 8 处理了数据: 8 生产了数据: 9 处理了数据: 9
6、使用gevent模块实现协程
# 协程,自动切换 import gevent,time #gevent是一个基于协程的python网络库,是第三方模块,需要下载 from gevent import monkey monkey.patch_all() def test1(): for i in range(10): time.sleep(1) print('test1-%d'% i ) def test2(): for i in range(10): time.sleep(2) print('test2-%d'% i ) g1 = gevent.spawn(test1) g2 = gevent.spawn(test2) g1.join() g2.join() # 协程 ------------------------------------------------------------------------ test1-0 test2-0 test1-1 test1-2 test2-1 test1-3 test1-4 test2-2 test1-5 test1-6 test2-3 test1-7 test1-8 test2-4 test1-9 test2-5 test2-6 test2-7 test2-8 test2-9
进程、线程、协程总结:
进程是资源分配的单位
线程是操作系统调度的单位
进程切换需要的资源最大,效率低
线程切换需要的资源一般,效率一般
线程是最小的调度单位,进程是最小的管理单元
协程切换任务资源很小,效率高
多进程、多线程根据cpu核数不一样可能是并行的,但是协成在一个线程中
四、概念部分
1、并发:指的是任务数多余CPU核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
2、并行:指的是任务数小于等于CPU核数,即任务真的是一起执行的
3、进程与程序的区别:
程序:编写完毕的代码,在没有运行的时候,称为程序
进程:正在运行的代码就是进程,占用了一些资源(用到了内存、CPU、键盘)
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
而进程是程序在处理机上的一次执行过程,它是一个动态的概念
程序可以作为一种软件资料长期存在,而进程是有一定生命期的
程序是永久的,进程是暂时的;程序是没有生命的,进程是有生命的
注意:同一个程序执行两次,就会在操作系统中出现两个进程,所以我们可以同时运行一个软件,分别做不同的事情也不会混乱
4、进程的状态
工作中,任务数往往大于CPU的核数,即一定有一些任务正在执行,而另外一些任务在等待CPU进行执行,因此导致有了不同的状态
就绪态:运行的条件都已近满足,正在等待CPU执行
执行态:CPU正在执行其功能
等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态
5、同步异步
所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。
所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
6、线程
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程;进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是CPU上的执行单位;
线程的创建开销小
进程之间是竞争关系,线程之间是协作关系
7、线程与进程的区别(概念)
(1)线程共享创建它的进程的地址空间;进程有自己的地址空间。
(2)线程可以直接访问其进程的数据段;进程有自己的父进程数据段副本。
(3)线程可以直接与其进程的其他线程通信;进程必须使用进程间通信与兄弟进程通信。
(4)新线程很容易创建;新进程需要父进程的重复。
(5)线程可以对同一进程的线程进行相当大的控n制;进程只能对子进程进行控制。
(6)对主线程的更改(取消、优先级更改等)可能会影响进程的其他线程的行为;对父进程的更改不会影响子进程。
补充:区别和联系
一个程序至少有一个进程,一个进程至少有一个线程.
线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
线线程不能够独立执行,必须依存在进程中
总结:
进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源
进程是资源分配的单位,线程是cpu调度的单位
资源
进程是资源分配的单位,线程是cpu调度的单位
8、进程和线程的区别:(代码实现)
(1)运行方式不同:
进程不能单独执行,他只是资源的集合;进程要操作CPU,必须要先创建一个线程;所有在同一个进程里的线程,是共享同一块进程所占的内存空间。
(2)关系
进程中第一个线程是主线程,主线程可以创建其他线程;其他线程也可以创建线程;线程之间是平等的。
进程有父进程和子进程,独立的内存空间,唯一的标识符:pid
多线程是指一个线程内开多个线程;线程所在进程的pid都是一样的
(3)速度
启动线程比启动进程快
运行线程和运行进程速度上是一样的,没有可比性
线程共享内存空间,进程的内存是独立的
(4)创建
父进程生成子进程,相当于复制一份内存空间,进程之间不能直接访问
创建新线程很简单,创建新进程需要对父进程进行一次复制
一个线程可以控制和操作同级线程里的其他线程,但是进程只能操作子进程
(5)交互
同一个进程里的线程之间可以直接访问
两个进程想通信必须通过一个中间代理来实现
总结:
(1)主线程等待子线程的原因:因为主线程实际上是代表进程的生命周期
(2)主进程等子进程的原因:因为需要给子进程收尸
(3)进程什么时候结束:在进程内所有其他线程都结束,进程才结束
9、GIL面试题
(1)描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因。
GIL:又叫全局解释器锁,每个线程在执行的过程中都需要先获取GIL,保证同一时刻只有一个线程在运行,目的是解决多线程同时竞争程序中的全局变量而出现的线程安全问题。它并不是python语言的特性,仅仅是由于历史的原因在CPython解释器中难以移除,因为python语言运行环境大部分默认在CPython解释器中。Python使用多进程是可以利用多核的CPU资源的。多线程爬取比单线程性能有提升,因为遇到IO阻塞会自动释放GIL锁。
(2)解决GIL问题的方案:
1.使用其它语言,例如C,Java
2.使用其它解释器,如java的解释器jython
3.使用多进程
线程释放GIL锁的情况:
1.在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL。
2.Python 3.x使用计时器(执行时间达到阈值后,当前线程释放GIL)或Python 2.x,tickets计数达到100。
(3)什么时候会释放Gil锁?
1、遇到像 i/o操作这种 会有时间空闲情况 造成cpu闲置的情况会释放Gil
2、线程释放GIL锁的情况: 在IO操作等可能会引起阻塞的系统调用之前,可以暂时释放GIL。
(4)如何避免GIL带来的影响?
方法一:用进程+协程 代替 多线程的方式
在多进程中,由于每个进程都是独立的存在,所以每个进程内的线程都拥有独立的GIL锁,互不影响。但是,由于进程之间是独立的存在,所以进程间通信就需要通过队列的方式来实现。
方法二:更换解释器
像JPython和IronPython这样的解析器由于实现语言的特性,他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。
(5)GIL有什么作用?
1、为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据的一致性和状态同步的完整性。
2、python为了利用多核,开始支持多线程,但线程是非独立的,所以同一进程里线程是数据共享,当各个线程访问数据资源时会出现竞状态,即数据可能会同时被多个线程占用,造成数据混乱,这就是线程的不安全。而解决多线程之间数据完整性和状态同步最简单的方式就是加锁。GIL能限制多线程同时执行,保证同一时间内只有一个线程在执行。
3、单核下实现多任务。在开发cPython解析器的时候是单核的情况下,用程序去切换线程,使用gil锁来控制线程的切换。
(6)为什么在python中有全局解释器锁,但Java,C++中却没有?
首先,因为它们都是编译型语言。其次,Java没有用该死的计数GC,撑死在全局GC触发时让别的玩意全候着。而C++直接没做GC,而智能指针靠着一堆atomic保障着。
(7)互斥锁和Gil锁的关系
Gil锁:保证同一时刻只有一个线程能使用到cpu
互斥锁:多线程时,保证修改共享数据时有序的修改,不会产生数据修改混乱