Python并发编程之多进程,多线程

基础概念

一、进程、程序和线程

  • 程序:程序只是一堆代码而已
  • 进程:指的是程序的运行过程,是对正在运行程序的一个抽象。进程是一个资源单位
  • 线程:每个进程有一个地址空间,而且默认就有一个控制线程。线程才是cpu上的执行单位

二、并发与并行

无论是并行还是并发,在用户看来都是'同时'运行的,不管是进程还是线程,都只是一个任务而已,真是干活的是cpu,cpu来做这些任务,而一个cpu同一时刻只能执行一个任务

  • 并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,(并行也属于并发)
  • 并行:同时运行,只有具备多个cpu才能实现并行

三、同步与异步

  • 同步:发出一个功能调用时,在没有得到结果之前,该调用就不会返回。
  • 异步:异步功能调用发出后,不会等返回,而是继续往下执行当,当该异步功能完成后,通过状态、通知或回调函数来通知调用者。

四、阻塞与非阻塞

  • 阻塞:调用结果返回之前,当前线程会被挂起(如遇到io操作)。函数只有在得到结果之后才会将阻塞的线程激活。
  • 非阻塞:在不能立刻得到结果之前也会立刻返回,同时该函数不会阻塞当前线程。

五、总结

  • 同步与异步针对的是函数/任务的调用方式:同步就是当一个进程发起一个函数(任务)调用的时候,一直等到函数(任务)完成,而进程继续处于激活状态。而异步情况下是当一个进程发起一个函数(任务)调用的时候,不会等函数返回,而是继续往下执行当,函数返回的时候通过状态、通知、事件等方式通知进程任务完成。
  • 阻塞与非阻塞针对的是进程或线程:阻塞是当请求不能满足的时候就将进程挂起,而非阻塞则不会阻塞当前进程

多进程

一、进程的状态

二、multiprocessing模块介绍

  • python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在python中大部分情况需要使用多进程。
  • multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。
  • 与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内。

三、Process类

1、方法介绍

  • p.start():启动进程,并调用该子进程中的p.run()
  • p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
  • p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
  • p.is_alive():如果p仍然运行,返回True
  • p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程

2、属性介绍

  • p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
  • p.name:进程的名称
  • p.pid:进程的pid

3、开启子进程的两种方式

注意:在windows中Process()必须放到# if __name__ == '__main__':下

1. 在UNIX中该系统调用是:fork,fork会创建一个与父进程一模一样的副本,二者有相同的存储映像、同样的环境字符串和同样的打开文件(在shell解释器进程中,执行一个命令就会创建一个子进程)

  2. 在windows中该系统调用是:CreateProcess,CreateProcess既处理进程的创建,也负责把正确的程序装入新进程。

  关于创建子进程,UNIX和windows

  1.相同的是:进程创建后,父进程和子进程有各自不同的地址空间(多道技术要求物理层面实现进程之间内存的隔离),任何一个进程的在其地址空间中的修改都不会影响到另外一个进程。

  2.不同的是:在UNIX中,子进程的初始地址空间是父进程的一个副本,提示:子进程和父进程是可以有只读的共享内存区的。但是对于windows系统来说,从一开始父进程与子进程的地址空间就是不同的。

在Windows操作系统中由于没有fork(linux操作系统中创建进程的机制),在创建子进程的时候会自动 import 启动它的这个文件,而在 import 的时候又执行了整个文件。因此如果将process()直接写在文件中就会无限递归创建子进程报错。所以必须把创建子进程的部分使用if __name__ ==‘__main__’ 判断保护起来,import 的时候  ,就不会递归运行了。
在windows中使用process模块的注意事项
#方式一:
from multiprocessing import Process
import time,os
 
def task(name):
    print('%s %s is running,parent id is <%s>' %(name,os.getpid(),os.getppid()))
    time.sleep(3)
    print('%s %s is running,parent id is <%s>' % (name, os.getpid(), os.getppid()))
 
