Python开发【第九篇】: 并发编程

内容概要

  • 操作系统介绍
  • 进程
  • 线程
  • 协程

 

二. 进程

python并发编程之多进程理论部分

 

在python程序中的进程操作

   运行中的程序就是一个进程。所有的进程都是通过它的父进程来创建的。因此,运行起来的python程序也是一个进程,那么我们也可以在程序中再创建子进程。多个进程可以实现并发效果,也就是说,当我们的程序中存在多个进程的时候,在某些时候,就会让程序的执行速度变快。创建进程这个功能需要借助python中强大的模块。

 

multiprocess模块

    multiprocess不是一个模块而是python中一个操作、管理进程的包。 这个包中几乎包含了和进程有关的所有子模块。大致分为四个部分:创建进程部分,进程同步部分,进程池部分,进程之间数据共享。

 

multiprocess.process模块

process模块介绍

  process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。

class Process(object):
    def __init__(self, group=None, target=None, name=None, args=(), kwargs={}):
        self.name = ''
        self.daemon = False
        self.authkey = None
        self.exitcode = None
        self.ident = 0
        self.pid = 0
        self.sentinel = None


1. 需要使用关键字的方式来指定参数
2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

参数介绍:
1 group   参数未使用,值始终为None
2 target  表示调用对象,即子进程要执行的任务
3 args    表示调用对象的位置参数元组,args=(1,2,'egon',)
4 kwargs  表示调用对象的字典,kwargs={'name':'egon','age':18}
5 name    为子进程的名称

 

1 p.start():启动进程,并调用该子进程中的p.run() 
2 p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
3 p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
4 p.is_alive():如果p仍然运行,返回True
5 p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程  
方法介绍
1 p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
2 p.name:进程的名称
3 p.pid:进程的pid
4 p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
5 p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
属性介绍
在Windows操作系统中由于没有fork(linux操作系统中创建进程的机制),在创建子进程的时候会自动 import 启动它的这个文件,而在 import 的时候又执行了整个文件。因此如果将process()直接写在文件中就会无限递归创建子进程报错。所以必须把创建子进程的部分使用if __name__ ==‘__main__’ 判断保护起来,import 的时候  ,就不会递归运行了。
在Windows中使用process模块注意事项

 

 

使用process模块创建进程

在一个python进程中开启子进程,start方法和并发效果。

from multiprocessing import Process

def f(name):
    print("hello",name)
    print("我是子进程")

if __name__ == '__main__':
    p = Process(target=f, args=("lsc",))
    p.start()
    print("执行主进程的内容")

# 执行结果:
# 执行主进程的内容
# hello lsc
# 我是子进程
在python中启动一个子进程
from multiprocessing import Process

def f(name):
    print("hello",name)
    print("我是子进程")

if __name__ == '__main__':
    p = Process(target=f, args=("lsc",))
    p.start()   # 启动子进程
    p.join()    # 主进程阻塞,等待子进程执行完毕
    print("执行主进程的内容")


# 执行结果:
# hello lsc
# 我是子进程
# 执行主进程的内容
join 方法
import os
from multiprocessing import Process

def f(x):
    print("子进程id :",os.getpid(),"父进程id :",os.getppid())




if __name__ == '__main__':
    print("主进程id :", os.getpid())
    p_lst = []
    for i in range(10):
        p = Process(target=f,args=(i,))
        p.start()

# 执行结果:
# 主进程id : 13192
# 子进程id : 8748 父进程id : 13192
# 子进程id : 13988 父进程id : 13192
# 子进程id : 7932 父进程id : 13192
# 子进程id : 11324 父进程id : 13192
# 子进程id : 9120 父进程id : 13192
# 子进程id : 12664 父进程id : 13192
# 子进程id : 11464 父进程id : 13192
# 子进程id : 7424 父进程id : 13192
# 子进程id : 12120 父进程id : 13192
# 子进程id : 12016 父进程id : 13192
查看主进程和子进程的进程号

 

