python多任务处理

进程

定义:一个正在运行的程序或者软件就是一个进程,它是操作系统进行资源分配的基本单位,每一个进程都会消耗一定的内存资源。
进程主要是负责索要内存资源,而实际上干活的是线程
一个程序至少有一个进程,一个进程默认有多个线程
没有进程就没有线程。
多进程可以完成多个任务,比如可以同时使用qq聊天和QQ音乐听歌,
在这里插入图片描述

多进程的使用

  1. multiprocessing 通过这个包中的Process进程类实现
    Process(group,target,)

手动创建进程管理,使用pocess模块
使用步骤:

  1. 创建要进行的程序
  2. 创建子进程对象
    p = Process(target=run_proc, args=(1,)
    p1 = Process(target)
    3.启动进程,子进程的执行是无序的,
    p.start()
    p1.start()
  3. 等待子进程执行结束join()

获取进程编号os.getpid() os.getppid()

如果在一个函数中获取编号,而这个函数交给了一个子进程执行那么os.getpid()获取的是子进程的编号,也就是放在哪里就是获取哪里的进程编号,
要获取父进程的编号就使用os.getppid()
获取当前进程对象multiprocessing.current_process()
根据进程编号杀死进程:os.kill(进程编号)

执行需要传递参数的进程

在创建进程时将参数传入Process(target=,args=,kwargs=)
要注意args是通过元组传递参数,传递要严格按照顺序kwargs是通过字典进行传参,传递时候通过关键字;两种方法可以同时使用,先使用元组将前面的元素参数传递完成再使用字典传递剩余参数.
注意!

  1. 进程之间不共享全局变量,对于window实际上会进行拷贝,对于LInux和mac不会拷贝,这里要注意
    创建一个子进程就是对主进程资源的拷贝,子进程是主进程的一个副本,
    如何解决Windows中的问题:
    通过
if __name__ == "__mian__":
	#这里调用子进程,可以防止Windows递归调用拷贝创建子进程.
  1. 主进程会等待虽有的子进程执行结束再结束.

进程之间的通信问题

如果创建里大量的进程,进程之间往往需要通信,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的工作流程

  1. 假设访问网络的IO操作时出现了阻塞,
  2. 显式切换到另一段没有被阻塞的代码段执行
  3. 直到原阻塞状态消失->回到原片段继续处理

通过gevent自动切换线程可以保证总是有greenlet在运行而不是等待IO,因此效率较高.
由于切换是在IO操作时自动完成,所以gevent需要修稿一些python的标准库,将一些常见的阻塞(sockets,select)等地方实现协程跳转.

分布式进程

将进程分布到多台电脑上,通过multiprocessing中的managers子模块主要问题时将Queue暴露到网络中,让其他的机器进程都可以访问,分布式进程就是对此进行了封装,这个过程也是本地队列的网络化.
创建分布式进程的步骤:

  1. 建立队列Queue,用来进程之间的通信.服务进程创建队列任务task_queue,用作为传递任务进程的通道;服务进程创建结果队列result_queue,作为任务进程完成任务后回复服务进程的通道.在分布式多进程环境下,必须通过由Queuemanager获得的Queue接口来添加任务.
  2. 把第一步创建的队列在网上注册,暴露给其他主机,注册后获得网络队列,相当于本地队列的映像.
  3. 建立一个对象(Queuemanager)实例manager,绑定端口和验证口令,
  4. 启动第三步的实例,监管信息通道
  5. 通过管理实例方法获得通过网络访问的queue对象,即再把网络队列实体化成可以使用的本地队列
  6. 创建任务到本地队列中,自动上传任务到网络队列,分配给任务进程进行处理.

# 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的百科

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值