Py并发编程及应用(多线程、多进程)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_44015805/article/details/100164597
# -*- coding: utf-8 -*-

'''
Py并发编程及应用.py
(多线程:创建、同步锁、线程通信、线程池、线程局部变量、线程定时器、任务调度器)
(多进程:创建、进程池、进程通信、并发及异步并发爬虫、全局解释器锁、垃圾回收机制)



深入:
1、线程锁、线程通信的概念和运用
2、任务调度器的运用
3、asyncio模块   异步并发模块(爬虫应用)
4、aiohttp模块   异步的requests(爬虫应用)



注意:
1、并发和并行是两个概念,
并行指在同一时刻有多条指令在多个处理器上同时执行;
并发是指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

2、默认情况下,主线程的名字为 MainThread,用户启动的多个线程的名字依次为 Thread-1、Thread-2、Thread-3、...、Thread-n 等。
3、注意线程执行完成后已经死亡状态,再次执行将引发 RuntimeError异常。另外对处于新建状态的线程两次调用start()方法也会引发异常。
4、如果要将某个线程设置为后台线程,则必须在该线程启动之前进行设置。也就是说,将 daemon 属性设为 True,必须在 start() 方法调用之前进行,否则会引发 RuntimeError 异常。
5、加锁操作RLock较为常用,注意加锁方式一般为 先加锁,然后try操作,最后finally解锁。
6、注意解决死锁问题的方式有:1、避免多次加锁 2、相同的加锁顺序 3、使用定时锁 即调用acquire()方法加锁时候指定timeout参数  4、死锁检测
7、注意线程池返回的Future对象方法result()会阻塞当前主线程;另外线程池类方法map()后可循环迭代每一个线程
8、Timer定时器只能控制函数在指定时间内执行一次。
如果要使用 Timer 控制函数多次重复执行,则需要函数内嵌套定时器 来循环执行下一次调度。

9、创建子进程时,multiprocessing.Process类通过实例化对象指定target参数执行函数 在IDLE里不显示函数执行内容。
通过在控制台通过执行文件可显示target参数执行函数的内容。
10、通过 multiprocessing.Process 来创建并启动进程时,程序必须先判断if __name__=='__main__':,否则可能引发异常。
11、进程通信中,使用实例化创建multiprocessing.Pipe类管道对象返回两个PipeConnection对象,
其中前一个PipeConnection对象用于接受数据,后一个PipeConnection对象用于发送数据

12、GIL 不能绝对保证线程安全
因为即便 GIL 仅允许一个 Python 线程执行,但Python 还有 check interval 这样的抢占机制。



使用:
第一部分:多线程相关
threading模块    线程封装

一、Python 主要通过两种方式来创建线程:
1、使用 threading 模块中的 Thread 类的构造器创建线程。
即直接对类 threading.Thread 进行实例化,并调用实例化对象的 start 方法创建线程。
2、继承 threading 模块中的 Thread 类创建线程类。
即用 threading.Thread 派生出一个新的子类,将新建类实例化,并调用其 start 方法创建线程。

二、Lock类 和 Rlock类 互斥锁  用来解决数据不同步的问题
threading模块提供了 Lock 和 RLock 两个类,都各自提供两个方法来 加互斥锁 和 解除互斥锁

三、线程通信
当线程在系统中运行时,线程的调度具有一定的透明性,通常程序无法准确控制线程的轮换执行,如果有需要,Python 可通过线程通信来保证线程协调运行。
线程通信的实现方式:
1、Condition类   来自threading模块
2、queue模块     队列
3、Event类       来自threading模块

四、线程池 ThreadPoolExecutor类
Executor 提供了两个子类,即 ThreadPoolExecutor 和 ProcessPoolExecutor,
其中 ThreadPoolExecutor类 用于创建线程池,而 ProcessPoolExecutor类 用于创建进程池。
1、线程池类方法(submit()、map()、shudown())
2、submit()类方法提交线程池后返回的Future对象方法
Future对象用result()方法来获取线程任务的运回值,但该方法会阻塞当前主线程,只有等到钱程任务完成后,result() 方法的阻塞才会被解除。
可通过 Future 的 add_done_callback() 方法来添加回调函数来避免直接调用result()方法阻塞线程
另外线程池类方法map()后可循环迭代每一个线程

五、线程局部变量  threading.local()

六、定时器  Timer子类    来自threading模块

七、任务调度器  sched模块  任务调度器模块
1、实例化任务调度器 sched.scheduler()
2、实例化任务调度器方法:绝对时间执行函数任务、相对时间执行函数任务、取消任务等


第二部分:多进程相关
multiprocessing模块 进程封装

八、multiprocessing.Process类    进程黄建
Python 在 multiprocessing模块下提供了 Process类 来创建新进程。
与 Thread 类似的是,使用 Process 创建新进程也有两种方式:
1、使用 multiprocessing模块 中的 Process类 来实例化对象创建新进程,并指定函数作为target参数函数
2、继承 Process类 ,并重写他的 run() 方法来创建进程类,程序创建Process子类的实例作为进程

九、进程启动的三种方式  spawn(windows仅支持此方式)、fork、forkserver
multiprocessing.set_start_method(' ')函数用于设置启动进程的方式,设置代码必须放在多进程代码之前。

十、进程池    multiprocessing.Pool类
通过 multiprocessing 模块的 Pool类来实例化创建进程池,然后通过进程池方法提交进程池、关闭进程池、等待进程池等

十一、进程通信    multiprocessing.Queue类  和 multiprocessing.Pipe类       
Python 为进程通信提供了两种机制:
Queue:一个进程向 Queue 中放入数据,另一个进程从 Queue 中读取数据。
Pipe:Pipe 代表连接两个进程的管道。程序在调用 Pipe() 函数时会产生两个连接端,分别交给通信的两个进程,接下来进程既可从该连接端读取数据,也可向该连接端写入数据。

十二、异步并发模块的性能提升需要后期单独重点深入
asyncio模块   异步并发模块
aiohttp模块   异步的requests

十三、垃圾回收机制    垃圾回收机制引用计数机制    
引用计数机制 即:对象的引用计数值为 0 时,说明这个对象永不再用,自然它就变成了垃圾,需要被回收。
gc模块 手动启动垃圾回收机制


'''

# =============================================================================
# #threading模块  线程封装
# #threading模块  Python 3 之后的线程模块,提供了功能丰富的多线程支持
# =============================================================================
#Python 主要通过两种方式来创建线程:
#1、使用 threading 模块中的 Thread 类的构造器创建线程。
#即直接对类 threading.Thread 进行实例化,并调用实例化对象的 start 方法创建线程。
#2、继承 threading 模块中的 Thread 类创建线程类。
#即用 threading.Thread 派生出一个新的子类,将新建类实例化,并调用其 start 方法创建线程。

import threading


help(threading)
threading.__doc__
threading.__file__
threading.__all__
dir(threading)


#使用 threading 模块中的 Thread 类的构造器创建线程。
#__init__(self, group=None, target=None, name=None, args=(), kwargs=None, *,daemon=None)

#构造器涉及如下几个参数:
#group:      指定该线程所属的线程组。目前该参数还未实现,因此它只能设为 None。
#target:     指定该线程要调度的目标方法。
#args:       指定一个元组,以位置参数的形式为 target 指定的函数传入参数。元组的第一个元素传给 target 函数的第一个参数,元组的第二个元素传给 target 函数的第二个参数……依此类推。
#kwargs:     指定一个字典,以关键字参数的形式为 target 指定的函数传入参数。
#daemon:     指定所构建的线程是否为后台线程  即 守护线程。

help(threading.Thread)
threading.Thread.__doc__
dir(threading.Thread)


help(threading.Thread.start)
help(threading.Thread.run)
help(threading.Thread.daemon)
help(threading.Thread.join)



####################
#创建线程方法一:  通过Thread类的构造器创建并启动多线程的步骤
#1、调用 Thread 类的构造器创建线程对象。在创建线程对象时,target参数 指定的函数将作为线程执行体。
#2、调用线程对象的 start() 方法启动该线程。

import threading

#先定义一个普通的action函数,准备作为线程执行体  即:target参数函数
def action(max):
    for i in range(max):
        #threading.current_thread()获取当前线程
        #.getName()获取名称
        print(threading.current_thread().getName() + ' ' + str(i))       #current_thread()获取当前线程;#getName()获取当前线程的名字

#主线程的执行体
for i in range(100):
    #输出当前线程的名称
    print(threading.current_thread().getName() + ' ' + str(i))
    if i == 20:
        #创建并启动第一个线程
        #target参数  指定函数作为线程执行体,
        #args参数    元组形式位置参数,用于指定为target参数的函数的参数。
        t1=threading.Thread(target = action, args=(100,))
        t1.start()
        
        #创建并启动第二个线程
        t2=threading.Thread(target = action, args=(100,))
        t2.start()