多个进程同时运行(注意,子进程的执行顺序不是根据启动顺序决定的)

import time
from multiprocessing import Process

def f(name,num):
    print("hello",name,num)
    time.sleep(1)
    
if __name__ == '__main__':
    p_lst = []   # 存放子进程对象
    for i in range(5):
        p = Process(target=f,args=("lsc",i))
        p.start()
        p_lst.append(p)
多个进程同时运行
import time
from multiprocessing import Process

def f(name,num):
    print('hello', name,num)
    time.sleep(1)

if __name__ == '__main__':
    p_lst = []
    for i in range(5):
        p = Process(target=f, args=('lsc',i))
        p.start()
        p_lst.append(p)
        p.join()          # 在这里使用join的效果: 串行执行子进程
    print('父进程在执行')

# 执行结果:
# hello lsc 0
# hello lsc 1
# hello lsc 2
# hello lsc 3
# hello lsc 4
# 父进程在执行
多个进程同时运行时,jsin方法的使用
import time
from multiprocessing import Process

def f(name,num):
    print('hello', name,num)
    time.sleep(1)

if __name__ == '__main__':
    p_lst = []
    for i in range(5):
        p = Process(target=f, args=('lsc',i))
        p.start()
        p_lst.append(p)
    for p in p_lst:p.join()   # 所有子进程并行执行
    print('父进程在执行')   # 父进程等待所有子进程执行结束

# 执行结果:
# hello lsc 1
# hello lsc 0
# hello lsc 2
# hello lsc 3
# hello lsc 4
# 父进程在执行
多个进程同时运行时,jsin方法的使用
import os
from multiprocessing import Process

class MyProcess(Process):
    def __init__(self,name):
        super().__init__()
        self.name = name

    def run(self):
        print("子进程: " ,os.getpid())
        print("父进程: ", os.getppid())
        print("我的名字是 %s" %self.name)

if __name__ == '__main__':
    p1 = MyProcess("lsc")
    p2 = MyProcess("ym")

    p1.start()
    p2.start()
    print("主进程: " ,os.getpid())
继承Process类的形式开启进程的方式

 

进程之间的数据隔离问题

import os
import time
from multiprocessing import Process
n = 0
def add():
    global n
    n += 1

if __name__ == '__main__':
    p_l = []
    for i in range(100):
        p = Process(target=add)
        p.start()   # 异步
        p_l.append(p)
    for p in p_l:p.join()
    print(n)

# 执行结果为 0,因为进程之间的数据是隔离的。
View Code

 

 

 

守护进程

会随着主进程的结束而结束。

主进程创建守护进程

  其一:守护进程会在主进程代码执行结束后就终止

  其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止

守护进程的特点 :随着主进程的代码结束而结束

import time
from multiprocessing import Process
def is_alive():
    while 1:
        time.sleep(1)
        print("is alive")

def main():
    print('主进程中的内容')
    time.sleep(0.5)
    print('主进程中的内容')
    time.sleep(0.5)
    print('主进程中的内容')
    time.sleep(0.5)
    print('主进程中的内容')
    time.sleep(0.5)
    print('主进程中的内容')

if __name__ == '__main__':
    p = Process(target=is_alive)
    p.daemon = True # 设置子进程为一个守护进程
    p.start()
    main()
    print("主进程结束~~")
View Code

正常的情况下
主进程会等待所有的子进程结束之后才结束,主进程要回收资源。
import time
from multiprocessing import Process

def is_alive():
    for i in range(10):
        time.sleep(0.5)
        print('is alive')

def son2():
    time.sleep(2)
    print('in son2')
    time.sleep(2)
    print('in son2')


def main():
    print('主进程中的内容')
    time.sleep(0.5)
    print('主进程中的内容')
    time.sleep(0.5)



