进程
定义:一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,每一个进程都会消耗一定的内存资源。
进程主要是负责索要内存资源,而实际上干活的是线程
一个程序至少有一个进程,一个进程默认有多个线程
没有进程就没有线程。
多进程可以完成多个任务,比如可以同时使用qq聊天和QQ音乐听歌,
多进程的使用
multiprocessing
通过这个包中的Process
进程类实现
Process(group,target,)
手动创建进程管理,使用pocess模块
使用步骤:
- 创建要进行的程序
- 创建子进程对象
p = Process(target=run_proc, args=(1,)
p1 = Process(target)
3.启动进程,子进程的执行是无序的,
p.start()
p1.start()- 等待子进程执行结束join()
获取进程编号os.getpid()
os.getppid()
如果在一个函数中获取编号,而这个函数交给了一个子进程执行那么os.getpid()
获取的是子进程的编号,也就是放在哪里就是获取哪里的进程编号,
要获取父进程的编号就使用os.getppid()
获取当前进程对象multiprocessing.current_process()
根据进程编号杀死进程:os.kill(进程编号)
执行需要传递参数的进程
在创建进程时将参数传入Process(target=,args=,kwargs=)
要注意args
是通过元组传递参数,传递要严格按照顺序kwargs
是通过字典进行传参,传递时候通过关键字;两种方法可以同时使用,先使用元组将前面的元素参数传递完成再使用字典传递剩余参数.
注意!
- 进程之间不共享全局变量,对于window实际上会进行拷贝,对于LInux和mac不会拷贝,这里要注意
创建一个子进程就是对主进程资源的拷贝,子进程是主进程的一个副本,
如何解决Windows中的问题:
通过
if __name__ == "__mian__":
#这里调用子进程,可以防止Windows递归调用拷贝创建子进程.
- 主进程会等待虽有的子进程执行结束再结束.
进程之间的通信问题
如果创建里大量的进程,进程之间往往需要通信,python提供了Queue
,Pipe
方法,其区别在于后者常用来在两个进程之间通信,而前者用于多个任务之间通信.
Queue实现多进程之间数据传递有两个方法Put
,Get
.
Put用于插入数据到队列,有两个可选参数,blocked和timeout,如果blocked=True,且timeout为正值,则该方法会阻塞timeout指定时间,直到队列有剩余空间,如果超时,会抛出Queue.Full.如果blocked=False,那么如果队列已满,则会立即报出Queue.Full.错误
Get方法可以从队列读取并删除一个元素,其也有两个参数,blocked和timeout,如果blocked=True,且timeout为正值,则该方法会阻塞timeout指定时间,那么在指定时间里没有获取到任何元素,就会抛出Queue.Empty异常,如果超时,如过blocked=False,当Queue中有一个值可用,那么就立即返回该值,否则队列为空立即抛出异常.
如:在一个主进程中创建三个子进程,两个用于向Queue中写入数据,一个用于从Queue中读取数据.
from multiprocessing import Process, Queue
import time, random, os
def write_task(q,url):
print("Process %s is writing..." % os.getpid())
for url in url:
q.put(url)# 向队列中写入数据
print("put %s to queue" % url)
time.sleep(random.random())
def read_task(q):
print("process %s is reading..." % os.getpid())
while True:
url = q.get(True)# blocked默认为True,从队列中获取数据,
print("Get %s from queue" % url)
if __name__ == "__main__":
# 父进程创建队列并传递给各个子进程
q = Queue()
# 创建进程
write_process1 = Process(target=write_task, args=(q, ['url1','url2','uel3']))
write_process2 = Process(target=write_task, args=(q, ['url4','url5','uel6']))
read_process = Process(target=read_task,args=(q,))
# 启动进程
write_process1.start()
write_process2.start()
read_process.start()
# 等待写入结束
write_process1.join()
write_process2.join()
# 由于read进程是一个死循环所以要用terminate进行终止
read_process.terminate()
Pipe
Pipe
通信是用来进行两个进程之间的通信,两个进程位于管道的两端,Pipe
方法返回(conn1,conn2)
代表一个管道的两个端,
Pipe
方法的参数:
duplex
:如果为True,那么这个管道是全双工模式,也就是conn1和conn2都可以收发.若为False则,conn1只负责接收,conn2只负责发送,
send'
方法:发送数据
recv
方法:接收数据
如果没有消息可以接收,那么recv方法会一直阻塞,如果管道已经被关闭,recv会抛出EOFError
from multiprocessing import Process,Pipe
import os, time, random
# 通过创建一个Pipe实例,得到管道两端:p = Pipe()->(conn1,conn2)
# 通过在子进程任务中传入Pipe实例中的管道一端,这里要注意,没有设定两端均可以收发时(也就是duplex=False)
# 这时p[0],用于接收数据,将p[0]传入接收数据的进程,将p[1]传入发送数据的进程,
# 在发送数据的进程中使用 p.send(数据内容)进行数据发送
# 在接收数据进程中使用p.recv()进行数据接收
# 这里有一个非常奇怪的事情,有可能会出现发送了数据但没有接收的现象,暂时还不清楚触发的原因
def send_task(pipe,urls):
for url in urls:
print("Process %s send %s" %(os.getpid(),url))
pipe.send(url)
time.sleep(random.random())
def recv_task(pipe):
while True:
print("Process %s rev:%s" %(os.getpid(),pipe.recv()))
time.sleep(random.random())
if __name__ == "__main__":
pipe = Pipe(duplex=True)#这里返回的是一个包含管道两端的元组,所以在后面传入pipe时要指定索引,
send_process = Process(target=send_task,args=(pipe[0],["url1","url2","url3"]))
recv_process = Process(target=recv_task,args=(pipe[1],))
send_process.start()
recv_process.start()
send_process.join()
recv_process.terminate()
**异常**
线程
线程是进程中执行代码的一个分支,线程要进行工作需要cpu进行调度,线程是cup调度的基本单位.
比如一个进程中有两个函数A,B 这时就可以通过线程去同时执行,
使用线程:
线程之间执行是无序的
主线程 会等子线程结束以后再结束
可以通过设置deamon=True
使子线程守护主线程,当主线程退出子线程直接销毁.
线程之间共享全局变量
线程之间共享全局变量可能会出现错误
线程同步
- 线程等待(join())方法
g_num = 0#不可修改的变量,不能在原有内存上修改
def task():
for i in range(1000000):
#每循环一次给全局变量加1
global g_num
g_num += 1
print("task1",g_num)\
def task2():
for i in range(1000000):
#每循环一次给全局变量加1
global g_num
g_num += 1
print("task2",g_num)
if __name__ == "__main__":
fi_thread = threading.Thread(target=task)
se_thread = threading.Thread(target=task2)
fi_thread.start()
# 线程等待
fi_thread.join()# 主线程等待子线程结束后再执行
se_thread.start()
- 互斥锁
对共享数据锁定,包获赠同一时刻只有一个线程操作,多个线程一起抢,抢到锁的先执行,没有抢到的要等待.threading
中有一个Lock
,要上锁就需要对所有的都上锁,
# 1.创建锁
# 创建锁
mu = threading.Lock()
# 上锁
mu.acquire()
#释放锁
mu.release()
当一个线程中的所有代码执行完就会被销毁.
带来的问题:互斥锁和join都将原来的多任务改成了单任务,提高了数据的安全性但是降低了执行的效率.
死锁:
一直等待对方释放锁的场景就是死锁.
python全局解释器锁(GIL)
在python原始解释器中存在GIL,在解释执行python代码时会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到了I/O操作或者擦欧总次数达到了一定的数目才会释放GIL,由于GIL的存在,在进行多线程操作时不能调用多个CPU内核,只能利用一个内核,所以在进行CPU密集型操作时,不推荐使用多线程,更推荐使用多进程.多线程多用于I/O密集型操作,如网络爬虫开发,大多数时间是在等待socket返回数据,网络IO的操作延时比CUP大得多.
协程
协程也叫微线程,是一种轻量级用户级线程,写成拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来时候,恢复先前保存的寄存器上下文和栈,因此协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态.
在并发编程中,协程与线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据,和其他资源,
协程需要用户自己编写调度逻辑,对于CPU来讲,协程其实是单线程,CUP不需要考虑如何调度切换,省去了CUP切换开销,在一定程度上好于多线程,
python中yield
提供了协程的基本支持,通常gevent
库更好.
gevent库有以下几个特点:
- 基于libev的快速时间循环,Linux上是epoll机制
- 基于greenlet的轻量级执行单元
- API复用了python标准库内容
- 支持SSL的协作式sockets
- 可以通过线程池或c-ares实现DNS查询
- 通过monkey patching功能使第三方某块编程协作式
gevent对协程的支持本质上是greenlet在实现切换工作,
greenlet的工作流程
- 假设访问网络的IO操作时出现了阻塞,
- 显式切换到另一段没有被阻塞的代码段执行
- 直到原阻塞状态消失->回到原片段继续处理
通过gevent自动切换线程可以保证总是有greenlet在运行而不是等待IO,因此效率较高.
由于切换是在IO操作时自动完成,所以gevent需要修稿一些python的标准库,将一些常见的阻塞(sockets,select)等地方实现协程跳转.
分布式进程
将进程分布到多台电脑上,通过multiprocessing中的managers子模块主要问题时将Queue暴露到网络中,让其他的机器进程都可以访问,分布式进程就是对此进行了封装,这个过程也是本地队列的网络化.
创建分布式进程的步骤:
- 建立队列Queue,用来进程之间的通信.服务进程创建队列任务task_queue,用作为传递任务进程的通道;服务进程创建结果队列result_queue,作为任务进程完成任务后回复服务进程的通道.在分布式多进程环境下,必须通过由Queuemanager获得的Queue接口来添加任务.
- 把第一步创建的队列在网上注册,暴露给其他主机,注册后获得网络队列,相当于本地队列的映像.
- 建立一个对象(Queuemanager)实例manager,绑定端口和验证口令,
- 启动第三步的实例,监管信息通道
- 通过管理实例方法获得通过网络访问的queue对象,即再把网络队列实体化成可以使用的本地队列
- 创建任务到本地队列中,自动上传任务到网络队列,分配给任务进程进行处理.
# coding:utf-8
# taskmanager.py for windows
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support, Queue
# 任务个数
task_number = 10
# 定义收发队列
task_queue = Queue(task_number)
result_queue = Queue(task_number)
def get_task():
return task_queue
def get_result():
return result_queue
# 创建queuemanager,继承Basemanager
class Queuemanager(BaseManager):
pass
def win_run():
# win下绑定接口不能呢个使用lambda,所以要先定义函数再绑定,
Queuemanager.register('get_task_queue',callable=get_task)
Queuemanager.register('get_result_queue',callable=get_result)
# 绑定端口设置验证口令,Windows下要写IP地址
manager = Queuemanager(address=('127.0.0.1', 8001), authkey=bytes('test', encoding='utf-8'))
# 启动
manager.start()
try:
task = manager.get_task_queue()
result = manager.get_result_queue()
for url in ["Imageurl_"+str(i) for i in range(10)]:
print("put task %s ..." % url)
task.put(url)
print("try get result...")
for i in range(10):
print("result is %s" % result.get(timeout=10))
except:
print("error")
finally:
# 一定要关闭,否则会报错
manager.shutdown()
if __name__=="__main__":
freeze_support()
win_run()
taskworker.py
import time
from multiprocessing.managers import BaseManager
class Queuemanager(BaseManager):
pass
Queuemanager.register('get_task_queue')
Queuemanager.register('get_result_queue')
# 连接到服务器
server_addr = '127.0.0.1'
print("connect to server %s..." % server_addr)
# 端口与验证口令要和服务进程保持一致
m = Queuemanager(address=(server_addr, 8001), authkey=bytes('test', encoding='utf-8'))
# 从网络连接
m.connect()
# 获取queue对象
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列获取任务,并将结果写入result队列:
while(not task.empty()):
img_url = task.get(True, timeout = 5)
print("run task download %s ..." % img_url)
time.sleep(1)
result.put("%s --> success" % img_url)
print('worker exit')
备注127.0.0.1
是本机地址127.0.0.1的百科