#注意:  
#默认情况下,主线程的名字为 MainThread,用户启动的多个线程的名字依次为 Thread-1、Thread-2、Thread-3、...、Thread-n 等。
print('主线程执行完成!')


#Threading 模块中,除了 current_thread() 函数外,还经常使用如下 2 个函数:
#threading.enumerate():      返回一个正运行线程的 list。“正运行”是指线程处于“启动后,且在结束前”状态,不包括“启动前”和“终止后”状态。
#threading.activeCount():    返回正在运行的线程数量。与 len(threading.enumerate()) 有相同的结果



####################
#创建线程方法二:  通过继承 Thread 类来创建并启动线程的步骤
#1、定义 Thread 类的子类,并重写该类的 run() 方法。
#run() 方法的方法体就代表了线程需要完成的任务,因此把 run() 方法称为线程执行体。
#2、创建 Thread 子类的实例,即创建线程对象。
#3、调用线程对象的 start() 方法来启动线程。

import threading

#先定义类继承Thread类,来准备实例化创建和启动线程
class FkThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        self.i = 0
    #重写run()方法,run()方法代表线程需要完成的任务,成为线程执行体
    def run(self):
        while self.i < 100:
            print(threading.current_thread().getName() + ' ' + str(self.i))
            self.i += 1

for i in range(100):
    print(threading.current_thread().getName() + ' ' + str(i))
    if i == 20:
        
        #实例化来创建第一个线程 然后 启动线程
        ft1 = FkThread()
        ft1.start()
        
        #实例化来创建第二条线程 然后 启动线程
        ft2 = FkThread()
        ft2.start()

print('主线程执行完成!')



####################
#线程的状态

##########
#线程的新建状态  和  线程的就绪状态
#启动线程使用start()方法,而不是run()方法
#调用 start() 方法来启动线程,系统会把该 run() 方法当成线程执行体来处理;
#在调用线程对象的 start() 方法之后,该线程立即进入就绪状态(相当于“等待执行”),但该线程并未真正进入运行状态。

#但如果直接调用线程对象的 run() 方法,则 run() 方法立即就会被执行,而且在该方法返回之前其他线程无法并发执行。

import threading

# 先定义函数  准备作为线程执行体的target参数函数
def action(max):
    for i in range(max):
        #直接调用run()方法时,Thread的name属性返回的是该对象的名字
        #而不是当前线程的名字
        #使用threading.current_thread().name总是获取当前线程的名字
        print(threading.current_thread().name +  " " + str(i))
        
for i in range(100):
    #调用Thread的currentThread()方法获取当前线程
    print(threading.current_thread().name +  " " + str(i))
    if i == 20:
        # 直接调用线程对象的run()方法
        # 系统会把线程对象当成普通对象,把run()方法当成普通方法
        # 所以下面两行代码并不会启动两个线程,而是依次执行两个run()方法
        threading.Thread(target=action,args=(100,)).run()
        threading.Thread(target=action,args=(100,)).run()


##########
#线程的运行状态
#如果处于就绪状态的线程获得了 CPU,开始执行 run() 方法的线程执行体,则该线程处于运行状态。


##########
#线程的阻塞状态
#线程将会进入阻塞状态的几种情况:
#1、线程调用 sleep() 方法主动放弃其所占用的处理器资源。
#2、线程调用了一个阻塞式 I/O 方法,在该方法返回之前,该线程被阻塞。
#3、线程试图获得一个锁对象,但该锁对象正被其他线程所持有。关于锁对象的知识,后面将有更深入的介绍。
#4、线程在等待某个通知(Notify)。


##########
#线程的死亡状态
#线程结束后就处于死亡状态。线程结束有两种方式:一种是run()方法或代表线程执行体的targe函数执行完成。另一种是出现异常。

#is_alive()方法  测试某个显示是否已经死亡。
#当线程处于就绪、运行、阻塞三种状态时,该方法将返回 True;当线程处于新建、死亡两种状态时,该方法将返回 False。
import threading

#先定义一个函数  用于准备作为线程执行体target参数函数来使用
def action(max):
    '''
    先定义一个函数  用于准备作为线程执行体target参数函数来使用
    循环输出当前线程名称100次
    '''
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))

#创建线程对象    进入新建状态
sd=threading.Thread(target= action, args=(100,))

for i in range(300):
    print(threading.current_thread().name + ' ' + str(i))
    if i == 20:
        #启动线程    进入就绪状态
        sd.start()
        print(sd.is_alive())    #当线程处于就绪、运行、阻塞三种状态时,该方法将返回 True;当线程处于新建、死亡两种状态时,该方法将返回 False。

#    if i > 20:
#        #注意线程执行完成后已经死亡状态,再次执行将引发 RuntimeError异常。另外对处于新建状态的线程两次调用start()方法也会引发异常。
#        sd.start()



####################
#join()方法  
#join()方法让一个线程等待另一个线程完成。
#当某个程序执行流中调用其他线程的 join() 方法时候,调用线程将被阻塞,直到被join()方法加入的join线程执行完成。

import threading

#先定义一个函数  用于准备作为线程执行体target参数函数来使用
def action(max):
    '''
    先定义一个函数  用于准备作为线程执行体target参数函数来使用
    循环输出当前线程名称100次
    '''
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))

#直接新建并启动 子线程
threading.Thread(target=action,args=(100,),name='新线程').start()

#演示join()方法  
for i in range(100):
    if i == 20:
        #演示阻塞第20次执行线程,等运行完 被join的线程后 继续运行20次以后的线程
        jt=threading.Thread(target=action,args=(100,),name='被join的线程')
        jt.start()
        
        #join(timeout=None)方法 还可以指定一个 timeout 参数,该参数指定等待被 join 的线程的时间最长为 timeout 秒。如果在 timeout 秒内被 join 的线程还没有执行结束,则不再等待
        jt.join()
    print(threading.current_thread().name + ' ' + str(i))
    


####################
#后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程。调用 Thread 对象的 daemon 属性可以将指定线程设置成后台线程

#创建后台线程有两种方式:
#1、主动将线程的 daemon属性 设置为 True
#2、也可在创建Thread对象时通过daemon参数将其设为后台线程  即 守护线程。

#后台线程有一个特征,如果所有的前台线程都死亡了,那么后台线程会自动死亡。
#Python 解释器的垃圾回收线程就是典型的后台线程。
    
import threading

#先定义一个函数  用于准备作为线程执行体target参数函数来使用
def action(max):
    '''
    先定义一个函数  用于准备作为线程执行体target参数函数来使用
    循环输出当前线程名称100次
    '''
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))

t=threading.Thread(target=action,args=(100,),name='后台线程')

# 将此线程设置成后台线程,通过设置线程的daemon属性为 True
# 也可在创建Thread对象时通过daemon参数将其设为后台线程

#注意:如果要将某个线程设置为后台线程,则必须在该线程启动之前进行设置。
#也就是说,将 daemon 属性设为 True,必须在 start() 方法调用之前进行,否则会引发 RuntimeError 异常。
t.daemon=True    
t.start()

for i in range(10):
    print(threading.current_thread().name + ' ' +str(i))











# =============================================================================
# #Lock类 和 Rlock类 互斥锁  用来解决数据不同步的问题
# #threading模块提供了 Lock 和 RLock 两个类,都各自提供两个方法来 加互斥锁 和 解除互斥锁
# =============================================================================


##########
#银行取钱  数据不同步  问题示例

#先定义一个账户类,并且封装账户编号 和 账户余额
class Account:
    '''封装账户编号 和 余额 这两个成员属性'''
    def __init__(self,account_no,balance):
        self.account_no = account_no
        self.balance = balance

import threading
import time
#定义一个函数  来准备作为线程的执行函数   来模拟取钱操作
def draw(account,draw_amount):
    '''传入参数 实例化账户  和 取钱金额;来实现取钱操作,如果实例化账户余额足够则取款成功,否则取款失败'''
    if account.balance >= draw_amount:
        print(threading.current_thread().name + '取钱成功,吐出钞票:' + str(draw_amount))
        #time.sleep(0.2)
        account.balance -= draw_amount
        print('余额为:' + str(account.balance))
    else:
        print(threading.current_thread().name + '取钱失败,余额不足!')       

#创建实例化 账户对象
acct= Account('1234567',1000)

#启动两个线程模拟对同一个实例化 的账户对象进行取钱操作
#多次运行,则会出现 余额不足但仍取款成功 的问题
#原由线程的 run()方法不具有线程安全性
threading.Thread(name='甲', target=draw, args=(acct,800)).start()
threading.Thread(name='乙', target=draw, args=(acct,800)).start()



##########
#Lock类 和 RLock类 的相同:  加锁和解锁方法
#1、acquire(blocking=True, timeout=-1):    请求对 Lock 或 RLock 加锁,其中 timeout 参数指定加锁多少秒。
#2、release():                             释放锁。