if __name__ == '__main__':
    p = Process(target=is_alive)
    p.daemon = True  # 设置子进程为一个守护进程
    p.start()
    p1 = Process(target=son2)
    p1.start()
    main()
    p1.join()   # 主进程的最后一句代码
View Code

 

 

进程同步(multiprocess.Lock)

锁 —— multiprocess.Lock

  程序的异步执行,让多个任务可以同时在几个进程中并发处理,他们之间的运行没有顺序,一旦开启也不受我们控制。尽管并发编程能更加充分的利用IO资源,但是也带来了新的问题。

  当多个进程使用同一份数据资源的时候,就会引发数据安全或顺序混乱问题。

import time
import json
from multiprocessing import Process

def check_ticket(name):
    with open("db.json") as f:
        tickit_dic = json.load(f)
        count = tickit_dic["count"]
        print("%s 查询余票 %s" %(name,count))
    return count

def buy_ticket(name):
    tickit_dic = json.load(open("db.json"))
    time.sleep(0.01)    # 模拟读数据库时的网络延迟
    if tickit_dic["count"] >= 1:
        tickit_dic["count"] -=1
        print("%s 购买成功." %name)
        time.sleep(0.02)  # 模拟写数据时的网络延迟
        with open("db.json","w") as f:
            json.dump(tickit_dic,f)
    else:
        print("%s 没有余票了" %name)

def main(name):
    check_ticket(name)  # 查看票
    buy_ticket(name)    # 买票

if __name__ == '__main__':
    for i in range(5):   # 模拟并发, 5个人客户端买票
        p = Process(target=main,args=("lsc %s" %i,))
        p.start()

# 执行结果:
# lsc 1 查询余票 1
# lsc 1 购买成功.
# lsc 0 查询余票 1
# lsc 2 查询余票 1
# lsc 0 购买成功.
# lsc 2 购买成功.
# lsc 3 查询余票 0
# lsc 3 没有余票了
# lsc 4 查询余票 0
# lsc 4 没有余票了
# 多进程中产生了一个数据安全问题, 余票只有一张,但是三个人买到了票
模拟过年抢火车票 
import time
import json
from multiprocessing import Process,Lock

def check_ticket(name):
    with open("db.json") as f:
        tickit_dic = json.load(f)
        count = tickit_dic["count"]
        print("%s 查询余票 %s" %(name,count))
    return count

def buy_ticket(name):
    tickit_dic = json.load(open("db.json"))
    time.sleep(0.01)    # 模拟读数据库时的网络延迟
    if tickit_dic["count"] >= 1:
        tickit_dic["count"] -=1
        print("%s 购买成功." %name)
        time.sleep(0.02)  # 模拟写数据时的网络延迟
        with open("db.json","w") as f:
            json.dump(tickit_dic,f)
    else:
        print("%s 没有余票了" %name)

def main(name,lock):
    check_ticket(name)  # 查看票
    # lock.acquire()   # 锁定
    # buy_ticket(name)    # 买票
    # lock.release()   # 解锁

    # 也可以用 with
    with lock:
        buy_ticket(name)

if __name__ == '__main__':
    lock = Lock()  # 拿到一把锁
    for i in range(5):   # 模拟并发, 5个人客户端买票
        p = Process(target=main,args=("lsc %s" %i,lock))
        p.start()

# 执行结果:
# lsc 0 查询余票 1
# lsc 1 查询余票 1
# lsc 0 购买成功.
# lsc 2 查询余票 1
# lsc 3 查询余票 0
# lsc 1 没有余票了
# lsc 2 没有余票了
# lsc 3 没有余票了
# lsc 4 查询余票 0
# lsc 4 没有余票了
# 用锁 实际上降低了程序的执行效率,从原本的同时执行某段代码 变成几个进程顺序的执行 拖慢了速度 提高了数据的安全性
使用锁来保证数据安全
#加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理