if __name__ == '__main__':
    # Process(target=task,kwargs={'name':'子进程1'})
    p=Process(target=task,args=('子进程1',))
    p.start() #仅仅只是给操作系统发送了一个信号
 
    print('主进程', os.getpid(), os.getppid())
 
#方式二
from multiprocessing import Process
import time,os
 
class MyProcess(Process):
    def __init__(self,name):
        super().__init__()
        self.name=name
 
    def run(self):
        print('%s %s is running,parent id is <%s>' % (self.name, os.getpid(), os.getppid()))
        time.sleep(3)
        print('%s %s is running,parent id is <%s>' % (self.name, os.getpid(), os.getppid()))
 
if __name__ == '__main__':
    p=MyProcess('子进程1')
    p.start()
    print('主进程', os.getpid(), os.getppid())
View Code

四、守护进程

主进程创建守护进程

  • 守护进程会在主进程代码执行结束后就终止
  • 守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
  • 注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止
  • 一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行
from multiprocessing import Process
 
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")
 
def bar():
    print(456)
    time.sleep(3)
    print("end456")
 
if __name__ == '__main__':
    p1=Process(target=foo)
    p2=Process(target=bar)
 
    p1.daemon=True
    p1.start()
    p2.start()
    # p2.join()
    print("main-------")
 
"""
#主进程代码运行完毕,守护进程就会结束
main-------
456
end456
"""
View Code

五、互斥锁

进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的, 而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理

from multiprocessing import Process

def work():
    global n
    n=0
    print('子进程内: ',n)


if __name__ == '__main__':
    n = 100
    p=Process(target=work)
    p.start()
    print('主进程内: ', n)
    p.join()
    print('主进程内: ',n)

"""
主进程内:  100
子进程内:  0
主进程内:  100
"""
进程之间的数据隔离问题
"""
db.txt的内容为:{"count":1}
"""
from multiprocessing import Process, Lock
import json
import time


def search(name):
    time.sleep(1)
    dic = json.load(open('db.txt', 'r', encoding='utf-8'))
    print('<%s> 查看到剩余票数【%s】' % (name, dic['count']))


def get(name):
    time.sleep(1)
    dic = json.load(open('db.txt', 'r', encoding='utf-8'))
    if dic['count'] > 0:
        dic['count'] -= 1
        time.sleep(3)
        json.dump(dic, open('db.txt', 'w', encoding='utf-8'))
        print('<%s> 购票成功' % name)

####方式一:
def task(name, mutex):
    search(name)
    mutex.acquire()  # =============
    get(name)
    mutex.release()  # =============
    
    
####方式二 推荐:
def task(name, mutex):
    search(name)
    with mutex: # =============
        get(name)
    
if __name__ == '__main__':
    mutex = Lock()  # =============
    for i in range(10):
        p = Process(target=task, args=('路人%s' % i, mutex))
        p.start()
View Code
#加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
虽然可以用文件共享数据实现进程间通信,但问题是:
1.效率低(共享数据基于文件,而文件是硬盘上的数据)
2.需要自己加锁处理

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

六、队列(推荐使用)

1、Queue

  • Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
  • maxsize是队列中允许最大项数,省略则无大小限制。
  • q.put方法用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
  • q.get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.
  • q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
  • q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
  • q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
"""
基本用法
"""
from multiprocessing import Process, Queue
import time
 
q = Queue(3)
 
# put ,get ,put_nowait,get_nowait,full,empty
q.put(3)
q.put(3)
q.put(3)
 
print(q.full())  # 满了 True
print(q.get())
print(q.get())
print(q.get())
print(q.empty())  # 空了 True
 
"""
基于队列来实习一个生产者消费者模型
问题是主进程永远不会结束,原因是:生产者p在生产完后就结束了,
但是消费者c在取空了q之后,则一直处于死循环中且卡在q.get()这一步。
"""
from multiprocessing import Process,Queue
import time,random
def consumer(q,name):
    while True:
        res=q.get()
        time.sleep(random.randint(1,3))
        print('%s 吃 %s' %(name,res))
 