#Lock 和 RLock 的不同:    
#1、threading.Lock:   #它是一个基本的锁对象,每次只能锁定一次,其余的锁请求,需等待锁释放后才能获取。
#2、threading.RLock:  #它代表可重入锁(Reentrant Lock)。对于可重入锁,在同一个线程中可以对它进行多次锁定,也可以多次释放。
#如果使用 RLock,那么 acquire() 和 release() 方法必须成对出现。
#如果调用了 n 次 acquire() 加锁,则必须调用 n 次 release() 才能释放锁。

import threading

help(threading.Lock)
help(threading.RLock)

#示例
#修改Account类,加锁,来让其线程安全
#修改的该类中,定义了一个 RLock 对象。在程序中实现 draw() 方法时,进入该方法开始执行后立即请求对 RLock 对象加锁,
#当执行完 draw() 方法的取钱逻辑之后,程序使用 finally 块来确保释放锁。
class Account:
    def __init__(self,account_no, balance):
        self.account_no = account_no
        self._balance = balance
        self.lock = threading.RLock()
    
    def getBalance(self):
        return self._balance
    
    def draw(self,draw_amount):
        #加锁操作
        #加锁操作RLock较为常用,注意加锁方式一般为 先加锁,然后try操作,最后finally解锁。
        self.lock.acquire()
        try:
            if self._balance >= draw_amount:
                print(threading.current_thread().name + '取钱成功,吐出钞票:' + str(draw_amount))
                time.sleep(0.001)
                #取钱成功后修改余额
                self._balance -= draw_amount
                print('\t余额为:' + str(self._balance))
            else:
                print(threading.current_thread().name + '取钱失败,余额不足!')
        finally:
            #解锁操作
            self.lock.release()

#定义函数  准备作为线程的target参数执行函数  来模拟取钱操作
def draw(account,draw_amount):
    account.draw(draw_amount)

acct=Account('123456789',1000)
threading.Thread(name='甲', target=draw, args=(acct,800)).start()
threading.Thread(name='乙', target=draw, args=(acct,800)).start()




##########
#死锁
#当两个线程相互等待对方释放同步监视器时,就会发生死锁。
#Py解释器没有监测,也没有采取措施来处理死锁情况,所以在进行多线程时应该采取措施避免出现死锁。
#注意:一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程都处于阻塞状态,无法继续。

#死锁示例
#在系统中出现多个同步监视器的情况下,很容易发生死锁
import threading
import time

class A:
    def __init__(self):
        self.lock=threading.RLock()
    def foo(self,b):
        try:
            #加锁操作
            self.lock.acquire()
            print('当前线程名:' + threading.current_thread().name + '进入A实例的foo()方法')
            time.sleep(0.02)
            print('当前线程名:' + threading.current_thread().name + '企图调用B实例的last()方法')
            
            #调用另一个类实例方法中也有加锁解锁操作,容易发生死锁问题
            b.last()    
        finally:
            #解锁操作
            self.lock.release()     
    def last(self):
        try:
            self.lock.acquire()
            print('进入了A类的last()方法内部')
        finally:
            self.lock.release()

class B:
    def __init__(self):
        self.lock=threading.RLock()
    def bar(self,a):
        try:
            self.lock.acquire()
            print('当前线程名:' + threading.current_thread().name + '进入了B实例的bar()方法')
            time.sleep(0.2)
            print('当前线程名:' + threading.current_thread().name + '企图调用A实例的last()方法')
            a.last()
        finally:
            self.lock.release()
    def last(self):
        try:
            self.lock.acquire()
            print('进入了B类的last()方法内部')
        finally:
            self.lock.release()               
a=A()
b=B()
def init():
    threading.current_thread().name='主线程'
    a.foo(b)
    print('进入了主线程之后')
def action():
    threading.current_thread().name='副线程'
    b.bar(a)
    print('进入了副线程之后')

#以action()为target参数函数启动新线程

#两个线程开启 即出现死锁问题。
#注意解决死锁问题的方式有:1、避免多次加锁 2、相同的加锁顺序 3、使用定时锁 即调用acquire()方法加锁时候指定timeout参数  4、死锁检测
threading.Thread(target=action).start()
#init()    #若开启则出现死锁问题。











# =============================================================================
# #线程通信
# #当线程在系统中运行时,线程的调度具有一定的透明性,通常程序无法准确控制线程的轮换执行,如果有需要,Python 可通过线程通信来保证线程协调运行。
# #线程通信的实现方式:1、threading模块的Condition类  2、queue队列模块  3、threading模块的Event类
# =============================================================================

####################
#Condition类 
#来自threading线程模块
#Condition对象可以让那些己经得到 Lock 对象却无法继续执行的线程释放 Lock 对象,
#Condition对象也可以唤醒其他处于等待状态的线程


import threading

help(threading.Condition)
threading.Condition.__doc__

#Condition 类提供了如下几个方法:
#acquire([timeout]) / release():  调用 Condition 关联的 Lock 的 acquire() 或 release() 方法。
#wait([timeout]):                 导致当前线程进入 Condition 的等待池等待通知并释放锁,直到其他线程调用该 Condition 的 notify() 或 notify_all() 方法来唤醒该线程。在调用该 wait() 方法时可传入一个 timeout 参数,指定该线程最多等待多少秒。
#notify():                        唤醒在该 Condition 等待池中的单个线程并通知它,收到通知的线程将自动调用 acquire() 方法尝试加锁。如果所有线程都在该 Condition 等待池中等待,则会选择唤醒其中一个线程,选择是任意性的。
#notify_all():                    唤醒在该 Condition 等待池中等待的所有线程并通知它们。


#示例    使用Condition()对象方法实现线程通信
import threading

class Account:
    '''
    定义一个Account 类,提供 draw() 和 deposit() 两个方法,分别对应于该账户的取钱和存款操作。
    因为这两个方法可能需要并发修改 Account 类的 self.balance 成员变量的值,所以它们都使用 Lock 来控制线程安全。
    除此之外,这两个方法还使用了 Condition 的 wait() 和 notify_all() 来控制线程通信。
    
    实现要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。
    不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
    '''
    def __init__(self,account_no, balance):
        #封装账户编号 和 账户余额 的两个变量
        self.account_no = account_no
        self._balance = balance
        self.cond = threading.Condition()
        self._flag = False                        #定义代表是否已经存钱的旗帜
    
    def getBalance(self):
        #因为账户余额不允许随便修改,所以只为self._balance提供getter方法
        return self._balance
    
    def draw(self,draw_amount):
        #加锁操作  Condition类对象加锁操作,
        #Condition类对象的加锁方法 相当于调用 Condition 绑定的Lock的acquire()
        self.cond.acquire()
        try:
            if not self._flag:
                #Condition类对象阻塞操作,将当前线程放入等待池 等待通知
                self.cond.wait()                  #如果没有存钱,则将取钱方法阻塞 并放入等待池等待通知
            else:
                print(threading.current_thread().name + '取钱:' + str(draw_amount))
                self._balance -= draw_amount      #取钱 并 余额变动
                print('账户余额为:' + str(self._balance))
                self._flag = False                #将标识账户是否已有存款的旗帜设为 False
                #Condition类对象唤醒操作, 唤醒等待池中的其他所有线程 并 通知他们
                self.cond.notify_all()            #唤醒等待池中的其他所有线程 并 通知他们
        #解锁操作  Condition类对象解锁操作
        finally:
            self.cond.release()
            
    def deposit(self, deposit_amount):
        #加锁操作  Condition类对象加锁操作,
        #Condition类对象的加锁方法 相当于调用 Condition 绑定的Lock的acquire()        self.cond.acquire()
        self.cond.acquire()
        try:
            if self._flag:
                #Condition类对象阻塞操作,将当前线程放入等待池 等待通知
                self.cond.wait()                  #如果已经存钱,则将存钱方法阻塞 并放入等待吃等待通知
            else:
                print(threading.current_thread().name + '存款:' + str(deposit_amount))
                self._balance += deposit_amount   #存钱 并 余额变动
                print('账户余额为:' + str(self._balance))
                self._flag =  True                #将标识账户是否已有存款的旗帜设为 True
                #Condition类对象唤醒操作, 唤醒等待池中的其他所有线程 并 通知他们
                self.cond.notify_all()            #唤醒等待池中的其他所有线程 并 通知他们
        #解锁操作  Condition类对象解锁操作
        finally:
            self.cond.release()


'''
程序使用 Condition 的 wait() 和 notify_all() 方法进行控制,
对存款者线程而言,当程序进入 deposit() 方法后,如果 self._flag 为 True,则表明账户中已有存款,
程序调用 Condition 的 wait() 方法被阻塞;否则,程序向下执行存款操作,
当存款操作执行完成后,系统将 self._flag 设为 True,然后调用 notify_all() 来唤醒其他被阻塞的线程。

如果系统中有存款者线程,存款者线程也会被唤醒,但该存款者线程执行到 ① 号代码处时再次进入阻塞状态,
只有执行 draw() 方法的取钱者线程才可以向下执行。
同理,取钱者线程的运行流程也是如此
'''