#因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。
队列和管道都是将数据存放于内存中
队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,
我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可扩展性。 

 

 

进程间通信——队列(multiprocess.Queue)

进程间通信

IPC (Inter-Process Communication)

 

队列 

创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。 

Queue([maxsize]) 
创建共享的进程队列。
参数 :maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。
底层队列使用管道和锁定实现。
Queue([maxsize]) 
创建共享的进程队列。maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。 
Queue的实例q具有以下方法:

q.get( [ block [ ,timeout ] ] ) 
返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。

q.get_nowait( ) 
同q.get(False)方法。

q.put(item [, block [,timeout ] ] ) 
将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。

q.qsize() 
返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。


q.empty() 
如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。

q.full() 
如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。。
方法介绍

 

代码示例:

from multiprocessing import Queue
q = Queue(3)  # 表示只放3个消息
# qut que_nowait
# gut gut_nowait
# full:队列满了 empty:队列空了
q.put(1)
q.put(1)
q.put(1)

# q.put(1)   # 如果队列已经满了,程序就会停在这里,等待数据被别人取走,再将数据放入队列。
           # 如果队列中的数据一直不被取走,程序就会永远停在这里。

try:
    q.put_nowait(3) # 可以使用put_nowait,如果队列满了不会阻塞,但是会因为队列满了而报错。queue.Full
except: # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去,但是会丢掉这个消息。
    print('队列已经满了')
print(q.full()) # 队列满了返回: True

print(q.get())
print(q.get())
print(q.get())
# print(q.get()) # 同put方法一样,如果队列已经空了,那么继续取就会出现阻塞。
try:
    q.get_nowait(3) # 可以使用get_nowait,如果队列满了不会阻塞,但是会因为没取到值而报错。queue.Empty
except:     # 因此我们可以用一个try语句来处理这个错误。这样程序不会一直阻塞下去。
    print('队列已经空了')

print(q.empty()) #  队列空了返回: True
队列使用

 

进程间通信

from multiprocessing import Process,Queue

def consumer(q):
    """ 消费者 """
    print("消费数据",q.get())

def producer(q):
    """ 生产者 """
    q.put({1,2,3})


if __name__ == '__main__':
    q = Queue()   # 实例化队列
    Process(target=consumer,args=(q,)).start()
    Process(target=producer,args=(q,)).start()

# 执行结果:
# 消费数据 {1, 2, 3}
进程之间通信
import time
from multiprocessing import Process,Queue

def f(q):
    time.sleep(1)
    q.put({1,2,3})  #调用主函数中p进程传递过来的进程参数 put函数向队列中添加一条数据。

if __name__ == '__main__':
    q = Queue() # 创建一个Queue对象
    p = Process(target=f,args=(q,))
    p.start()
    print("父进程获取数据:",q.get())  # 从队列中取数据
子进程发送数据给父进程

 

生产者消费者模型  

  在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。

 

为什么要使用生产者和消费者模式  

  在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。

 

什么是生产者消费者模式  

  生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

 

基于队列实现生产者消费者模型
import time
import random
from multiprocessing import Process,Queue

def consumer(name,q):
    while True:
        food = q.get()
        time.sleep(random.uniform(1,2))
        print("%s 吃了 %s" %(name,food))

def prodeucer(n,q,food):
    for i in range(n):
        time.sleep(random.random())
        q.put("%s%s"%(food,i))
        print("生产了 %s%s" %(food,i))

if __name__ == '__main__':
    q = Queue()
    # 生产者
    p1 = Process(target=consumer,args=("lsc",q))
    # 消费者
    p2 = Process(target=prodeucer,args=(10,q,"苹果"))

    # 启动子进程
    p1.start()
    p2.start()
    print("父进程")
基于队列实现生产者消费者模型

此时的问题是主进程永远不会结束,原因是:生产者p在生产完后就结束了,但是消费者c在取空了q之后,则一直处于死循环中且卡在q.get()这一步。

 