def producer(q,name,food):
    for i in range(3):
        time.sleep(random.randint(1,3))
        res='%s%s' %(food,i)
        q.put(res)
        print('%s 生产了 %s' %(name,res))
 
if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=(q,'egon','包子'))
    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,'alex'))
    #开始
    p1.start()
    c1.start()
    print('')
 
"""
解决方式:让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环
"""
from multiprocessing import Process,Queue
import time,random,os
def consumer(q,name):
    while True:
        res=q.get()
        if res is None:break
        time.sleep(random.randint(1,3))
        print('%s 吃 %s' %(name,res))
 
def producer(q,name,food):
    for i in range(3):
        time.sleep(random.randint(1,3))
        res='%s%s' %(food,i)
        q.put(res)
        print('%s 生产了 %s' %(name,res))
 
if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=(q,'egon','包子'))
 
    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,'alex'))
 
    #开始
    p1.start()
    c1.start()
 
    p1.join()
    q.put(None)
    print('')
 
 
"""
有多个生产者和多个消费者时,有几个消费者就需要发送几次结束信号:相当low,需要使用JoinableQueue
"""
from multiprocessing import Process,Queue
import time,random,os
def consumer(q,name):
    while True:
        res=q.get()
        if res is None:break
        time.sleep(random.randint(1,3))
        print('\%s 吃 %s' %(name,res))
 
def producer(q,name,food):
    for i in range(3):
        time.sleep(random.randint(1,3))
        res='%s%s' %(food,i)
        q.put(res)
        print('%s 生产了 %s' %(name,res))
 
if __name__ == '__main__':
    q=Queue()
    #生产者们:即厨师们
    p1=Process(target=producer,args=(q,'egon1','包子'))
    p2=Process(target=producer,args=(q,'egon2','骨头'))
    p3=Process(target=producer,args=(q,'egon3','泔水'))
 
    #消费者们:即吃货们
    c1=Process(target=consumer,args=(q,'alex1'))
    c2=Process(target=consumer,args=(q,'alex2'))
 
    #开始
    p1.start()
    p2.start()
    p3.start()
    c1.start()
    c2.start()
 
    p1.join()
    p2.join()
    p3.join()
    q.put(None)
    q.put(None)
    q.put(None)
    print('')
生产者消费者模型

2、JoinableQueue

JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

  • maxsize是队列中允许最大项数,省略则无大小限制。
  • JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:
  • q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
  • q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
from multiprocessing import Process,JoinableQueue
import time
 
def producer(q):
    for i in range(2):
        res='包子%s' %i
        time.sleep(0.5)
        print('生产者生产了%s' %res)
 
        q.put(res)
    q.join()
 
def consumer(q):
    while True:
        res=q.get()
        if res is None:break
        time.sleep(1)
        print('消费者吃了%s' % res)
        q.task_done()  #向q.join()发送一次信号,证明一个数据已经被取走了
 
 
if __name__ == '__main__':
    #容器
    q=JoinableQueue()
 
    #生产者们
    p1=Process(target=producer,args=(q,))
    p2=Process(target=producer,args=(q,))
    p3=Process(target=producer,args=(q,))
 
    #消费者们
    c1=Process(target=consumer,args=(q,))
    c2=Process(target=consumer,args=(q,))
    c1.daemon=True
    c2.daemon=True
 
    # p1.start()
    # p2.start()
    # p3.start()
    # c1.start()
    # c2.start()
 
    # 开始
    p_l = [p1, p2, p3, c1, c2]
    for p in p_l:
        p.start()
 
    p1.join()
    p2.join()
    p3.join()
    print('')
 
#主进程等--->p1,p2,p3等---->c1,c2
#p1,p2,p3结束了,证明c1,c2肯定全都收完了p1,p2,p3发到队列的数据
#因而c1,c2也没有存在的价值了,应该随着主进程的结束而结束,所以设置成守护进程
View Code

七、进程池

"""
服务端多进程
"""
from socket import *
from multiprocessing import Process
 
server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server.bind(('127.0.0.1',8080))
server.listen(5)
 