#定义一个函数,作为线程的target参数执行函数。  模拟重复max次执行取钱操作
def draw_many(account, draw_amount,max):
    for i in range(max):
        account.draw(draw_amount)

#定义一个函数,作为线程的target参数执行函数。  模拟重复max次执行存钱操作
def deposit_many(account,deposit_amount,max):
    for i in range(max):
        account.deposit(deposit_amount)
        

acct=Account('123456789',0)                        #实例化创建一个账户
threading.Thread(name='取钱者 X ', target=draw_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 甲 ', target=deposit_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 乙 ', target=deposit_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 丙 ', target=deposit_many, args=(acct,800,100)).start()




####################
#queue队列模块
#掌握Queue阻塞队列的特性后,利用Queue来实现线程通信

import queue
help(queue)
queue.__doc__
queue.__all__
dir(queue)

#queue模块的三个队列类的简单介绍如下:
#queue.Queue(maxsize=0):       代表 FIFO(先进先出)的常规队列,maxsize 可以限制队列的大小。如果队列的大小达到队列的上限,就会加锁,再次加入元素时就会被阻塞,直到队列中的元素被消费。如果将 maxsize 设置为 0 或负数,则该队列的大小就是无限制的。
#queue.LifoQueue(maxsize=0):   代表 LIFO(后进先出)的队列,与 Queue 的区别就是出队列的顺序不同。
#PriorityQueue(maxsize=0):     代表优先级队列,优先级最小的元素先出队列。


#这三个队列类的属性和方法基本相同, 它们都提供了如下属性和方法:
#
#Queue.qsize():                返回队列的实际大小,也就是该队列中包含几个元素。
#Queue.empty():                判断队列是否为空。
#Queue.full():                 判断队列是否已满。
#
#Queue.put(item, block=True, timeout=None):向队列中放入元素。
#如果队列己满,且 block 参数为 True(阻塞),当前线程被阻塞,
#timeout 指定阻塞时间,如果将 timeout 设置为 None,则代表一直阻塞,直到该队列的元素被消费;
#如果队列己满,且 block 参数为 False(不阻塞),则直接引发 queue.FULL 异常。
#
#Queue.put_nowait(item):       向队列中放入元素,不阻塞。相当于在上一个方法中将 block 参数设置为 False。
#
#Queue.get(item, block=True, timeout=None):从队列中取出元素(消费元素)。
#如果队列已满,且 block 参数为 True(阻塞),当前线程被阻塞,
#timeout 指定阻塞时间,如果将 timeout 设置为 None,则代表一直阻塞,直到有元素被放入队列中; 
#如果队列己空,且 block 参数为 False(不阻塞),则直接引发 queue.EMPTY 异常。
#
#Queue.get_nowait(item):       从队列中取出元素,不阻塞。相当于在上一个方法中将 block 参数设置为 False。




#示例  阻塞队列线程
import queue

#先定义了一个大小为 2 的 Queue,程序先向该队列中放入两个元素,此时队列还没有满,两个元素都可以被放入。
#当程序试图放入第三个元素时,如果使用 put() 方法尝试放入元素将会阻塞线程
bq=queue.Queue(2)              #实例化创建一个长度为2的先进先出常规队列
bq.put('Python')
bq.put('python')
print('11111111')
#bq.put('Python')               #阻塞线程
print('22222222')



#示例  掌握Queue阻塞队列的特性后,利用Queue来实现线程通信
import threading
import time
import queue

def product(bq):
    #定义函数 用于线程的target参数执行函数
    #参数Queue队列对象,函数内实现队列的放入元素功能
    str_tuple=('Python','Kotlin','Swift')
    for i in range(9):
        print(threading.current_thread().name + '生产者准备生产元组元素...')
        time.sleep(0.2)
        #Queue.put()方法 向队列中放入元素  如果队列已满则阻塞,直到元素被Queue.get()方法消费
        bq.put(str_tuple[i % 3])        #%求余操作
        print(threading.current_thread().name + '生产者生产元组元素完成!')

def consume(bq):
    #定义函数 用于线程target参数执行函数
    #参数Queue队列对象,函数内实现队列的取出元素消费功能
    while True:
        print(threading.current_thread().name + '消费者准备消费元组元素...')
        time.sleep(0.2)
        #Queue.get()方法 从队列中取出元素  如果队列已空则阻塞,直到元素被Queue.put()方法存入
        t=bq.get()
        print(threading.current_thread().name + '消费者消费 [%s] 元素完成!!!'%t)

#实例化创建一个容量为1的常规队列  先进先出  用作线程target参数执行函数的参数。
bq=queue.Queue(maxsize=1)      #创建一个容量为1的队列

#启动3个生产者 线程
threading.Thread(target=product, args=(bq,)).start()
threading.Thread(target=product, args=(bq,)).start()
threading.Thread(target=product, args=(bq,)).start()

#Queue 队列的大小为 1,因此三个生产者线程无法连续放入元素,必须等待消费者线程取出一个元素后,其中的一个生产者线程才能放入一个元素。
#三个生产者线程都想向 Queue 中放入元素,但只要其中一个生产者线程向该队列中放入元素之后,其他生产者线程就必须等待,等待消费者线程取出 Queue 队列中的元素。

#启动1个消费 线程
threading.Thread(target=consume, args=(bq,)).start()






####################
#Event类 来自threading线程模块
#Event类 是一种非常简单的线程通信机制,一个线程发出一个 Event,另一个线程可通过该 Event 被触发。

#Event类 本身管理一个内部旗标,
#程序可以通过 Event 的 set() 方法将该旗标设置为 True,也可以调用 clear() 方法将该旗标设置为 False。
#程序可以调用 wait() 方法来阻塞当前线程,直到 Event 的内部旗标被设置为 True。

import threading
help(threading.Event)
threading.Event.__doc__
dir(threading.Event)

#Event 提供了如下方法:
#is_set():                 该方法返回 Event 的内部旗标是否为True。
#set():                    该方法将会把 Event 的内部旗标设置为 True,并唤醒所有处于等待状态的线程。
#clear():                  该方法将 Event 的内部旗标设置为 False,通常接下来会调用 wait() 方法来阻塞当前线程。
#wait(timeout=None):       该方法会阻塞当前线程。


#示例  Event的简单用法
import threading
import time

event=threading.Event()

def cal(name):
    #定义函数 准备用于线程target参数执行函数
    #函数内实现 等待事件,进入等待阻塞状态
    print('%s 启动\t' % threading.current_thread().getName())
    print('%s 准备开始计算状态' % name)
    event.wait()            #wait方法阻塞当前线程进入等待状态
    
    #收到事件后才能继续进入运行状态
    print('%s 收到通知了\t' % threading.current_thread().getName())
    print('%s 正式开始计算!' % name)

#创建并启动2条线程,他们都会在启动、准备开始计算状态处 阻塞,等待。
threading.Thread(target=cal, args=('甲',)).start()
threading.Thread(target=cal, args=('乙',)).start()
time.sleep(2)
print('--------------------')

#发出事件  直到set方法设置Event内部旗帜为True 并 唤醒等待线程开始继续往下执行
print('主线程发出事件')
event.set()                 #set方法唤醒所有处于等待状态的线程



#示例
#结合 Event 的内部旗标,同样实现前面的 Account 的生产者-消费者效果:
#存钱线程(生产者)存钱之后,必须等取钱线程(消费者)取钱之后才能继续向下执行。

#Event 实际上类似于 Condition 和旗标的结合体,但 Event 本身并不带 Lock 对象,因此如果要实现线程同步,还需要额外的 Lock 对象。

import threading

class Account:
    '''
    定义一个Account 类,提供 draw() 和 deposit() 两个方法,分别对应于该账户的取钱和存款操作。

    
    实现要求存款者和取钱者不断地重复存款、取钱的动作,而且要求每当存款者将钱存入指定账户后,取钱者就立即取出该笔钱。
    不允许存款者连续两次存钱,也不允许取钱者连续两次取钱。
    '''
    
    #定义构造器  封装账户编号、账户余额两个类属性
    def __init__(self,account_no,balance):
        self.accout_no =  account_no
        self._balance = balance
        self.lock = threading.Lock()
        self.event = threading.Event()
    
    #因为账户余额不允许随便修改,所以只为self._balance提供getter方法
    def getBalance(self):
        return self._balance
    
    #定义取钱操作 线程加锁的安全方式
    def draw(self,draw_amount):
        #加锁操作
        self.lock.acquire()
        if self.event.is_set():        #is_set()方法返回 Event 的内部旗标是否为True。
            print(threading.current_thread().name + '取钱:' + str(draw_amount))
            self._balance -= draw_amount
            print('账户余额为:' + str(self._balance))
            
            #clear方法设置Event内部旗帜为False,通常接下来会调用 wait() 方法来阻塞当前线程。
            self.event.clear()         #clear方法设置Event内部旗帜为False,通常接下来会调用 wait() 方法来阻塞当前线程。
            self.lock.release()        #解锁操作
            self.event.wait()          #阻塞当前线程 进入等待状态
        else:
            self.lock.release()        #解锁操作
            self.event.wait()          #阻塞当前线程 进入等待状态
    
    #定义存钱操作 线程加锁的安全方式
    def deposit(self,deposit_amount):
        #加锁操作
        self.lock.acquire()
        if not self.event.is_set():
            print(threading.current_thread().name + '存款:' + str(deposit_amount))
            self._balance += deposit_amount
            print('账户余额为:' + str(self._balance))
            
            #set方法将内部旗标设置为 True,并唤醒所有处于等待状态的线程
            self.event.set()
            self.lock.release()        #解锁操作
            self.event.wait()          #阻塞当前线程 进入等待状态
        else:
            self.lock.release()        #解锁操作
            self.event.wait()          #阻塞当前线程 进入等待状态
        
        
#定义一个函数,模拟重复max次执行取钱操作
def draw_many(account, draw_amount,max):
    for i in range(max):
        account.draw(draw_amount)

#定义一个函数,模拟重复max次执行存钱操作
def deposit_many(account,deposit_amount,max):
    for i in range(max):
        account.deposit(deposit_amount)
        

acct=Account('123456789',0)                        #实例化创建一个账户
threading.Thread(name='取钱者 X ', target=draw_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 甲 ', target=deposit_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 乙 ', target=deposit_many, args=(acct,800,100)).start()
threading.Thread(name='存款者 丙 ', target=deposit_many, args=(acct,800,100)).start()











# =============================================================================
# #线程池    ThreadPoolExecutor类
# #线程池的基类是 concurrent.futures 模块中的 Executor,
# #Executor 提供了两个子类,即 ThreadPoolExecutor 和 ProcessPoolExecutor,
# #其中 ThreadPoolExecutor类 用于创建线程池,而 ProcessPoolExecutor类 用于创建进程池。
# =============================================================================
'''
注意:
1、submit()线程池类方法提交线程池后返回的Future对象,用result()方法来获取线程任务的运回值,
但该方法会阻塞当前主线程,只有等到钱程任务完成后,result() 方法的阻塞才会被解除。
可通过 Future 的 add_done_callback() 方法来添加回调函数来避免直接调用result()方法阻塞线程
另外线程池类方法map()后可循环迭代每一个线程
'''

from concurrent.futures import ThreadPoolExecutor

help(ThreadPoolExecutor)
ThreadPoolExecutor.__doc__
dir(ThreadPoolExecutor)


#Exectuor类提供了如下常用方法:
#1、submit(fn, *args, **kwargs): 将 fn 函数提交给线程池。*args 代表传给 fn 函数的参数,*kwargs 代表以关键字参数的形式为 fn 函数传入参数。
#2、map(func, *iterables, timeout=None, chunksize=1):该函数类似于全局函数 map(func, *iterables),
#只是该函数将会启动多个线程,以异步方式立即对 iterables 执行 map 处理。
#3、shutdown(wait=True):         关闭线程池


#程序将 fn 函数提交(submit)给线程池后,submit 方法会返回一个 Future 对象,
#Future类主要用于获取线程任务函数的返回值。
#由于线程任务会在新线程中以异步方式执行,因此,线程执行的函数相当于一个“将来完成”的任务,所以 Python 使用 Future 来代表。


#Future对象提供了如下方法:
#1、cancel():                  取消该 Future 代表的线程任务。如果该任务正在执行,不可取消,则该方法返回 False;否则,程序会取消该任务,并返回 True。
#2、cancelled():               返回 Future 代表的线程任务是否被成功取消。
#3、running():                 如果该 Future 代表的线程任务正在执行、不可被取消,该方法返回 True。
#4、done():                    如果该 Funture 代表的线程任务被成功取消或执行完成,则该方法返回 True。
#5、result(timeout=None):      获取该 Future 代表的线程任务最后返回的结果。
#如果 Future 代表的线程任务还未完成,该方法将会阻塞当前线程,
#其中 timeout 参数指定最多阻塞多少秒。
#6、exception(timeout=None):   获取该 Future 代表的线程任务所引发的异常。如果该任务成功完成,没有异常,则该方法返回 None。
#7、add_done_callback(fn):     为该 Future 代表的线程任务注册一个“回调函数”,当该任务成功完成时,程序会自动触发该 fn 函数。

'''
使用线程池来执行线程任务的步骤如下:
1、调用 ThreadPoolExecutor 类的构造器创建一个线程池。
2、定义一个普通函数作为线程任务。
3、调用 ThreadPoolExecutor 对象的 submit() 方法来提交线程任务。
4、当不想提交任何任务时,调用 ThreadPoolExecutor 对象的 shutdown() 方法来关闭线程池。
'''


###################
#示例
#示例如何使用线程池来执行线程任务
from concurrent.futures import ThreadPoolExecutor
import threading
import time

def action(max):
    #定义一个函数  准备用作线程池fn参数函数
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))
        my_sum += 1
    return my_sum