解决方式无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环。

import time
import random
from multiprocessing import Process,Queue

def consumer(name,q):
    while True:
        food = q.get()
        if food == None:break   # 取到None数据后,结束循坏
        time.sleep(random.uniform(1,2))
        print("%s 吃了 %s" %(name,food))

def prodeucer(n,q,food):
    for i in range(n):
        time.sleep(random.random())
        q.put("%s%s"%(food,i))
        print("生产了 %s%s" %(food,i))
    else:q.put(None)   # 循坏结束后,发送结束信号

if __name__ == '__main__':
    q = Queue()
    # 生产者
    p1 = Process(target=consumer,args=("lsc",q))
    # 消费者
    p2 = Process(target=prodeucer,args=(10,q,"苹果"))

    # 启动子进程
    p1.start()
    p2.start()
    print("父进程")
View Code

 

多个生产者和多个消费者时

import time
import random
from multiprocessing import Process,Queue

def consumer(name,q):
    while True:
        food = q.get()
        if food == None:break   # 取到None数据后,结束循坏
        time.sleep(random.uniform(1,2))
        print("%s 吃了 %s" %(name,food))

def prodeucer(n,q,food):
    for i in range(n):
        time.sleep(random.random())
        q.put("%s%s"%(food,i))
        print("生产了 %s%s" %(food,i))


if __name__ == '__main__':
    q = Queue()
    # 消费者
    c1 = Process(target=consumer,args=("lsc",q))
    c2 = Process(target=consumer,args=("ym",q))

    # 生产者
    p1 = Process(target=prodeucer,args=(10,q,"苹果"))
    p2 = Process(target=prodeucer,args=(10,q,"苹果"))

    # 启动子进程
    c1.start()
    c2.start()
    p1.start()
    p2.start()

    p1.join()
    p2.join()  #  #必须保证生产者全部生产完毕,才应该发送结束信号
    q.put(None)      #有几个消费者就应该发送几次结束信号None
    q.put(None)      #发送结束信号
    print("父进程")
多个消费者的例子:有几个消费者就需要发送几次结束信号

 

进程的总结

multiprocessing模块
Process类
  # 创建一个进程对象 Process(target = 函数,args = (参数,参数))
  # 开启进程 start()
  # 阻塞主进程 join()
  # deamon 设置守护进程 :等待主进程的代码结束之后就立即结束

锁 lo = Lock()
  # with lo:
  # 多个进程处理同一个资源 (文件 、数据库中的数据)
  # 必须对进程中的代码加锁 来保证数据的安全
  # 锁的特点 :在代码的任意一个位置 都可以引入锁

IPC 进程之间的通信(管道pipe 队列queue)
  # 队列是 进程之间数据安全的
  # 生产者消费者模型 - 基于队列完成


了解进程的特点 :数据隔离 能利用多核 使用的开销大

 

 

 

 

三. 线程

并发编程理论部分:

  http://www.cnblogs.com/linhaifeng/articles/6817679.html

  http://www.cnblogs.com/Eva-J/articles/8306047.html

 

线程和python

全局解释器锁GIL

在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
GIL锁 是为了解决解释型语言 在多线程中出现的数据不安全的情况 而统一在Cpython解释器中 加上的一把线程锁
理论: https://www.cnblogs.com/linhaifeng/articles/7449853.html

Theading模块

multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性

 

线程的创建

import os
import time
from threading import Thread

def func(n):
    print("start thread %s" %n, "进程id",os.getpid())
    time.sleep(1)
    print("end thread %s" %n)

print("--->",os.getpid())
for i in range(20):
    t = Thread(target=func,args=(i,))   # 开启线程
    t.start()
线程的创建Threading.Thread类

 

多线程和多进程对比

1. 进程id
对于线程来说 进程id都是相同的
from threading import Thread
from multiprocessing import Process
import os

