1.1 进程与线程简介
1、什么是进程(process)?(进程是资源集合)
定义:1)进程是资源分配最小单位
2)当一个可执行程序被系统执行(分配内存资源)就变成了一个进程
1. 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
2. 程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念
3. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
4. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
5. 进程之间有自己独立的内存,各进程之间不能相互访问
6. 创建一个新线程很简单,创建新进程需要对父进程进行复制
多道编程: 在计算机内存中同时存放几道相互独立的程序,他们共享系统资源,相互穿插运行
单道编程: 计算机内存中只允许一个的程序运行
进程并发性:
1)在一个系统中,同时会存在多个进程被加载到内存中,同处于开始到结束之间的状态
2)对于一个单CPU系统来说,程序同时处于运行状态只是一种宏观上的概念
他们虽然都已经开始运行,但就微观而言,任意时刻,CPU上运行的程序只有一个
3)由于操作系统分时,让每个进程都觉得自己独占CPU等资源
注:如果是多核CPU(处理器)实际上是可以实现正在意义的同一时间点有多个线程同时运行
线程并发性:
1)操作系统将时间划分为很多时间段,尽可能的均匀分配给每一个线程。
2)获取到时间片的线程被CPU执行,其他则一直在等待,所以微观上是走走停停,宏观上都在运行。
多核CPU情况:
如果你的程序的线程数少于CPU的核心数,且系统此时没有其他进程同时运行,那么这个程序的每个线程会享有一个CPU,
当同时运行的线程数多于CPU核心数时,CPU会采用一定的调度算法每隔一段时间就将这些线程调入或调出CPU
以确保每个线程都能分享一部分CPU时间,实现多线程并发。
2、有了进程为什么还要线程?
1. 进程优点:
提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率
2. 进程的两个重要缺点
a. 第一点:进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
b. 第二点:进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。
c. 例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息
d. 你会说,操作系统不是有分时么?分时是指在不同进程间的分时呀
e. 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀
3、什么是线程(thread)(线程是操作系统最小的调度单位)
定义:1)线程是操作系统调度的最小单位
2)它被包含在进程之中,是进程中的实际运作单位
3)进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
1. 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
2. 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
3. 无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行
4. 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
5. 所有在同一个进程里的线程是共享同一块内存空间的,不同进程间内存空间不同
6. 同一个进程中的各线程可以相互访问资源,线程可以操作同进程中的其他线程,但进程仅能操作子进程
7. 两个进程想通信,必须要通过一个中间代理
8. 对主线程的修改可能回影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是
同一进程下的线程共享内存
4、进程和线程的区别
启动一个线程比启动一个进程快,运行速度没有可比性。
先有一个进程然后才能有线程。
1、进程包含线程
2、线程共享内存空间
3、进程内存是独立的(不可互相访问)
4、进程可以生成子进程,子进程之间互相不能互相访问(相当于在父级进程克隆两个子进程)
5、在一个进程里面线程之间可以交流。两个进程想通信,必须通过一个中间代理来实现
6、创建新线程很简单,创建新进程需要对其父进程进行克隆。
7、一个线程可以控制或操作同一个进程里面的其它线程。但进程只能操作子进程。
8、父进程可以修改不影响子进程,但不能修改。
9、线程可以帮助应用程序同时做几件事
5、进程和程序的区别
1. 程序只是一个普通文件,是一个机器代码指令和数据的集合,所以,程序是一个静态的实体
2. 而进程是程序运行在数据集上的动态过程,进程是一个动态实体,它应创建而产生,应调度执行因等待资
源或事件而被处于等待状态,因完成任务而被撤消
3. 进程是系统进行资源分配和调度的一个独立单位
4.一个程序对应多个进程,一个进程为多个程序服务(两者之间是多对多的关系)
5. 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程
1.2 多线程
Python多线程编程中常用方法:
1、join()方法:如果一个线程或者在函数执行的过程中调用另一个线程,并且希望待其完成操作后才能执行,
那么在调用线程的时就可以使用被调线程的join方法join([timeout]) timeout:可选参数,线程运行的最长时间
2、isAlive()方法:查看线程是否还在运行
3、getName()方法:获得线程名
4、setDaemon()方法:主线程退出时,需要子线程随主线程退出,则设置子线程的setDaemon()
1、线程2种调用方式:直接调用, 继承式调用
import threading import time def sayhi(num): # 定义每个线程要运行的函数 print("running on number:%s" % num) time.sleep(3) #1、target=sayhi :sayhi是定义的一个函数的名字 #2、args=(1,) : 括号内写的是函数的参数 t1 = threading.Thread(target=sayhi, args=(1,)) # 生成一个线程实例 t2 = threading.Thread(target=sayhi, args=(2,)) # 生成另一个线程实例 t1.start() # 启动线程 t2.start() # 启动另一个线程 print(t1.getName()) # 获取线程名 print(t2.getName())
import threading import time class MyThread(threading.Thread): def __init__(self,num): threading.Thread.__init__(self) self.num = num def run(self):#定义每个线程要运行的函数 print("running on number:%s" %self.num) time.sleep(3) if __name__ == '__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()
2、for循环同时启动多个线程
说明:下面利用for循环同时启动50个线程并行执行,执行时间是3秒而不是所有线程执行时间的总和
import threading import time def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.start()
3、t.join(): 实现所有线程都执行结束后再执行主线程
说明:在4中虽然可以实现50个线程同时并发执行,但是主线程不会等待子线程结束在这里我们可以使用t.join()指定等待某个线程结束的结果
import threading import time start_time = time.time() def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) t_objs = [] #将进程实例对象存储在这个列表中 for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.start() #启动一个线程,程序不会阻塞 t_objs.append(t) print(threading.active_count()) #打印当前活跃进程数量 for t in t_objs: #利用for循环等待上面50个进程全部结束 t.join() #阻塞某个程序 print(threading.current_thread()) #打印执行这个命令进程 print("----------------all threads has finished.....") print(threading.active_count()) print('cost time:',time.time() - start_time)
4、setDaemon(): 守护线程,主线程退出时,需要子线程随主线程退出
import threading import time start_time = time.time() def sayhi(num): #定义每个线程要运行的函数 print("running on number:%s" %num) time.sleep(3) for i in range(50): t = threading.Thread(target=sayhi,args=('t-%s'%i,)) t.setDaemon(True) #把当前线程变成守护线程,必须在t.start()前设置 t.start() #启动一个线程,程序不会阻塞 print('cost time:',time.time() - start_time)
5、GIL锁和用户锁(Global Interpreter Lock 全局解释器锁
)
1.全局解释器锁:保证同一时间仅有一个线程对资源有操作权限
作用:在一个进程内,同一时刻只能有一个线程通过GIL锁 被CUP调用,切换条件:I/O操作、固定时间(系统决定)
说明:python多线程中GIL锁只是在CPU操作时(如:计算)才是串行的,其他都是并行的,所以比串行快很多
1)为了解决不同线程同时访问同一资源时,数据保护问题,而产生了GIL
2)GIL在解释器的层面限制了程序在同一时间只有一个线程被CPU实际执行,而不管你的程序里实际开了多少条线程
3)为了解决这个问题,CPython自己定义了一个全局解释器锁,同一时间仅仅有一个线程可以拿到这个数据
4)python之所以会产生这种不好的状况是因为python启用一个线程是调用操作系统原生线程,就是C接口
5)但是这仅仅是CPython这个版本的问题,在PyPy,中就没有这种缺陷
2. 用户锁:线程锁(互斥锁Mutex) :当前线程还未操作完成前其他所有线程都无法对其操作,即使已经释放了GIL锁
1. 在有GIL锁时为何还需要用户锁
1)GIL锁只能保证同一时间只能有一个线程对某个资源操作,但当上一个线程还未执行完毕时可能就会释放GIL,其他线程就可以操作了
2. 线程锁的原理
1)当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作
2)这样就可以防止还未计算完成,释放GIL锁后其他线程对这个资源操作导致混乱问题
3. 在有GIL的情况下执行 count = count + 1 会出错,用线程锁解决方法
# 1)第一步:count = 0 count初始值为0 # 2)第二步:线程1要执行对count加1的操作首先申请GIL全局解释器锁 # 3)第三步:调用操作系统原生线程在操作系统中执行 # 4)第四步:count加1还未执行完毕,时间到了被要求释放GIL # 5)第五步:线程1释放了GIL后线程2此时也要对count进行操作,此时线程1还未执行完,所以count还是0 # 6)第六步:线程2此时拿到count = 0后也要对count进行加1操作,假如线程2执行很快,一次就完成了 # count加1的操作,那么count此时就从0变成了1 # 7)第七步:线程2执行完加1后就赋值count=1并释放GIL # 8)第八步:线程2执行完后cpu又交给了线程1,线程1根据上下文继续执行count加1操作,先拿到GIL # 锁,完成加1操作,由于线程1先拿到的数据count=0,执行完加1后结果还是1 # 9)第九步:线程1将count=1在次赋值给count并释放GIL锁,此时连个线程都对数据加1,但是值最终是1
1、使用线程锁解决上面问题的原理
1) 在GIL锁中再加一个线程锁,线程锁是用户层面的锁
2) 线程锁就是一个线程在对数据操作前加一把锁,防止其他线程复制或者操作这个数据
3) 只有这个线程对数据操作完毕后才会释放这个锁,其他线程才能操作这个数据
2、定义一个线程锁非常简单只用三步:
第一步: lock = threading.Lock() #定义一把锁
第二步: lock.acquire() #对数据操作前加锁防止数据被另一线程操作
第三步: lock.release() #对数据操作完成后释放锁
6、死锁
1. 死锁定义
两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
2. 死锁举例
1. 启动5个线程,执行run方法,假如thread1首先抢到了A锁,此时thread1没有释放A锁,紧接着执行代码mutexB.acquire(),抢到了B锁,
在抢B锁时候,没有其他线程与thread1争抢,因为A锁没有释放,其他线程只能等待
2. thread1执行完func1函数,然后执行func2函数,此时thread1拿到B锁,然后执行time.sleep(2),此时不会释放B锁
3. 在thread1执行func2的同时thread2开始执行func1获取到了A锁,然后继续要获取B锁
4. 不幸的是B锁还被thread1占用,thread1占用B锁时还需要同时获取A锁才能向下执行,但是此时发现A锁已经被thread2暂用,这样就死锁了
7、递归锁:lock = threading.RLock() 解决死锁问题
1. 递归锁的作用是同一线程中多次请求同一资源,但是不会参数死锁。
2. 这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。
3. 直到一个线程所有的acquire都被release,其他的线程才能获得资源。
8、Semaphore(信号量)
1. 互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
2. 比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去
3. 作用就是同一时刻允许运行的线程数量
9、events总共就只有四个方法
1. event.set() : 设置标志位
2. event.clear() : 清除标志位
3. event.wait() : 等待标志被设定
4. event.is_set() : 判断标志位是否被设定
1.3 进程
1、多线程和多进程各自应用场景
1. I/O操作不占用CPU(从硬盘,网路读入数据等)
2. 计算占用CPU,这种情况最好不用多线程
3. python多线程不适合CPU密集型的任务,适合I/O密集型的任务
4. python的多进程适合CPU密集型任务
2、一次性起多个进程,并在进程中调用线程
import multiprocessing,time,threading #3 被多线程调用的函数 def thread_run(): print(threading.get_ident()) #打印线程id号 time.sleep(2) #2 被多进程调用的函数,以及在这个函数中起一个进程 def run(name): time.sleep(2) print("hello",name) t = threading.Thread(target=thread_run,) #在进程调用的函数中启用一个线程 t.start() #1 一次性启动多个进程 if __name__ == '__main__': for i in range(10): p = multiprocessing.Process(target=run,args=('bob %s'%i,)) #启用一个多线程 p.start()
3、进程间互相访问数据的三种方法
注:不同进程间内存是不共享的,所以互相之间不能访问对方数据
1. 在父进程中定义队列q,使用父进程启用一个子进程,子进程中无法操作父进程的q
from multiprocessing import Process import queue import threading def f(): q.put([42, None, 'hello']) if __name__ == '__main__': q = queue.Queue() #1 在父进程中定义一个队列实例q # p = threading.Thread(target=f,) #在线程程中就可以相互访问,线程中内存共享 p = Process(target=f,) #2 在父进程中起一个子进程 p,在子进程中使用父进程的q会报错 p.start() print(q.get()) p.join()
2. 法1: 利用Queues实现父进程到子进程(或子进程间)的数据传递
1. 我们以前学的queue是线程queue.Queue()只有在同一个进程的线程间才能访问
2. 如果两个进程间想要通信必须要使用进程Queue,用法和多线程的相同
3. queue.Queue()是线程q不可以传递给子进程,但是Queue是进程q,父进程会将进程q克隆了一份给子进程
4.既然是两个q为什么在子进程中在q中放入一个数据在父进程中可以取出来呢? 其实原因是这样的:
1)子进程向q中放入数据的时候,用pickle序列化将数据放到一个中间地方(翻译),翻译又把子进程放
入的数据用pickle反序列化给父进程,父进程就可以访问这个q了,这样就实现了进程间的数据通信了
2) 在多线程中两个线程可以修改同一份数据,而Queue仅仅实现了进程间的数据传递
from multiprocessing import Process, Queue def f(qq): # 将符进程中的q传递过来叫qq qq.put([42, None, 'hello']) # 此时子进程就可以使用符进程中的q if __name__ == '__main__': q = Queue() # 使用Queue()在父进程中定义一个队列实例q p = Process(target=f, args=(q,)) # 在父进程中起一个子进程 p,将父进程刚定义的q传递给子进程p p.start() print(q.get()) p.join() # 运行结果: [42, None, 'hello']
3. 法2: 使用管道pipe实现两个进程间数据传递
说明:其实pip实现进程间通信就好像一条电话线一样,一个在电话线这头发送,一个在电话线那头接收
from multiprocessing import Process, Pipe def f(conn): conn.send([42, None, 'hello']) # 3 子进程发送数据,就像socket一样 print("son process recv:", conn.recv()) conn.close() if __name__ == '__main__': parent_conn, child_conn = Pipe() # 1 生成一个管道实例,实例一生成就会生成两个返回对象,一个是管道这头,一个是管道那头 p = Process(target=f, args=(child_conn,)) # 2 启动一个子进程将管道其中一头传递给子进程 p.start() print(parent_conn.recv()) # 4 父进程收消息 # prints "[42, None, 'hello']" parent_conn.send('i am parent process') p.join() # 运行结果: # [42, None, 'hello'] # son process recv: i am parent process
4.法3: Managers实现很多进程间数据共享
说明: manager实质和Queue一样,启用是个线程其实就是将字典或者列表copy十份
from multiprocessing import Process, Manager import os def f(d, l): d[1] = '1' # 是个进程对字典放入的是同一个值,所以看上去效果不明显 l.append(os.getpid()) # 将这是个进程的进程id放入列表中 if __name__ == '__main__': with Manager() as manager: # 1 将Manager()赋值给manager d = manager.dict() # 2 定义一个可以在多个进程间可以共享的字典 l = manager.list(range(5)) # 3 定义一个可以在多个进程间可以共享的列表,默认写五个数据 p_list = [] for i in range(10): # 生成是个进程 p = Process(target=f, args=(d, l)) # 将刚刚生成的可共享字典和列表传递给子进程 p.start() p_list.append(p) for res in p_list: res.join() print(d) print(l)
4、进程之间需要锁的原因
说明:虽然每个进程是独立运行的,但是他们共享同一块屏幕,如果大家都在屏幕打数据就会打乱了
from multiprocessing import Process, Lock def f(l, i): l.acquire() #一个进程要打印数据时先锁定 print('hello world', i) l.release() #打印完毕后就释放这把锁 if __name__ == '__main__': lock = Lock() #先生成一把锁 for num in range(5): Process(target=f, args=(lock, num)).start() # 运行结果: # hello world 4 # hello world 0 # hello world 2 # hello world 3 # hello world 1
5、进程池
1. 进程池的作用就是限制同一时间可以启动进程的=数量
2. 进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进
,那么程序就会等待,直到进程池中有可用进程为止。
3. 进程池中有两个方法:
1)apply: 多个进程异步执行,一个一个的执行
2)apply_async: 多个进程同步执行,同时执行多个进程
from multiprocessing import Process,Pool import time,os def foo(i): time.sleep(2) print("in the process",os.getpid()) #打印子进程的pid return i+100 def call(arg): print('-->exec done:',arg,os.getpid()) if __name__ == '__main__': pool = Pool(3) #进程池最多允许5个进程放入进程池 print("主进程pid:",os.getpid()) #打印父进程的pid for i in range(10): #用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数 pool.apply_async(func=foo, args=(i,),callback=call) #用法2 串行 启动进程不在用Process而是直接用pool.apply() # pool.apply(func=foo, args=(i,)) print('end') pool.close() #关闭pool pool.join() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。
6、僵尸进程
1)僵尸进程定义
1. 僵尸进程产生的原因就是父进程产生子进程后,子进程先于父进程退出
2. 但是父进程由于种种原因,并没有处理子进程发送的退出信号,那么这个子进程就会成为僵尸进程。
2)用python写一个僵尸进程
import os, sys, time #产生子进程 pid = os.fork() if pid == 0: #子进程退出 sys.exit(0) #父进程休息30秒 time.sleep(30) # 先产生一个子进程,子进程退出,父进程休息30秒,那就会产生一个僵尸进程
1.4 协程(Coroutine)
1、什么是协程(进入上一次调用的状态)
1. 协程,又称微线程,纤程,协程是一种用户态的轻量级线程。
2. 线程的切换会保存到CPU的栈里,协程拥有自己的寄存器上下文和栈,
3. 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
4. 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态
5. 协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)
2、协程的好处
1. 无需线程上下文切换的开销(可以理解为协程切换就是在不同函数间切换,不用像线程那样切换上下文CPU)
2. 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突
3. 用法:最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
3、协程缺点
1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上
2. 线程阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
4、使用yield实现协程相同效果
import time import queue def consumer(name): print("--->starting eating baozi...") while True: new_baozi = yield # 只要遇到yield程序就返回,yield还可以接收数据 print("[%s] is eating baozi %s" % (name, new_baozi)) time.sleep(1) def producer(): r = con.__next__() # 直接调用消费者的__next__方法 r = con2.__next__() # 函数里面有yield第一次加括号调用会变成一个生成器函数不执行,运行next才执行 n = 0 while n < 5: n += 1 con.send(n) # send恢复生成器同时并传递一个值给yield con2.send(n) print("