#实例化创建线程池  参数max_workers指定工作线程数量为2条
pool=ThreadPoolExecutor(max_workers=2)        #创建一个包含2条线程的线程池

#类方法submit()  将fn参数函数提交给线程池
future1=pool.submit(action,50)                #向线程池提交一个fn参数函数action,50为函数参数
future2=pool.submit(action,100)               #继续向线程池提交一个fn参数函数action,100为函数参数

type(future1)                                 #查看 实例化提交线程池以后的类型

#类方法done()  判断提交线程池后的future对象 即线程任务 是否运行完成
time.sleep(1)
print(future1.done())                         #输出future对象是否运行完毕
time.sleep(1)
print(future2.done())                         #输出future对象是否运行完毕

#类方法result()  返回提交线程池后的future对象 即线程任务 的程序运行结果  
#注意提交线程池后返回的Future对象,用result()方法来获取线程任务的运回值,
#但该方法会阻塞当前主线程,只有等到钱程任务完成后,result() 方法的阻塞才会被解除。
#可通过 Future 的 add_done_callback() 方法来添加回调函数来避免直接调用result()方法阻塞线程
print(future1.result())                       #输出future对象的程序运行结果
print(future2.result())                       #输出future对象的程序运行结果

#类方法shutdown()  关闭线程池
pool.shutdown()                               #线程池关闭



####################
#示例   add_done_callback()  提交线程池后的Future对象方法  添加回调函数
#通过 Future 的 add_done_callback() 方法来添加回调函数来避免直接调用result()方法阻塞线程
from concurrent.futures import ThreadPoolExecutor
import threading
import time

def action(max):
    #定义一个函数  准备用作线程池fn参数函数    
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))
        my_sum += i 
    return my_sum


with ThreadPoolExecutor(max_workers=2) as pool:
    future1 = pool.submit(action,50)
    future2 = pool.submit(action,100)
    
    def get_result(future):
        print(future.result())
        
    future1.add_done_callback(get_result)
    future2.add_done_callback(get_result)
    print('--------------------')
    
'''
由于程序并未直接调用 future1、future2 的 result() 方法,因此主线程不会被阻塞,
可以立即看到输出主线程打印出的横线。
接下来将会看到两个新线程并发执行,当线程任务执行完成后,get_result() 函数被触发,输出线程任务的返回值。
'''



####################
#示例  线程池类方法 map(func, *iterables, timeout=None, chunksize=1)
#该方法的功能类似于全局函数 map(),区别在于线程池的 map() 方法会为 iterables 的每个元素启动一个线程,以并发方式来执行 func 函数。
#这种方式相当于启动 len(iterables) 个线程,井收集每个线程的执行结果。
from concurrent.futures import ThreadPoolExecutor
import threading
import time

def action(max):
    #定义一个函数  准备用作线程池fn参数函数    
    my_sum = 0
    for i in range(max):
        print(threading.current_thread().name + ' ' + str(i))
        my_sum += i 
    return my_sum

with ThreadPoolExecutor(max_workers=4) as pool:
    results = pool.map(action,(50,100,150))           #启动3个线程
    print('---------------')
    for r in results:                                 #for循环迭代每个线程及结果
        print(r)











# =============================================================================
# #线程局部变量
# #线程局部变量为每一个使用该变量的线程都提供一个变量的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。
# #从线程的角度看,就好像每一个线程都完全拥有该变量一样。
# =============================================================================

#示例  threading.local()来赋值声明线程局部变量
#作用好像每个线程都完成拥有该变量亦一样
from concurrent.futures import ThreadPoolExecutor
import threading

mydata = threading.local()        #定义线程局部变量

def action(max):
    for i in range(max):
        try:
            mydata.x += i
        except:
            mydata.x = i
        
        print('%s mydata.x 的值为: %d' %(threading.current_thread().name, mydata.x))
    

with ThreadPoolExecutor(max_workers=2) as pool:
    pool.submit(action,10)
    pool.submit(action,10)