def work(name):
    print("我是 %s" % name,"PID:",os.getpid())

if __name__ == '__main__':
    for i in range(5):
        Thread(target=work,args=("线程",)).start()
        Process(target=work,args=("进程",)).start()
    print("主程序PID:",os.getpid())
进程id比较
2.执行效率
多线程的开销远远小于多进程
import time,os
from threading import Thread
from multiprocessing import Process
def func(n):
    print('start thread %s' % n, os.getpid())
    time.sleep(1)
    print('end thread %s' % n)

if __name__ == '__main__':
    start1 = time.time()
    t_l = []
    for i in range(50):
        t = Thread(target=func, args=(i,))  # 开启线程
        t.start()
        t_l.append(t)
    for t in t_l:t.join()
    end1 = time.time()

    start2 = time.time()
    p_l = []
    for i in range(50):
        p = Process(target=func, args=(i,))   # 开启进程
        p.start()
        p_l.append(p)
    for p in p_l:p.join()
    end2 = time.time()

    print(end1 - start1)
    print(end2 - start2)
线程开销小

  3. 线程数据共享

import time
from threading import Thread

n = 0
def func():
    global n
    n +=1

t_l = []
for i in range(50):
    t = Thread(target=func)
    t.start()
    t_l.append(t)
for t in t_l:t.join() # 等待所有线程执行完毕
print(n)
执行结果: 50
线程共享数据

练习 :多线程实现socket
import socket
import time
import os
from threading import Thread

def func2():
    time.sleep(1)
    print("3",os.getpid())

def talk(conn,addr):
    while 1:
        t = Thread(target=func2)
        t.start()
        msg = conn.recv(1024).decode("utf-8")
        msg_up = msg.upper()
        conn.send(msg_up.encode("utf-8"))
        print("2",os.getpid())

sk = socket.socket()
sk.bind(("127.0.0.1",9000))
sk.listen()

# 主程序死循环,来一个tcp连接就会开一个线程
while 1:
    conn,addr = sk.accept()
    t = Thread(target=talk,args=(conn,addr))
    t.start()
    print("1",os.getpid())
server
import socket
sk = socket.socket()
sk.connect(("127.0.0.1",9000))
while 1:
    msg = input(">>>")
    sk.send(msg.encode("utf-8"))
    server_msg = sk.recv(1024).decode("utf-8")
    print(server_msg)

sk.close()
client

 

守护线程

# 守护线程什么时候执行结束???
    # 守护线程 会等待所有的子线程结束之后才结束
    # 守护进程 是主进程代码结束就结束了 不会主动守护子进程

# 守护进程是怎么结束的?   守护进程只和主进程代码有关系
    # 主进程代码执行完 主进程没结束 守护进程先结束
    # 主进程回收守护进程的资源

# 守护线程到底是怎么结束的  守护线程永远是这个进程中最后结束的线程
    # 主线程 会等待所有的非守护线程结束
    # 主线程才结束
    # 主进程结束了
    # 守护线程才结束
import time
from threading import Thread

def deamon_t():
    while 1:
        time.sleep(1)
        print("is alive")

def thread_2():
    print("start t2")
    time.sleep(5)
    print("end t2")

def main():
    print("main start")
    time.sleep(1.5)
    print("main end")

t1 = Thread(target=deamon_t)
t2 = Thread(target=thread_2)
# 开启守护线程
t1.daemon = True
t1.start()
t2.start()
main()

# 执行结果:
# start t2
# main start
# is alive
# main end     --> 主线程已经执行结束了
# is alive
# is alive    --> 守护线程还在执行
# is alive
# is alive     --> 守护线程永远是这个进程中最后结束的线程
# end t2
守护线程

 

 

Python标准模块--concurrent.futures

#1 介绍
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.

#2 基本方法
#submit(fn, *args, **kwargs)
异步提交任务

#map(func, *iterables, timeout=None, chunksize=1) 
取代for循环submit的操作