def talk(conn,client_addr):
    while True:
        try:
            msg=conn.recv(1024)
            if not msg:break
            conn.send(msg.upper())
        except Exception:
            break
 
if __name__ == '__main__': #windows下start进程一定要写到这下面
    while True:
        conn,client_addr=server.accept()
        p=Process(target=talk,args=(conn,client_addr))
        p.start()
 
"""
服务端进程池
#Pool内的进程数默认是cpu核数,假设为4(查看方法os.cpu_count())
#开启6个客户端,会发现2个客户端处于等待状态
#在每个进程内查看pid,会发现pid使用为4个,即多个客户端公用4个进程
"""
from socket import *
from multiprocessing import Pool
import os
 
server=socket(AF_INET,SOCK_STREAM)
server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server.bind(('127.0.0.1',8080))
server.listen(5)
 
def talk(conn,client_addr):
    print('进程pid: %s' %os.getpid())
    while True:
        try:
            msg=conn.recv(1024)
            if not msg:break
            conn.send(msg.upper())
        except Exception:
            break
 
if __name__ == '__main__':
    p=Pool()
    while True:
        conn,client_addr=server.accept()
        p.apply_async(talk,args=(conn,client_addr))
        # p.apply(talk,args=(conn,client_addr)) #同步的话,则同一时间只有一个客户端能访问
 
"""
客户端都一样
"""
from socket import *
 
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
 
 
while True:
    msg=input('>>: ').strip()
    if not msg:continue
 
    client.send(msg.encode('utf-8'))
    msg=client.recv(1024)
    print(msg.decode('utf-8'))
View Code

多线程

多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。

一、进程与线程的区别

  • 开进程的开销远大于开线程
  • 同一进程内的多个线程共享该进程的地址空间
  • from multiprocessing import Process p1=Process(target=task,) 换成 from threading import Thread t1=Thread(target=task,)
  • 计算密集型的多线程不能增强性能,多进程才可以,I/O密集型的多线程会加快程序的执行速度

二、threading模块介绍

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

1、Thread实例对象的方法

  • isAlive(): 返回线程是否活动的。
  • getName(): 返回线程名。
  • setName(): 设置线程名。

2、threading模块提供的一些方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread,currentThread,active_count,enumerate
import time
 
def task():
    print('%s is ruuning' %currentThread().getName())
    time.sleep(2)
    print('%s is done' %currentThread().getName())
 
if __name__ == '__main__':
    # 在主进程下开启线程
    t=Thread(target=task,name='子线程1')
    t.start()
    t.setName('儿子线程1')
    # t.join()
    print(t.getName())
 
    currentThread().setName('主线程')
    print(t.isAlive())
 
    print('主线程',currentThread().getName())
    print(active_count())
    print(enumerate()) #连同主线程在内有两个运行的线程
 
"""
子线程1 is ruuning
儿子线程1
True
主线程 主线程
2
[<_MainThread(主线程, started 8672)>, <Thread(儿子线程1, started 7512)>]
儿子线程1 is done
"""
View Code

3、开启子线程的两种方式

注意:在windows中Process()必须放到# if __name__ == '__main__':下

#方式一
from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)
 
if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    print('主线程')
     
#方式二
from threading import Thread
import time
class Sayhi(Thread):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        time.sleep(2)
        print('%s say hello' % self.name)
 
 
if __name__ == '__main__':
    t = Sayhi('egon')
    t.start()
    print('主线程')
View Code

三、守护线程

  • 无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁,需要强调的是:运行完毕并非终止运行
  • 对主进程来说,运行完毕指的是主进程代码运行完毕---主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
  • 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕---主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
  • 一定要在t.start()前设置,设置t为守护线程
from threading import Thread
import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")
 
def bar():
    print(456)
    time.sleep(3)
    print("end456")
 
t1=Thread(target=foo)
t2=Thread(target=bar)
 
t1.daemon=True
t1.start()
t2.start()
print("main-------")
"""
123
456
main-------
end123
end456
"""
View Code