'''
程序中作为线程执行体的 action 函数使用 mydata.x 记录 0~10 的累加值,
如果两个线程共享同一个 mydata 变量,将会看到 mydata.x 最后会累加到 90(0~9 的累加值是 45,但两次累加会得到 90)。
但由于 mydata 是 threading.local 变量,因此程序会为每个线程都创建一个该变量的副本,所以将会看到两个线程的 mydata.x 最后都累加到 45。
'''











# =============================================================================
# #线程定时器
# #线程定时器是线程类threading类的Timer子类,该子类可用于控制指定函数在特定时间内执行一次
# =============================================================================
'''
注意:
#Timer定时器只能控制函数在指定时间内执行一次,
#如果要使用 Timer 控制函数多次重复执行,则需要函数内嵌套定时器 来循环执行下一次调度。
'''

from threading import Timer
help(Timer)
Timer.__doc__
dir(Timer)                      


###################
#示例  定时器Timer 控制 10s 后执行 hello 函数。
from threading import Timer

def hello():
    print('hello,world')

#创建一个定时器线程
t=Timer(10,hello)               #定时10秒以后执行hello函数
t.start()                       #定时器线程启动




###################
#示例  循环调用定时器
#Timer定时器只能控制函数在指定时间内执行一次,
#如果要使用 Timer 控制函数多次重复执行,则需要函数内嵌套定时器 来循环执行下一次调度。
from threading import Timer
import time

count=0 
def print_time():
    '''
    定义函数,用于Timer()定时器中参数函数
    函数中嵌套Timer()定时器,可循环判断,如果count<10,则循环调用10次定时器输出当前时间
    '''
    print('当前时间:%s' % time.ctime())
    global t,count
    count+=1
    if count<10:
        t=Timer(1, print_time)
        t.start()

t = Timer(1,print_time)
t.start()











# =============================================================================
# #sched模块  任务调度模块
# #sched模块  可以比 Timer线程定时器 执行更复杂的任务调度
# ##sched模块提供了 sched.scheduler 类,该类代表一个任务调度器。
# =============================================================================
'''
使用:
1、实例化任务调度器 sched.scheduler()
2、实例化任务调度器方法:绝对时间执行函数任务、相对时间执行函数任务、取消任务等
'''


import sched
help(sched)
sched.__doc__
sched.__all__                 #只有一个方法 scheduler() 定义线程调度器
dir(sched)


#sched.scheduler(timefunc=time.monotonic, delayfunc=time.sleep) 构造器支持两个参数:
#1、timefunc:        该参数指定生成时间戳的时间函数,默认使用 time.monotonic 来生成时间戳。
#2、delayfunc:       该参数指定阻塞程序的函数,默认使用 time.sleep 函数来阻塞程序。

help(sched.scheduler)
dir(sched.scheduler)


help(sched.scheduler.enterabs)       #绝对时间执行函数任务
help(sched.scheduler.enter)          #相对时间执行函数任务
help(sched.scheduler.run)            #运行所有需要调度的任务
help(sched.scheduler.queue)          #返回调度器的调度队列,根据任务优先级


#sched.scheduler 调度器支持如下常用属性和方法:

#1、scheduler.enterabs(time, priority, action, argument=(), kwargs={}):
#指定在 time 时间点执行 action 函数,argument 和 kwargs 都用于向 action 函数传入参数,其中 argument 使用位置参数的形式传入参数,kwargs 使用关键字参数的形式传入参数。
#该方法返回一个 event,它可作为 cancel() 方法的参数用于取消该调度。
#priority 参数指定该任务的优先级,当在同一个时间点有多个任务需要执行时,优先级高(值越小代表优先级越高)的任务会优先执行。

#2、scheduler.enter(delay, priority, action, argument=(),kwargs={}):
#该方法与上一个方法基本相同,只是 delay 参数用于指定多少秒之后执行 action 任务。

#3、scheduler.cancel(event):         取消任务。如果传入的 event 参数不是当前调度队列中的 event,程序将会引发 ValueError 异常。
#4、scheduler.empty():               判断当前该调度器的调度队列是否为空。

#5、scheduler.run(blocking=True):    运行所有需要调度的任务。
#如果调用该方法的 blocking 参数为 True,该方法将会阻塞线程,直到所有被调度的任务都执行完成。

#6、scheduler.queue:                 该只读属性返回该调度器的调度队列。



###################
#示例     sched.scheduler执行任务调度
import sched
import time
import threading

def print_time(name='default'):
    print('%s的时间:%s' % (name,time.ctime()))

print('主线程开始时间:',time.ctime())

#实例化创建 线程调度器
s=sched.scheduler()

#enter()方法  相对时间  任务调度
s.enter(10,1,print_time)                               #参数1指定10秒后开始运行,参数2位运行优先等级为1,参数3为调度运行函数
s.enter(5,2,print_time, argument=('位置参数',))        #参数1指定10秒后开始运行,参数2位运行优先等级为1,参数3为调度运行函数,参数4为参数3函数的元组参数
s.enter(5,1,print_time, kwargs={'name':'字典参数'})    #参数1指定10秒后开始运行,参数2位运行优先等级为1,参数3为调度运行函数,参数3为参数3函数的字典参数

#run()方法  运行所有需要调度的任务
s.run()
print('主线程结束时间:',time.ctime())











# =============================================================================
# #multiprocessding模块  进程封装
# =============================================================================
'''
注意:
1、multiprocessing.Process类通过实例化对象指定target参数执行函数 在IDLE里不显示函数执行内容。
通过在控制台通过执行文件可显示target参数执行函数的内容。
2、通过 multiprocessing.Process 来创建并启动进程时,程序必须先判断if __name__=='__main__':,否则可能引发异常。
'''


#探索模块
import multiprocessing

help(multiprocessing)
multiprocessing.__doc__
multiprocessing.__all__
dir(multiprocessing)


####################
#multiprocessing.Process类  进程创建

#Python 在 multiprocessing模块下提供了 Process 来创建新进程。
#与 Thread 类似的是,使用 Process 创建新进程也有两种方式:
#1、使用 multiprocessing模块 中的 Process类 来实例化对象创建新进程,并指定函数作为target参数函数
#2、继承 Process类 ,并重写他的 run() 方法来创建进程类,程序创建Process子类的实例作为进程

import multiprocessing

help(multiprocessing.Process)
dir(multiprocessing.Process)

#Process 类也有如下类似的方法和属性:
#run():                  重写该方法可实现进程的执行体。
#start():                该方法用于启动进程。
#join([timeout]):        该方法类似于线程的 join() 方法,当前进程必须等待被 join 的进程执行完成才能向下执行。
#name:                   该属性用于设置或访问进程的名字。
#is_alive():             判断进程是否还活着。
#daemon:                 该属性用于判断或设置进程的后台状态。
#pid:                    返回进程的 ID。
#authkey:                返回进程的授权 key。
#terminate():            中断该进程。

help(multiprocessing.Process.start)
help(multiprocessing.Process.run)
help(multiprocessing.Process.daemon)
help(multiprocessing.Process.join)



#示例    通过实例化创建进程    指定target参数执行函数
#注意:
#1、multiprocessing.Process类通过实例化对象指定target参数执行函数 在IDLE里不显示函数执行内容。
#通过在控制台通过执行文件可显示target参数执行函数的内容。
import multiprocessing
import os

def action(max):
    for i in range(max):
        print('(%s)子进程  (父进程:(%s)):%d' %(os.getpid(), os.getppid(), i))

if __name__ == '__main__':
    for i in range(100):
        print('(%s)主进程:%d' %(os.getpid(), i))
        if i == 20:
            np1=multiprocessing.Process(target=action, args=(100,))
            np1.start()
            
            np2=multiprocessing.Process(target=action, args=(100,))
            np2.start()
            np2.join()
    print('主进程执行完毕')
    


#示例    继承Process类来创建子进程
#注意:
#1、multiprocessing.Process类通过实例化对象指定target参数执行函数 在IDLE里不显示函数执行内容。
#通过在控制台通过执行文件可显示target参数执行函数的内容。
import multiprocessing
import os

class MyProcess(multiprocessing.Process):
    def __init__(self,max):
        self.max=max
        super().__init__()
    #重写run方法作为进程执行体
    def run(self):
        for i in range(self.max):
            print('(%s)子进程(父进程:(%s)):%d' %(os.getpid(), os.getppid(), i))
    
if __name__ == '__main__':
    for i in range(100):
        print('(%s)主进程:%d' %(os.getpid(),i))
        if i == 20:
            mp1=MyProcess(100)
            mp1.start()
            mp2=MyProcess(100)
            mp2.start()
            mp2.join()
    print('主进程执行完毕!')











# =============================================================================
# #进程启动的三种方式  spawn(windows仅支持此方式)、fork、forkserver
# #spawn:      父进程启动一个全新的python解释器(windows平台仅支持此方式启动进程)
# #fork:        父进程使用 os.fork() 来启动一个 Python 解释器进程。
# #forkserver:  如果使用这种方式来启动进程,程序将会启动一个服务器进程。
# =============================================================================
'''
注意:
1、multiprocessing.set_start_method(' ')函数用于设置启动进程的方式,设置代码必须放在多进程代码之前。
2、window只支持spawn方式启动进程
'''