#shutdown(wait=True) 
相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前

#result(timeout=None)
取得结果

#add_done_callback(fn)
回调函数

# done()
判断某一个线程是否完成

# cancle()
取消某个任务
用法介绍
import time
import random
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def func(n,name):
    print("%s in son thread %s" %(name,n))
    time.sleep(random.random())
    print("%s end %s" %(name,n))

if __name__ == '__main__':
    tp = ProcessPoolExecutor(10)  # 只需要换这个类就行了,线程和进程可以随意切换

    name = "lsc"
    for i in range(10):
        tp.submit(func,i,name)  # 一个参数是函数, 后面都是参数

    tp.shutdown()  # 直接join整个线程池中的任务
    print("main")
进程池
import time
import random
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def func(n,name):
    print("%s in son thread %s" %(name,n))
    time.sleep(random.random())
    print("%s end %s" %(name,n))

tp = ThreadPoolExecutor(10)  # 线程池,一共有10个线程

name = "lsc"
for i in range(10):  # 有10个人,同时只能有4个线程执行
    tp.submit(func,i,name)  # 提交任务 开启执行,第一个参数是函数, 后面都是参数

tp.shutdown()  # 直接join整个线程池中的任务
print("main")
线程池
 建议开进程和线程的数量
    cpu个数*1 -cpu个数*2个进程
    cpu个数*5个线程

 例如4核CPU
     5个进程 20条线程 100的并发
     一条线程 再开500个协程
    100 * 500 = 50000
import time
import random
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def func(n,name):
    print("%s in son thread %s" %(name,n))
    time.sleep(random.randint(1,4))
    print("%s end %s" %(name,n))
    return n * "*"

if __name__ == '__main__':
    tp = ThreadPoolExecutor(5)  # 换成ProcessPoolExecutor也是可以获取到返回值
    task_l = []
    for i in range(20):
        task_obj = tp.submit(func,i,"lsc") # 接收 func 函数的返回值
        task_l.append(task_obj)     # 将返回的结果对象追加到列表中
    for task in task_l:
        # print(task)   # <Future at 0x294e7f0 state=finished returned str>
        print(task.result())    # 打印返回值的结果
    print("main")






简化操作 tp.map()
import time
import random
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def func(n):
    print("in son thread %s" %(n))
    time.sleep(random.randint(1,4))
    print("end %s" %(n))
    return n * "*"

if __name__ == '__main__':
    tp = ThreadPoolExecutor(5)  # 换成ProcessPoolExecutor也是可以获取到返回值
    ret = tp.map(func,range(20))   # tp.map,给func只能传一个参数
    for i in ret:print(i)
    print("main")
获取返回值
from urllib.request import urlopen
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def parser_page(ret):
    dic = ret.result()   # 拿到 get_html 函数 return 的数据
    # 拿到数据就可以进行处理了
    print("开始处理...")
    print(dic["url"],len(dic["html"]))

def get_html(url):
    ret = urlopen(url)  # 打开网页
    html = ret.read()   # 读取网页内容
    return {"url":url,"html":html}  # 返回一个字典

tp = ThreadPoolExecutor(4)  # 线程池,开启4个线程

for url in ['https://www.cnblogs.com/Eva-J/articles/9374538.html','http://www.sogou.com','http://www.JD.com','http://www.qq.com','http://www.baidu.com']:
    task_obj = tp.submit(get_html,url)      # 执行get_html函数,获取数据. task_obj 接收返回值
    task_obj.add_done_callback(parser_page)     # 回调函数,调用parser_page函数
回调方法
需要回调函数的场景:进程池中任何一个任务一旦处理完了,就立即告知主进程:我好了额,你可以处理我的结果了。主进程则调用一个函数去处理该结果,该函数即回调函数

我们可以把耗时间(阻塞)的任务放到进程池中,然后指定回调函数(主进程负责执行),这样主进程在执行回调函数时就省去了I/O的过程,直接拿到的是任务的结果。

 

 