四、 GIL

  • 在Cpython解释器中,同一个进程下开启的多线程,因为有GIL的存在,同一时刻同一进程中只能有一个线程被执行
  • GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
  • 保护不同的数据的安全,就应该加不同的锁。
  • 在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内
  • 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
  • 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。
  • GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock
如果多个线程的target=work,那么执行流程是:

多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行

解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码

#分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程

#单核情况下,分析结果: 
  如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
  如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

#多核情况下,分析结果:
  如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
  如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

 
#结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,
每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,
假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,
可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,
为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  
这可以说是Python早期版本的遗留问题。 


应用:

多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析

五、互斥锁

  • 线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来
  • join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高
过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限

  线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果

  既然是串行,那我们执行

  t1.start()

  t1.join

  t2.start()

  t2.join()

  这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。
from threading import Thread, Lock
import time

n = 10

def task(mutex):
    global n
    mutex.acquire()
    temp = n
    time.sleep(0.1)
    n = temp - 1
    mutex.release()

if __name__ == '__main__':
    mutex = Lock()
    t_l = []
    for i in range(10):
        t = Thread(target=task,args=(mutex,))
        t_l.append(t)
        t.start()

    for t in t_l:
        t.join()

    print('', n)  #主 0
View Code

六、死锁现象与递归锁

  • 进程也有死锁与递归锁
  • 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
  • 解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。 这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁
# 死锁
from threading import Thread,Lock
import time
 
mutexA=Lock()
mutexB=Lock()
 
class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()
 
    def f1(self):
        mutexA.acquire()
        print('%s 拿到了A锁' %self.name)
 
        mutexB.acquire()
        print('%s 拿到了B锁' %self.name)
        mutexB.release()
 
        mutexA.release()
 
 
    def f2(self):
        mutexB.acquire()
        print('%s 拿到了B锁' % self.name)
        time.sleep(0.1)
 
        mutexA.acquire()
        print('%s 拿到了A锁' % self.name)
        mutexA.release()
 
        mutexB.release()
 
if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()
 
 
#互斥锁只能acquire一次
from threading import Thread,Lock
 
mutexA=Lock()
 
mutexA.acquire()
mutexA.release()
 
 
# 递归锁:可以连续acquire多次,每acquire一次计数器+1,只有计数为0时,才能被抢到acquire
from threading import Thread,RLock
import time
 
mutexB=mutexA=RLock()  #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
 
class MyThread(Thread):
    def run(self):
        self.f1()
        self.f2()
 
    def f1(self):
        mutexA.acquire()
        print('%s 拿到了A锁' %self.name)
 
        mutexB.acquire()
        print('%s 拿到了B锁' %self.name)
        mutexB.release()
 
        mutexA.release()
 
 
    def f2(self):
        mutexB.acquire()
        print('%s 拿到了B锁' % self.name)
        time.sleep(7)
 
        mutexA.acquire()
        print('%s 拿到了A锁' % self.name)
        mutexA.release()
 
        mutexB.release()
 
if __name__ == '__main__':
    for i in range(10):
        t=MyThread()
        t.start()
View Code

七、信号量Semaphore

信号量与进程池的概念很像,但是要区分开,信号量涉及到加锁的概念
进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

同进程的一样
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

互斥锁同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
from threading import Thread,Semaphore,currentThread
import time,random
 
sm=Semaphore(3)
 
def task():
    # sm.acquire()
    # print('%s in' %currentThread().getName())
    # sm.release()
    with sm:
        print('%s in' %currentThread().getName())
        time.sleep(random.randint(1,3))
 
if __name__ == '__main__':
    for i in range(4):
        t=Thread(target=task)
        t.start()
View Code

八、Event

线程的一个关键特性是每个线程都是独立运行且状态不可预测。
如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。
为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象,而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。
一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
  • event.isSet():返回event的状态值;
  • event.wait():如果 event.isSet()==False将阻塞线程;
  • event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
  • event.clear():恢复event的状态值为False。
from threading import Thread,Event
import time
 
event=Event()
 
def student(name):
    print('学生%s 正在听课' %name)
    event.wait(2)
    print('学生%s 课间活动' %name)
 