#示例
import multiprocessing
import os

#help(multiprocessing.set_start_method)

def foo(q):
    '''参数q为一个队列,可在队列内存取元素'''
    print('被启动的新进程: (%s)' % os.getpid())
    q.put('Python')
    
if __name__ == '__main__':
    #设置使用fork方式启动进程
    multiprocessing.set_start_method('spawn')

    #创建一个进程的队列
    q = multiprocessing.Queue()

    #创建并启动子进程
    mp = multiprocessing.Process(target=foo, args=(q, ))
    mp.start()
    
    #获取队列中的消息
    print(q.get())
    mp.join()










# =============================================================================
# #进程池    multiprocessing.Pool类
# #通过 multiprocessing 模块的 Pool类来实例化创建进程池,然后通过进程池方法提交进程池、关闭进程池、等待进程池等
# =============================================================================

import multiprocessing 

help(multiprocessing.Pool)
multiprocessing.Pool.__doc__
dir(multiprocessing.Pool)


#进程池具有如下常用方法:

#1、apply(func[, args[, kwds]]):          将 func 函数提交给进程池处理。其中 args 代表传给 func 的位置参数,kwds 代表传给 func 的关键字参数。
#该方法会被阻塞直到 func 函数执行完成。
#2、apply_async(func[, args[, kwds[, callback[, error_callback]]]]):
#这是 apply() 方法的异步版本,该方法不会被阻塞。其中 callback 指定 func 函数完成后的回调函数,error_callback 指定 func 函数出错后的回调函数。

#3、map(func, iterable[, chunksize]):     类似于 Python 的 map() 全局函数,只不过此处使用新进程对 iterable 的每一个元素执行 func 函数。
#4、map_async(func, iterable[, chunksize[, callback[, error_callback]]]):
#这是 map() 方法的异步版本,该方法不会被阻塞。其中 callback 指定 func 函数完成后的回调函数,error_callback 指定 func 函数出错后的回调函数。

#5、imap(func, iterable[, chunksize]):    这是 map() 方法的延迟版本。
#6、imap_unordered(func, iterable[, chunksize]):
#功能类似于 imap() 方法,但该方法不能保证所生成的结果(包含多个元素)与原 iterable 中的元素顺序一致。

#7、starmap(func, iterable[,chunksize]):  功能类似于 map() 方法,但该方法要求 iterable 的元素也是 iterable 对象,程序会将每一个元素解包之后作为 func 函数的参数。

#8、close():                              关闭进程池。在调用该方法之后,该进程池不能再接收新任务,它会把当前进程池中的所有任务执行完成后再关闭自己。
#9、terminate():                          立即中止进程池。
#10、join():                              等待所有进程完成。


#示例    实例化创建进程池
#使用 apply_async() 方法启动进程:
import multiprocessing
import time
import os

#定义一个函数,准备用作进程池func参数函数
def action(name='default'):
    print('(%s)进程正在执行,参数为:%s'%(os.getpid(), name))
    time.sleep(3)

#通过 multiprocessing.Process 来创建并启动进程时,程序必须先判断if __name__=='__main__':,否则可能引发异常。
if __name__ == '__main__':
    #实例化创建进程池
    pool=multiprocessing.Pool(processes=4)
    #异步提交进程池
    pool.apply_async(action)
    pool.apply_async(action,args=('位置参数',))
    pool.apply_async(action,kwds={'name':'字典参数'})
    #关闭进程池,不再接受新任务,会等待进程池中所有任务完成后关闭
    pool.close()
    #等待所有进程完成
    pool.join()




#示例    实例化创建进程池  通过上下文管理协议创建
#使用map()方法来启动进程
import multiprocessing
import time
import os

#定义一个函数,准备用作进程池func参数函数
def action(max):
    my_sum=0
    for i in range(max):
        print('(%s)进程正在执行:%d'%(os.getpid(), i))
        my_sum+=i
    return my_sum

#通过 multiprocessing.Process 来创建并启动进程时,程序必须先判断if __name__=='__main__':,否则可能引发异常。
if __name__ == '__main__':
    #上下文管理器创建进程池  进程数量为4
    with multiprocessing.Pool(processes=4) as pool:
        #提交并启动三个进程
        results=pool.map(action,(50,100,150))
        print('-----------------')
        #for循环迭代每个进程及结果
        for r in results:             
            print(r)











# =============================================================================
# #进程通信    multiprocessing.Queue类  和 multiprocessing.Pipe类
#            
# #Python 为进程通信提供了两种机制:
# #Queue:一个进程向 Queue 中放入数据,另一个进程从 Queue 中读取数据。
# #Pipe:Pipe 代表连接两个进程的管道。程序在调用 Pipe() 函数时会产生两个连接端,分别交给通信的两个进程,接下来进程既可从该连接端读取数据,也可向该连接端写入数据。
# =============================================================================
'''
注意:
进程通信中,使用实例化创建multiprocessing.Pipe类管道对象返回两个PipeConnection对象,
其中前一个PipeConnection对象用于接受数据,后一个PipeConnection对象用于发送数据
'''



import multiprocessing

help(multiprocessing.Queue)
help(multiprocessing.Pipe)



##########
#Queue:一个进程向 Queue 中放入数据,另一个进程从 Queue 中读取数据。
dir(multiprocessing.Queue)
#multiprocessing 模块下的 Queue 和 queue 模块下的 Queue 基本类似,
#它们都提供了 qsize()、empty()、full()、put()、put_nowait()、get()、get_nowait() 等方法。
#区别只是 multiprocessing 模块下的 Queue 为进程提供服务,而 queue 模块下的 Queue 为线程提供服务。


#示例    使用multiprocessing.Queue类来实现进程之间的通信
import multiprocessing

#
def f(q):
    '''
    创建一个函数,用作进程target参数的执行函数
    参数q为一个实例化创建的multiprocessing.Queue类对象
    '''
    print('(%s)进程开始放入数据...'%multiprocessing.current_process().pid)
    #向队列中放入元素
    q.put('血皇敖天')

if __name__ == "__main__":
    #实例化创建进程通信的队列
    q=multiprocessing.Queue()
    #实例化创建并启动子进程运行函数
    p=multiprocessing.Process(target=f, args=(q,))
    p.start()
    print('(%s)进程开始取出数据...'%multiprocessing.current_process().pid)
    #从进程通信的队列中取出元素
    print(q.get())
    #插入实现子进程
    p.join()




##########
#Pipe:Pipe 代表连接两个进程的管道。程序在调用 Pipe() 函数时会产生两个连接端,分别交给通信的两个进程,接下来进程既可从该连接端读取数据,也可向该连接端写入数据。

dir(multiprocessing.Pipe)
#使用 Pipe 实现进程通信,程序会调用 multiprocessing.Pipe() 函数来创建一个管道,
#该函数会返回两个 PipeConnection 对象,代表管道的两个连接端(一个管道有两个连接端,分别用于连接通信的两个进程)。


#PipeConnection 对象包含如下常用方法:

#1、send(obj):          发送一个 obj 给管道的另一端,另一端使用 recv() 方法接收。需要说明的是,该 obj 必须是可 picklable 的(Python 的序列化机制),如果该对象序列化之后超过 32MB,则很可能会引发 ValueError 异常。
#2、recv():             接收另一端通过 send() 方法发送过来的数据。
#3、fileno():           关于连接所使用的文件描述器。
#4、close():            关闭连接。
#5、poll([timeout]):    返回连接中是否还有数据可以读取。

#6、send_bytes(buffer[, offset[, size]]):
#发送字节数据。如果没有指定 offset、size 参数,则默认发送 buffer 字节串的全部数据;如果指定了 offset 和 size 参数,则只发送 buffer 字节串中从 offset 开始、长度为 size 的字节数据。通过该方法发送的数据,应该使用 recv_bytes() 或 recv_bytes_into 方法接收。

#7、recv_bytes([maxlength]):  
#接收通过 send_bytes() 方法发迭的数据,maxlength 指定最多接收的字节数。该方法返回接收到的字节数据。

#8、recv_bytes_into(buffer[, offset]):
#功能与 recv_bytes() 方法类似,只是该方法将接收到的数据放在 buffer 中。


#示例    使用multiprocessing.Pipe类来实现进程之间的通信
import multiprocessing

def f(conn):
    '''
    创建一个函数,用作进程target参数的执行函数
    参数conn为实例化创建的multiprocessing.Pipe类管道对象返回的两个PipeConnection对象中的后一个用于发送数据的对象
    '''
    print('(%s)进程开始发送数据...'%multiprocessing.current_process().pid)
    conn.send('血皇敖天')