四. 协程

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。、

需要强调的是:

#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

对比操作系统控制线程的切换,用户在单线程内控制协程的切换

优点如下:

#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu

缺点如下:

#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
#2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

总结协程特点:

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

Gevent模块

安装:pip3 install gevent

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的

g2=gevent.spawn(func2)

g1.join() #等待g1结束

g2.join() #等待g2结束

#或者上述两步合作一步:gevent.joinall([g1,g2])

g1.value#拿到func1的返回值
用法介绍
import gevent
def eat(name):
    print('%s eat 1' %name)
    gevent.sleep(2)
    print('%s eat 2' %name)

def play(name):
    print('%s play 1' %name)
    gevent.sleep(1)
    print('%s play 2' %name)


g1=gevent.spawn(eat,'egon')
g2=gevent.spawn(play,name='egon')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('')
遇到IO主动切换

上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头

 

Gevent之同步与异步

from gevent import spawn,joinall,monkey;monkey.patch_all()

import time
def task(pid):
    """
    Some non-deterministic task
    """
    time.sleep(0.5)
    print('Task %s done' % pid)


def synchronous():  # 同步
    for i in range(10):
        task(i)

def asynchronous(): # 异步
    g_l=[spawn(task,i) for i in range(10)]
    joinall(g_l)
    print('DONE')
    
if __name__ == '__main__':
    print('Synchronous:')
    synchronous()
    print('Asynchronous:')
    asynchronous()
#  上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn。
#  初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,
#  后者阻塞当前流程,并执行所有给定的greenlet任务。执行流程只会在 所有greenlet执行完后才会继续向下走。

 

 

 

Gevent之应用举例一

通过gevent实现单线程下的socket并发

注意 :from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞

from gevent import monkey;monkey.patch_all()
import socket
import gevent


def talk(conn,addr):
    while 1:
        msg = conn.recv(1024)
        print(addr,msg)
        conn.send(b"received")



sk = socket.socket()
sk.bind(('127.0.0.1', 9000))
sk.listen()

while 1:
    conn,addr = sk.accept()
    # 将socket连接扔给协程去接收
    gevent.spawn(talk,conn,addr)    # spawn(cls, *args, **kwargs) 第一个参数是函数,后面都是参数
server
import socket
from threading import Thread
def client():
    sk = socket.socket()   #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了
    sk.connect(('127.0.0.1',9000))
    while True:
        sk.send(b'yuan')
        print(sk.recv(1024))
    
for i in range(500):
    Thread(target=client).start()
    


# 进程 + 协程
# 进程 + 线程 + 协程
    # 5进程 20线程 500个协程 = 50000
client

 使用 pstree 查看进程树,在操作系统看来,只有一个线程

 

Gevent之应用举例二

from gevent import monkey;monkey.patch_all()
import time
import gevent
from urllib import request

def get_url(url,filename):
    res = request.urlopen(url)
    with open(filename,'wb') as f:
        f.write(res.read())


url_list = [
    ('http://www.baidu.com','baidu'),
    ('http://www.cnblogs.com/Eva-J/articles/8306047.html','cnblog1'),
    ('http://www.sogou.com','sogou'),
    ('http://www.douban.com','douban'),
    ('http://www.JD.com','jd')
]

# 顺序执行
start = time.time()
for url,name in url_list:
    get_url(url,name+'.html')
print(time.time() - start)
# 执行时间: 76.38136887550354


# 开启协程
start = time.time()
g_l = []
for url,name in url_list:
    g = gevent.spawn(get_url,url,name)
    g_l.append(g)
gevent.joinall(g_l)
print(time.time() - start)
# 执行时间: 19.80513286590576
爬虫

 






posted @ 2019-03-11 17:00  LiShiChao  阅读(229)  评论(0编辑  收藏  举报