def teacher(name):
    print('老师%s 正在授课' %name)
    time.sleep(7)
    event.set()
 
if __name__ == '__main__':
    stu1=Thread(target=student,args=('tom',))
    stu2=Thread(target=student,args=('rose',))
    stu3=Thread(target=student,args=('jack',))
    t1=Thread(target=teacher,args=('tony',))
 
    stu1.start()
    stu2.start()
    stu3.start()
    t1.start()
 
 
from threading import Thread,Event,currentThread
import time
 
event=Event()
 
def conn():
    n=0
    while not event.is_set():
        if n == 3:
            print('%s try too many times' %currentThread().getName())
            return
        print('%s try %s' %(currentThread().getName(),n))
        event.wait(0.5)
        n+=1
 
    print('%s is connected' %currentThread().getName())
 
def check():
    print('%s is checking' %currentThread().getName())
    time.sleep(5)
    event.set()
 
if __name__ == '__main__':
    for i in range(3):
        t=Thread(target=conn)
        t.start()
    t=Thread(target=check)
    t.start()
View Code

九、定时器

"""
60s后换验证码
"""
from threading import Timer
import random
 
class Code:
    def __init__(self):
        self.make_cache()
 
    def make_cache(self,interval=60):
        self.cache=self.make_code()
        print(self.cache)
        self.t=Timer(interval,self.make_cache)
        self.t.start()
 
    def make_code(self,n=4):
        res=''
        for i in range(n):
            s1=str(random.randint(0,9))
            s2=chr(random.randint(65,90))
            res+=random.choice([s1,s2])
        return res
 
    def check(self):
        while True:
            code=input('请输入你的验证码>>: ').strip()
            if code.upper() == self.cache:
                print('验证码输入正确')
                self.t.cancel()
                break
 
obj=Code()
obj.check()
View Code

十、线程queue

import queue
 
q=queue.Queue(3) #先进先出->队列
 
q.put('first')
q.put(2)
q.put('third')
# q.put(4)
# q.put(4,block=False) #q.put_nowait(4)
# q.put(4,block=True,timeout=3)
 
print(q.get())
print(q.get())
print(q.get())
# print(q.get(block=False)) #q.get_nowait()
# print(q.get_nowait())
# print(q.get(block=True,timeout=3))
 
#======================================
q=queue.LifoQueue(3) #后进先出->堆栈
q.put('first')
q.put(2)
q.put('third')
 
print(q.get())
print(q.get())
print(q.get())
 
#======================================
q=queue.PriorityQueue(3) #优先级队列
 
q.put((10,'one'))
q.put((40,'two'))
q.put((30,'three'))
 
print(q.get())
print(q.get())
print(q.get())
View Code

十一、线程池

#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)
回调函数
"""
shutdown
把ProcessPoolExecutor换成ThreadPoolExecutor,其余用法全部相同
"""
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import os, time, random


def task(n):
    print('%s is runing' % os.getpid())
    time.sleep(random.randint(1, 3))
    return n ** 2


if __name__ == '__main__':
    pool = ProcessPoolExecutor(max_workers=3)
    futures = []
    for i in range(11):
        future = pool.submit(task, i)
        futures.append(future)
    pool.shutdown(True)
    print('+++>')
    for future in futures:
        print(future.result())

"""
map方法(参数)
"""
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

import os, time, random


def task(n):
    print('%s is runing' % os.getpid())
    time.sleep(random.randint(1, 3))
    return n ** 2


if __name__ == '__main__':
    pool = ThreadPoolExecutor(max_workers=3)
    ret = pool.map(task, range(1, 12))  # map取代了for+submit
    for i in ret:
        print(i) #i就是返回值
"""
add_done_callback
"""
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import requests
import os


def get_page(url):
    print('<进程%s> get %s' % (os.getpid(), url))
    respone = requests.get(url)
    if respone.status_code == 200:
        return {'url': url, 'text': respone.text}