if __name__ =='__main__':
    #实例化创建进程通信的pipe管道
    #返回两个PipeConnection对象,前一个用于接收数据,后一个用于发送数据
    parent_conn, child_conn = multiprocessing.Pipe()
    #实例化创建并启动子进程 来执行函数 从而发送管道数据
    p=multiprocessing.Process(target=f, args=(child_conn,))
    p.start()
    print('(%s)进程开始接收数据...'%multiprocessing.current_process().pid)
    #前一个生成返回的实例化对象用于接受数据
    print(parent_conn.recv())
    p.join()












# =============================================================================
# #并发及异步并发爬虫
# #concurrent.futures.ThreadPoolExecutor     线程池类、
# #concurrent.futures.ProcessPoolExecutor    进程池类
# #asyncio模块   异步并发模块
# #aiohttp模块   异步的requests
# =============================================================================
'''
Py并发编程Futures并发爬虫.py
Py并发编程asyncio模块异步并发爬虫.py


深入:
asyncio模块   异步并发模块
aiohttp模块   异步的requests
'''



#示例    单线程简易爬虫
import requests
import time

def download_one(url):
    resp = requests.get(url)
    print('Read {} from {}'.format(len(resp.content), url))
   
def download_all(sites):
    for site in sites:
        download_one(site)
def main():
    sites = [
        'http://c.biancheng.net',
        'http://c.biancheng.net/c',
        'http://c.biancheng.net/python'
    ]
    start_time = time.perf_counter()
    download_all(sites)
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
   
if __name__ == '__main__':
    main()



####################
#示例    多线程/多进程 简易爬虫
import concurrent.futures
import requests
import threading
import time

def download_one(url):
    resp = requests.get(url)
    print('Read {} from {}'.format(len(resp.content), url))
    
def download_all(sites):
#    '''多线程版本'''
#    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
#        executor.map(download_one, sites)
    
    '''多进程版本'''
    # ProcessPoolExecutor() 表示类实例化创建进程池,使用多个进程并行的执行程序。
    #通常省略参数 workers,因为系统会自动返回 CPU 的数量作为可以调用的进程数。
    with concurrent.futures.ProcessPoolExecutor() as executor:
        executor.map(download_one, sites)

def main():
    sites = [
        'http://c.biancheng.net',
        'http://c.biancheng.net/c',
        'http://c.biancheng.net/python'
    ]
    start_time = time.perf_counter()
    download_all(sites)
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
    
if __name__ == '__main__':
    main()



####################
#示例    多线程迭代器  简易爬虫
import concurrent.futures
import requests
import time

def download_one(url):
    resp = requests.get(url)
    print('Read {} from {}'.format(len(resp.content), url))

def download_all(sites):
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        to_do = []
        for site in sites:
            future = executor.submit(download_one, site)
            to_do.append(future)
        #as_completed(fs)  针对给定的 future 迭代器 fs,在其完成后返回完成后的迭代器。
        for future in concurrent.futures.as_completed(to_do):
            future.result()

def main():
    sites = [
        'http://c.biancheng.net',
        'http://c.biancheng.net/c',
        'http://c.biancheng.net/python'
    ]
    start_time = time.perf_counter()
    download_all(sites)
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))

if __name__ == '__main__':
    main()


####################
'''
异步并发爬虫 需后期深入:
asyncio模块   异步并发模块
aiohttp模块   异步的requests
'''










# =============================================================================
# #GIL 全局解释器锁
# #GIL 限制了 Python 多线程的性能,其本质上类似操作系统的 Mutex。
# #GIL 的功能是:在 CPython 解释器中执行的每一个 Python 线程,都会先锁住自己,以阻止别的线程执行。
# =============================================================================
'''
#注意:
GIL 不能绝对保证线程安全
因为即便 GIL 仅允许一个 Python 线程执行,但Python 还有 check interval 这样的抢占机制。
'''

#示例    单线程 测速
import time
start = time.time()
def CountDown(n):
    while n > 0:
        n -= 1
CountDown(100000)
print("Time used:",(time.time() - start))


#示例    多线程 是否会加速
#结果显示并 比单线程计算 没有 提速;缘由是CPython 解释器存在Gil全局解释器锁,
#GIL 的功能是:在 CPython 解释器中执行的每一个 Python 线程,都会先锁住自己,以阻止别的线程执行。
import time
from threading import Thread
start = time.time()
def CountDown(n):
    while n > 0:
        n -= 1
t1 = Thread(target=CountDown, args=[100000 // 2])
t2 = Thread(target=CountDown, args=[100000 // 2])
t1.start()
t2.start()
t1.join()
t2.join()
print("Time used:",(time.time() - start))


#CPython 使用引用计数来管理内容    即: 垃圾回收机制
#所有 Python 脚本中创建的实例,都会配备一个引用计数,来记录有多少个指针来指向它。
#当实例的引用计数的值为 0 时,会自动释放其所占的内存。

#示例
import sys

a=[]
b=a
print(sys.getrefcount(a))    #输出引用计数值,此处为3



#注意:GIL 不能绝对保证线程安全
#因为即便 GIL 仅允许一个 Python 线程执行,但Python 还有 check interval 这样的抢占机制。

#示例:
import threading

n = 0
def foo():
    global n
    n += 1
    
threads = []
for i in range(100):
    t = threading.Thread(target=foo)
    threads.append(t)

for t in threads:
    t.start()

for t in threads:
    t.join()

print(n)
'''
执行此代码会发现,其大部分时候会打印 100,但有时也会打印 99 或者 98,
原因在于 n+=1 这一句代码让线程并不安全。
'''












# =============================================================================
# #垃圾回收机制    gc模块 手动启动垃圾回收机制
# #垃圾回收机制引用计数机制    
# #即:对象的引用计数值为 0 时,说明这个对象永不再用,自然它就变成了垃圾,需要被回收。
# =============================================================================


import psutil         #获取系统信息模块
import os


help(psutil)
psutil.__doc__
psutil.__all__
dir(psutil)

help(psutil.Process)
psutil.Process.__doc__
dir(psutil.Process)

help(psutil.Process.memory_full_info)
psutil.Process.memory_full_info.__doc__



####################
#示例    通过函数内部局部变量是否垃圾回收 来查看内存占用空间
import os
import psutil         #获取系统信息模块

#显示当前 python 程序占用的内存大小
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024 / 1024
    print('{} memory used: {} MB'.format(hint, memory))

def func():
    show_memory_info('initial')            #输出较小内存占用空间
    #global a                              #如果将局部变量声明成全局变量,则持续占用较大内存空间
    a = [i for i in range(10000000)]       #临时变量占用较大内存空间。  函数运行完毕后即垃圾回收机制清除临时变量
    show_memory_info('after a created')    #输出垃圾回收机制后的较小内存占用空间
    #return a                              #如果函数返回局部变量,则也会持续占用较大内存空间

#输出占用内存空间 
func()
show_memory_info('finished')



####################
#示例    python内部的引用计数机制
import sys

a = []

#sys.getrefcount() 函数用于查看一个变量的引用次数
print(sys.getrefcount(a))        # 两次引用,一次来自a,一次来自 getrefcount

def func(a):
    print(sys.getrefcount(a))    # 四次引用,a,python的函数调用栈,函数参数,和 getrefcount

func(a)
print(sys.getrefcount(a))        # 两次引用,一次来自a,一次来自getrefcount。函数func调用已经不存在被垃圾回收了




#####################
#示例    手动启动垃圾回收
import gc             #gc(garbage collector)模块的引用计数技术来进行垃圾回收
import psutil         #获取系统信息模块

#显示当前 python 程序占用的内存大小
def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    info = p.memory_full_info()
    memory = info.uss / 1024 / 1024
    print('{} memory used: {} MB'.format(hint, memory))

def func():
    show_memory_info('initial')            #输出较小内存占用空间
    #global a                              #如果将局部变量声明成全局变量,则持续占用较大内存空间
    a = [i for i in range(10000000)]       #临时变量占用较大内存空间。  函数运行完毕后即垃圾回收机制清除临时变量
    show_memory_info('after a created')    #输出垃圾回收机制后的较小内存占用空间
    #return a                              #如果函数返回局部变量,则也会持续占用较大内存空间

show_memory_info('initial')
a = [i for i in range(10000000)]
show_memory_info('after a created')

del a

gc.collect()                              #gc(garbage collector)模块的引用计数技术来进行垃圾回收
show_memory_info('finish')

print(a)




####################
#示例    手动启动垃圾回收    循环调用嵌套示例
#Python使用标记清除(mark-sweep)算法和分代收集(generational),来启用针对循环引用的自动垃圾回收。
import gc

#循环引用嵌套,导致其a,b的引用计数不为0,从而垃圾会机制不成立,需要手动显示启动垃圾回收机制。
def func():
    show_memory_info('initial')
    a = [i for i in range(10000000)]
    b = [i for i in range(10000000)]
    show_memory_info('after a, b created')
    a.append(b)
    b.append(a)

func()
gc.collect()                              #这里如果不进行手动启动垃圾回收,则内存暂用空间依然高企
show_memory_info('finished')










猜你喜欢

转载自blog.csdn.net/weixin_44015805/article/details/100164597