python 线程,进程,协程
一、线程与进程
1、什么是进程(process)?(进程是资源集合)
1. 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
2. 程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念
3. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
4. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
5. 进程之间有自己独立的内存,各进程之间不能相互访问
6. 创建一个新线程很简单,创建新进程需要对父进程进行复制
多道编程: 在计算机内存中同时存放几道相互独立的程序,他们共享系统资源
单道编程: 计算机内存中只允许一个的程序运行
2、有了进程为什么还要线程?
1. 进程优点:
提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率
2. 进程的两个重要缺点:
a. 第一点:进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
b. 第二点:进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
c. 例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息
d. 你会说,操作系统不是有分时么?分时是指在不同进程间的分时呀
e. 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀
3、什么是线程(thread)(线程是操作系统最小的调度单位)
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. 一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程
二、多线程
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())
class HMTHread(threading.Thread): # 如果想给自定义线程类中传递参数 需要重写init方法 def __init__(self, name): # 子类必须调用父类的同名init方法 threading.Thread.__init__(self) self.name = name # 任务1 def work1(self): print(threading.current_thread()) print("工作1...") # 任务2 def work2(self): print(self.name) print(threading.current_thread()) print("工作2...") # 程序员需要实现一个方法run方法 def run(self): print(threading.current_thread()) self.work2() self.work1() if __name__ == '__main__': # 如果是单线程(无论是主线程还是子线程 同一时间只能执行一个任务) # 创建一个子线程 sub_thread = HMTHread("自定义线程") # 启动线程(无论使用系统线程 还是自定义线程 都需要启动线程) sub_thread.start()
2、for循环同时启动多个线程
(PS:下面利用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)
注:因为刚刚创建的线程是守护线程,所以主线程结束后子线程就结束了,运行时间不是3秒而是0.01秒
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锁后其他线程对这个资源操作导致混乱问题
import threading # 定义一个全局变量 num = 0 # 定义一个变量 保存互斥锁对象 lock = threading.Lock() def work1(): global num for i in range(1000000): # 上锁 lock.acquire() num += 1 # 解锁 lock.release() print("work1:", num) def work2(): global num for i in range(1000000): # 上锁 lock.acquire() num += 1 # 解锁 lock.release() print("work2:", num) if __name__ == '__main__': # 创建两个子线程 work1_thread = threading.Thread(target=work1) work2_thread = threading.Thread(target=work2) # 启动线程 work1_thread.start() work2_thread.start()
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暂用,这样就死锁了
from threading import Thread,Lock import time mutexA=Lock() mutexB=Lock() class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A锁\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B锁\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B锁\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A锁\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(2): t=MyThread() t.start() # 运行结果:输出下面结果后程序卡死,不再向下进行了 # Thread-1 拿到A锁 # Thread-1 拿到B锁 # Thread-1 拿到B锁 # Thread-2 拿到A锁
三、进程
1、进程间通信方式
1.利用Queues实现父进程到子进程(或子进程间)的数据传递
2.使用管道pipe实现两个进程间数据传递
3.Managers实现很多进程间数据共享
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']
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
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)
2、进程池
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() #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。
3、僵尸进程
僵尸进程定义
1. 僵尸进程产生的原因就是父进程产生子进程后,子进程先于父进程退出
2. 但是父进程由于种种原因,并没有处理子进程发送的退出信号,那么这个子进程就会成为僵尸进程。
#!/usr/bin/env python #coding=utf8 import os, sys, time #产生子进程 pid = os.fork() if pid == 0: #子进程退出 sys.exit(0) #父进程休息30秒 time.sleep(30) # 先产生一个子进程,子进程退出,父进程休息30秒,那就会产生一个僵尸进程
###查看僵尸进程结果
[root@linux-node4 ~]# ps -ef| grep defunct root 110401 96083 0 19:11 pts/2 00:00:00 python defunct.py root 110402 110401 0 19:11 pts/2 00:00:00 [python] <defunct> root 110406 96105 0 19:11 pts/3 00:00:00 grep --color=auto defunct
四、协程
1.1 什么是协程
1)协程微线程,纤程,本质是一个单线程
2)协程能在单线程处理高并发,因为遇到IO自动切换
-
线程遇到I/O操作会等待、阻塞
,协程遇到I/O会自动切换
(剩下的只有CPU操作)- 线程的状态
保存在CPU的寄存器和栈里而协程拥有自己的空间,所以无需上下文切换的开销,所以快
3)
为甚么协程能够遇到I/O自动切换
- greenlet是C语言写的一个模块,遇到IO手动切换
- 协程有一个gevent模块(封装了greenlet模块),遇到I/O自动切换
4)协程拥有自己的空间,所以无需上下文切换的开销
1.2 协程优缺点
-
协程缺点
- 无法利用多核资源:
协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上
,协程需要和进程配合才能运行在多CPU上 协程如果阻塞掉,整个程序都阻塞
- 无法利用多核资源:
-
协程最大的优点
- 不仅是处理高并发(单线程下处理高并发)
- 特别节省资源(协程本质是一个单线程,当然节省资源)
- 500W日活,用php写需要两百多态机器,但是golang只需要二十多太机器
1.3 Python中协程的模块
- greenlet:遇到I/O
手动切换
,是一个C模块 - gevent:对greenlet封装,遇到I/O
自动切换
(借助C语言库greenlet
) - asyncio:和gevent一样,也是实现协程的一个模块(
python自己实现
)
from urllib import request import gevent,time from gevent import monkey monkey.patch_all() #把当前程序所有的I/O操作给我单独做上标记 def f(url): print('GET: %s' % url) resp = request.urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) #1 并发执行部分 time_binxing = time.time() gevent.joinall([ gevent.spawn(f, 'https://www.python.org/'), gevent.spawn(f, 'https://www.yahoo.com/'), gevent.spawn(f, 'https://github.com/'), ]) print("并行时间:",time.time()-time_binxing) #2 串行部分 time_chuanxing = time.time() urls = [ 'https://www.python.org/', 'https://www.yahoo.com/', 'https://github.com/', ] for url in urls: f(url) print("串行时间:",time.time()-time_chuanxing) # 注:为什么要在文件开通使用monkey.patch_all() # 1. 因为有很多模块在使用I / O操作时Gevent是无法捕获的,所以为了使Gevent能够识别出程序中的I / O操作。 # 2. 就必须使用Gevent模块的monkey模块,把当前程序所有的I / O操作给我单独做上标记 # 3.使用monkey做标记仅用两步即可: 第一步(导入monkey模块): from gevent import monkey 第二步(声明做标记) : monkey.patch_all()