def parse_page(res):
    res = res.result()
    print('<进程%s> parse %s' % (os.getpid(), res['url']))
    parse_res = 'url:<%s> size:[%s]\n' % (res['url'], len(res['text']))
    with open('db.txt', 'a') as f:
        f.write(parse_res)


if __name__ == '__main__':
    urls = [
        'https://www.baidu.com',
        'https://www.python.org',
        'https://www.openstack.org',
        'https://help.github.com/',
        'http://www.sina.com.cn/'
    ]

    p = ProcessPoolExecutor(3)
    for url in urls:
        p.submit(get_page, url).add_done_callback(parse_page)  # parse_page拿到的是一个future对象obj,需要用obj.result()拿到结果
View Code
#基于多线程实现
from socket import *
from threading import Thread          #多线程
# from multiprocessing import Process  #多进程
 
def communicate(conn):
    while True:
        try:
            data=conn.recv(1024)
            if not data:break
            conn.send(data.upper())
        except ConnectionResetError:
            break
 
    conn.close()
 
def server(ip,port):
    server = socket(AF_INET, SOCK_STREAM)
    server.bind((ip,port))
    server.listen(5)
 
    while True:
        conn, addr = server.accept()
        t=Thread(target=communicate,args=(conn,))       # 多线程
        # t = Process(target=communicate,args=(conn,))  # 多进程
        t.start()
 
    server.close()
 
if __name__ == '__main__':
    server('127.0.0.1', 8081)
 
 
#基于线程池实现
from socket import *
from concurrent.futures import ThreadPoolExecutor   #线程池
# from concurrent.futures import ProcessPoolExecutor #进程池
 
def communicate(conn):
    while True:
        try:
            data=conn.recv(1024)
            if not data:break
            conn.send(data.upper())
        except ConnectionResetError:
            break
 
    conn.close()
 
def server(ip,port):
    server = socket(AF_INET, SOCK_STREAM)
    server.bind((ip,port))
    server.listen(5)
 
    while True:
        conn, addr = server.accept()
        pool.submit(communicate,conn)                      # #####
 
    server.close()
 
if __name__ == '__main__':
    pool=ThreadPoolExecutor(2)    #线程池
    #pool=ProcessPoolExecutor()   #进程池
    server('127.0.0.1', 8081)
View Code
import requests
from lxml import etree
import re
from multiprocessing.dummy import Pool
#需求:爬取梨视频的视频数据
headers = {
    'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
}
#原则:线程池处理的是阻塞且较为耗时的操作

#对下述url发起请求解析出视频详情页的url和视频的名称
url = 'https://www.pearvideo.com/category_5'
page_text = requests.get(url=url,headers=headers).text

tree = etree.HTML(page_text)
li_list = tree.xpath('//ul[@id="listvideoListUl"]/li')
urls = [] #存储所有视频的链接and名字
for li in li_list:
    detail_url = 'https://www.pearvideo.com/'+li.xpath('./div/a/@href')[0]
    name = li.xpath('./div/a/div[2]/text()')[0]+'.mp4'
    #对详情页的url发起请求
    detail_page_text = requests.get(url=detail_url,headers=headers).text
    #从详情页中解析出视频的地址(url)
    ex = 'srcUrl="(.*?)",vdoUrl'
    video_url = re.findall(ex,detail_page_text)[0]
    dic = {
        'name':name,
        'url':video_url
    }
    urls.append(dic)
#对视频链接发起请求获取视频的二进制数据,然后将视频数据进行返回
def get_video_data(dic):
    url = dic['url']
    print(dic['name'],'正在下载......')
    data = requests.get(url=url,headers=headers).content
    #持久化存储操作
    with open(dic['name'],'wb') as fp:
        fp.write(data)
        print(dic['name'],'下载成功!')
#使用线程池对视频数据进行请求(较为耗时的阻塞操作)
pool = Pool(4)
pool.map(get_video_data,urls)

pool.close()
pool.join()
线程池在爬虫案例中的应用

 

 

posted @ 2018-12-31 23:51  silencio。  阅读(489)  评论(0编辑  收藏  举报