day34 线程池 协程

线程的其他方法

Thread实例对象的方法
  # isAlive(): 返回线程是否是活动的。
  # getName(): 返回线程名。
  # setName(): 设置线程名。

threading模块提供的一些方法:
  # threading.currentThread(): 返回当前的线程变量对象。
  # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
其他方法
import threading
import time
from threading import Thread,current_thread

def f1(n):
    time.sleep(1)
    print('子线程对象', current_thread())  # <Thread(Thread-1, started 123145336967168)>
    print('子线程名称', current_thread().getName())  # 当前线程对象 Thread-1
    print('子线程ID', current_thread().ident)  # 123145336967168
    print('%s号线程任务'%n)


if __name__ == '__main__':
    t1 = Thread(target=f1,args=(1,))
    t1.start()

    print('主线程对象',current_thread())  # <_MainThread(MainThread, started 140734833878464)>
    print('主线程名称',current_thread().getName())  # 当前线程对象(是主线程对象) MainThread
    print('主线程ID',current_thread().ident)  # 当前线程ID 140734833878464

    print(threading.enumerate()) # [<_MainThread(MainThread, started 140734833878464)>, <Thread(Thread-1, started 123145336967168)>]
    print(threading.active_count())  # 2

"""
结果:
主线程对象 <_MainThread(MainThread, started 140734833878464)>
主线程名称 MainThread
主线程ID 140734833878464
[<_MainThread(MainThread, started 140734833878464)>, <Thread(Thread-1, started 123145336967168)>]
2
子线程对象 <Thread(Thread-1, started 123145336967168)>
子线程名称 Thread-1
子线程ID 123145336967168
1号线程任务


# 小结:
                                      
threading.current_thread()  <==等效于==> Thread(target=f1)
#这两个等效的前提是: 左边  的位置要跟 右边target(目标函数)所在位置 一样,即左边的是获取当前位置的线程变量对象,右边的是在target(目标函数)所在位置创建线程对象.
"""
栗子

线程队列 (重点)

线程队列,不需要从threading模块里面导入,直接import queue就可以了,这是python自带的

queue队列 :使用import queue,用法与进程队列 multiprocessing.Queue 一样,也有以下方法:

# put,put_nowait,get,get_nowait,full,empty,qsize
q = Queue(5)  # 5是size
q.put()  #放入数据,满了会等待(阻塞)
q.get()  #获取数据,没有数据了会等待(阻塞)

q.qsize()  # 当前放进去的元素的个数

q.empty()  # 是否为空,不可靠(因为多线程)
q.full() # 是否满了,不可靠(因为多线程)

q.put_nowait()   #添加数据,不等待,但是队列满了报错
q.get_nowait()  #获取数据,不等待,但是队列空了报错

class queue.Queue(maxsize=0) #先进先出(FIFO: fisrt in fisrt out)

import queue  # 线程中的队列使用的是这个,等效于进程中的队列  put,put_nowait,get,get_nowait,full,empty
q = queue.Queue(4)  # FIFO先进先出  first in first out


q.put(1)
q.put(2)
print(q.full())  # 不满
# print('当前队列内容的长度',q.qsize())
q.put(3)
print(q.full())  #
# q.put(4)  # 不报错,会阻塞

print(q.qsize())

try:
    q.put_nowait(4)  # 报错queue.Full
except Exception:
    print('queue full')


print(q.get())
print(q.get())
print(q.empty())  # 不空
print(q.get())
print(q.empty())  #
print(q.get())  # 不报错,会阻塞

try:
    print(q.get_nowait())  # 报错queue.Empty

except Exception:
    print('queue empty')
先进先出队列

class queue.LifoQueue(maxsize=0) #先进后出队列(或者后进先出(LIFO: last in fisrt out)),类似于栈

q = queue.LifoQueue(3)  # Lifo
q.put(1)
q.put(2)
print(q.full())  # 不满
# print('当前队列内容的长度',q.qsize())
q.put(3)
print(q.full())  #
# q.put(4)  # 不报错,会阻塞

print(q.qsize())

try:
    q.put_nowait(4)  # 报错queue.Full
except Exception:
    print('queue full')


print(q.get())
print(q.get())
print(q.empty())  # 不空
print(q.get())
print(q.empty())  #
print(q.get())  # 不报错,会阻塞

try:
    print(q.get_nowait())  # 报错queue.Empty

except Exception:
    print('queue empty')
后进先出队列

class queue.PriorityQueue(maxsize=0) #优先级的队列(存储数据时可设置优先级)

# 优先级队列 PriorityQueue

# put的数据是一个元组,元组的第一个参数是优先级数字(通常是数字,也可以是非数字之间的比较),数字越小优先级越高,越先被get拿到被取出来,第二个参数是put进去的值(可以是任意的数据类型)
# 如果说优先级(第一个参数)相同,那么比较值(第二个参数),值必须是相同的数据类型(不包括字典),否则报错
# 比较第二个参数:
# 如果第二个参数(或者其参数的元素)是数字: 数字==直接拿整体的数字==>比较大小,
# 如果第二个参数(或者其参数的元素)是字符串:字符串=依次取到每个字符=>比较每个字符的ASCII码.
q = queue.PriorityQueue(10)

q.put((-5, 'alex'))  # 放入元组,第一个元素是优先级(可以为负数,越小,优先级越高),第二个是真正的数据(数据类型随意)
q.put((2, 'blex'))
q.put((3, 'clex'))
q.put((3, '111'))

print(q.get())
print(q.get())
print(q.get())
print(q.get())
print('=======================')



q.put(('x', 123))
q.put(('y', 345))

print(q.get())
print(q.get())
print('=======================')

"""
('x', 123)
('y', 345)
"""


q.put((5, 'alex'))  # 放入元组,第一个元素是优先级(可以为负数,越小,优先级越高),第二个是真正的数据(数据类型随意)
q.put((2, 1))
q.put((3, (1,)))
# q.put((7, {1,2}))  # 优先级相同数据类型不同,报错TypeError: '<' not supported between instances of 'dict' and 'set'
q.put((7, {1:2}))
q.put((7, {1:'a'}))  # 优先级相同数据类型都是字典,报错TypeError: '<' not supported between instances of 'dict' and 'dict'


print(q.get())
print(q.get())
print(q.get())
print(q.get())

print('=======================')
优先级队列

  

线程池(重点)

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

统一使用方式,使用threadPollExecutor和ProcessPollExecutor的方式一样,而且只要通过这个concurrent.futures导入就可以直接用他们两个了

concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.
两者实现相同的接口,该接口由抽象Executor类定义。

#2 基本方法
#submit(fn, *args, **kwargs)
异步提交任务(万能传参,传入的实参可以是任意数据类型,注意fn的形参数量要和这里的实参数量对应)

#map(func, *iterables, timeout=None, chunksize=1) 
取代for循环submit的操作(参数1是函数,参数2是可迭代对象)

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

#result(timeout=None)
取得结果(相当于pool.get())

#add_done_callback(fn)
回调函数(功能类似于pool的callback,但是显然用法不同)
"""
multiprocessing.Pool和concurrent.futures.ThreadPoolExecutor,ProcessPoolExecutor中回调函数的区别:

进程的回调函数res = pool.apply_async(f1,args=(5,),callback=call_back_func)
(这里的callback是默认的关键字,call_back_func是自定义的回调函数名)==>作为异步对象的参数调用
线程的回调函数res = tp.submit(f1,11,12).add_done_callback(f2)
(这里的add_done_callback是默认的回调函数名,f2是自定义的回调函数)==>作为异步对象的方法调用)
"""
concurrent.futures方法

上栗子:

import time
import os
import threading
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def func(n):
    time.sleep(2)
    print('%s打印的:'%(threading.get_ident()),n)
    return n*n
tpool = ThreadPoolExecutor(max_workers=5) #默认一般起线程的数据不超过CPU个数*5
# tpool = ProcessPoolExecutor(max_workers=5) #进程池的使用只需要将上面的ThreadPoolExecutor改为ProcessPoolExecutor就行了,其他都不用改
#异步执行
t_lst = []
for i in range(5):
    t = tpool.submit(func,i) #提交执行函数,返回一个结果对象,i作为任务函数的参数 def submit(self, fn, *args, **kwargs):  可以传任意形式的参数
    t_lst.append(t)  #
    # print(t.result())
    #这个返回的结果对象t,不能直接去拿结果,不然又变成串行了,可以理解为拿到一个号码,等所有线程的结果都出来之后,我们再去通过结果对象t获取结果
tpool.shutdown() #起到原来的close阻止新任务进来 + join的作用,等待所有的线程执行完毕
print('主线程')
for ti in t_lst:
    print('>>>>',ti.result())

# 我们还可以不用shutdown(),用下面这种方式
# while 1:
#     for n,ti in enumerate(t_lst):
#         print('>>>>', ti.result(),n)
#     time.sleep(2) #每个两秒去去一次结果,哪个有结果了,就可以取出哪一个,想表达的意思就是说不用等到所有的结果都出来再去取,可以轮询着去取结果,因为你的任务需要执行的时间很长,那么你需要等很久才能拿到结果,通过这样的方式可以将快速出来的结果先拿出来。如果有的结果对象里面还没有执行结果,那么你什么也取不到,这一点要注意,不是空的,是什么也取不到,那怎么判断我已经取出了哪一个的结果,可以通过枚举enumerate来搞,记录你是哪一个位置的结果对象的结果已经被取过了,取过的就不再取了

#结果分析: 打印的结果是没有顺序的,因为到了func函数中的sleep的时候线程会切换,谁先打印就没准儿了,但是最后的我们通过结果对象取结果的时候拿到的是有序的,因为我们主线程进行for循环的时候,我们是按顺序将结果对象添加到列表中的。
# 37220打印的: 0
# 32292打印的: 4
# 33444打印的: 1
# 30068打印的: 2
# 29884打印的: 3
# 主线程
# >>>> 0
# >>>> 1
# >>>> 4
# >>>> 9
# >>>> 16
栗子
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import time
def f1(n,s):  # 要与 万能传参 的参数数量一致
    time.sleep(1)
    # print(n,s)
    return n * n

if __name__ == '__main__':
    tp = ThreadPoolExecutor(4) # 线程  默认的线程个数是cpu个数 * 5
    # tp = ProcessPoolExecutor(4) # 进程  默认的进程个数是cpu个数 这两个的方法一致
    # tp.map(f1, range(10))  # 异步提交任务,参数是(任务名,可迭代对象)
    res_lis = []
    for i in range(10):
        res = tp.submit(f1,i,'baobao')  # submit是给线程池异步提交任务,万能传参
        # print(res)  # <Future at 0x10617a208 state=running>

        res_lis.append(res)

    for t in res_lis:  # 4个4个的打印
        print(t.result())

    tp.shutdown()  # ==等效于==> close + join 主线程等待所有提交给线程池的任务全部执行完毕

    for t in res_lis:  # 全部一起打印
        print(t.result())  # 结果对象.result,#和get方法一样,如果没有结果,会等待,阻塞程序

    print('主线程')

"""
只需要将这一行代码改为下面这一行就可以了,其他的代码都不用变
tpool = ThreadPoolExecutor(max_workers=5) #默认一般起线程的数据不超过CPU个数*5
# tpool = ProcessPoolExecutor(max_workers=5)#默认一般起线程的数据不超过CPU个数

你就会发现为什么将线程池和进程池都放到这一个模块里面了,因为用法一样,所以不管是线程池还是进程池,更推荐使用这个from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
"""
ThreadPoolExecutor的简单使用
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

def func(n):
    time.sleep(2)
    return n*n

def call_back(m):
    print('结果为:%s'%(m.result()))  # 注意回调函数拿到的是线程(进程)对象,想要拿到值需要调用result方法

tpool = ThreadPoolExecutor(max_workers=5)
t_lst = []
for i in range(5):
    t = tpool.submit(func,i).add_done_callback(call_back)
    
"""
结果为:0
结果为:1
结果为:4
结果为:9
结果为:16

"""
回调函数简单应用

协程 

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

需要强调的是:

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

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

#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
#2. 单线程内就可以实现并发的效果,最大限度地利用cpu
协程优点
#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
#2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
协程缺点
# 1.必须在只有一个单线程里实现并发
# 2.修改共享数据不需加锁
# 3.用户程序里自己保存多个控制流的上下文栈
# 4.附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
协程特点总结

协程就是告诉Cpython解释器,你不是nb吗,不是搞了个GIL锁吗,那好,我就自己搞成一个线程让你去执行,省去你切换线程的时间,我自己切换比你切换要快很多,避免了很多的开销,对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。

协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:

#1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。

#2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换

  

生成器 

并发的本质:任务切换+保存状态,yield本身就是一种在单线程下可以保存任务运行状态的方法,

#1 yield可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
#2 send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换  
import time
#基于yield并发执行,多任务之间来回切换,这就是个简单的协程的体现,但是他能够节省I/O时间吗?不能,yield不能检测IO,不能实现遇到IO自动切换
def f1():
    for i in range(3):
        time.sleep(0.5)  # 发现什么?只是进行了切换,但是并没有节省I/O时间
        print('f1>>',i)
        # yield

def f2():
    # g = f1()
    for i in range(3):
        time.sleep(0.5)
        print('f2>>', i)
        # next(g)

#不写yield,下面两个任务是执行完func1里面所有的程序才会执行func2里面的程序,有了yield,我们实现了两个任务的切换+保存状态
#基于yield保存状态,实现两个任务直接来回切换,即并发的效果
#PS:如果每个任务中都加上打印,那么明显地看到两个任务的打印是你一次我一次,即并发执行的.
f1()
f2()

"""
f1>> 0
f1>> 1
f1>> 2
f2>> 0
f2>> 1
f2>> 2

有了yield:
f2>> 0
f1>> 0
f2>> 1
f1>> 1
f2>> 2
f1>> 2
生成器版协程

greenlet模块 

#安装==>在终端输入以下代码
pip3 install greenlet
import time
from greenlet import greenlet


# 真正的协程模块就是使用greenlet完成的切换
def f1(s):
    print('第一次f1==>'+s)
    g2.switch('taibai')  #切换到g2这个对象的任务去执行
    time.sleep(1)
    print('第一次f1==>'+s)
    g2.switch()
def f2(s):
    print('第一次f2==>'+s)
    g1.switch()
    time.sleep(1)
    print('第二次f2==>'+s)
g1 = greenlet(f1)  #实例化一个greenlet对象,并将任务名称作为参数传进去
g2 = greenlet(f2)
g1.switch('alex') #执行g1对象里面的任务,可以在第一次switch时传入参数,以后都不需要

"""
greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
"""
greenlet版协程

一般在工作中我们都是进程+线程+协程的方式来实现并发,以达到最好的并发效果,如果是4核的cpu,一般起5个进程,每个进程中20个线程(5倍cpu数量),每个线程可以起500个协程,大规模爬取页面的时候,等待网络延迟的时间的时候,我们就可以用协程去实现并发。 并发数量 = 5 * 20 * 500 = 50000个并发,这是一般一个4cpu的机器最大的并发数。nginx在负载均衡的时候最大承载量就是5w个。 

gevent模块(重点)

#安装==>在终端输入以下代码
pip3 install gevent
from gevent import monkey;monkey.patch_all()  # 必须写在最上面,这句话后面的所有阻塞全部能够识别了
import gevent
import time
import threading

# 遇到IO阻塞时会自动切换任务
def f1(name):
    print(f'{name}==第一次f1')
    print(threading.current_thread().getName())  # DummyThread-1 假线程,虚拟线程
    # gevent.sleep(1)  # gevent默认可以识别的io阻塞
    time.sleep(2)  # 加上mokey就能够识别到time模块的sleep了
    print(f'{name}==第二次f1')
    return name

def f2(name):
    print(threading.current_thread().getName())  # DummyThread-2
    print(f'{name}==第一次f2')
    # gevent.sleep(2)
    time.sleep(2)  # 来回切换,直到一个I/O的时间结束,这里都是我们的gevent做得,不再是控制不了的操作系统了。
    print(f'{name}==第二次f2')

s = time.time()

g1 = gevent.spawn(f1,'alex') #异步提交了f1任务
g2 = gevent.spawn(f2,name='egon') #创建一个协程对象g2,spawn括号内第一个参数是函数名,如f2,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数f2的,spawn是异步提交任务
# g1.join()  # 等待g1结束,上面只是创建协程对象,这个join才是去执行
# g2.join()  # 等待g2结束  有人测试的时候会发现,不写第二个join也能执行g2,是的,协程帮你切换执行了,但是你会发现,如果g2里面的任务执行的时间长,但是不写join的话,就不会执行完等到g2剩下的任务了

gevent.joinall([g1,g2])  # 这里等价于上述join两步合作一步
print(g1.value)#拿到func1的返回值

e = time.time()
print('执行时间:',e-s)  # 测试执行时间
print('主程序任务')

"""
结果:
alex==第一次f1
DummyThread-1
DummyThread-2
egon==第一次f2
alex==第二次f1
egon==第二次f2
alex
执行时间: 2.004991054534912
主程序任务

"""

'''
# spawn是类方法,参数是万能的
@classmethod
    def spawn(cls, *args, **kwargs):  # 万能形参==>实参可以随便传入
    g = cls(*args, **kwargs)
        g.start()
        return g
'''

# 我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程,虚拟线程,其实都在一个线程里面
# 进程线程的任务切换是由操作系统自行切换的,你自己不能控制
# 协程是通过自己的程序(代码)来进行切换的,自己能够控制,只有遇到协程模块能够识别的IO操作的时候,程序才会进行任务切换,实现并发效果,如果所有程序都没有IO操作,那么就基本属于串行执行了。
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)

if __name__ == '__main__':
    print('Synchronous:')
    synchronous()

    print('Asynchronous:')
    asynchronous()
#上面程序的重要部分是将task函数封装到greenlet内部线程的gevent.spawn。 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,后者阻塞当前流程,并执行所有给定的greenlet。执行流程只会在 所有greenlet执行完后才会继续向下走。


"""
# 结果:
Synchronous:同步,一个一个的打印
Task 0 done
Task 1 done
Task 2 done
Task 3 done
Task 4 done
Task 5 done
Task 6 done
Task 7 done
Task 8 done
Task 9 done
Asynchronous:异步,一起打印
Task 0 done
Task 1 done
Task 2 done
Task 3 done
Task 4 done
Task 5 done
Task 6 done
Task 7 done
Task 8 done
Task 9 done
"""
协程:同步异步对比

今日内容回顾:

1,线程的其他方法

Threading.current_thread() #当前线程对象

GetName() 获取线程名

Ident  获取线程id

Threading.Enumerate() #当前正在运行的线程对象的一个列表

Threading.active_count() #当前正在运行的线程数量

2,线程队列(重点)

Import queue

先进先出队列:queue.Queue(3)

先进后出\后进先出队列:queue.LifoQueue(3)  

优先级队列:queue.priorityQueue(3)

    Put的数据是一个元组,元组的第一个参数是优先级数字,数字越小优先级越高,越先被get到被取出来,第二个参数是put进去的值,如果说优先级相同,那么值别忘了应该是相同的数据类型,字典不行

3,线程池

From concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

P = ThreadPoolExecutor(4)  #默认的线程个数是cpu个数 * 5

P = ProcessPoolExecutor(4)  #默认的进程个数是cpu个数

P.map(f1,可迭代的对象)  #异步执行

Def f1(n1,n2):

Print(n1,n2)

P.submit(f1,11,12)  #异步提交任务

Res = P.submit(f1,11,12)

Res.result()  #get方法一样,如果没有结果,会等待,阻塞程序

Shutdown() #close+join,锁定线程池,等待线程池中所有已经提交的任务全部执行完毕

今日作业

  1. 多线程实现 一个socket并发聊天,就是一个服务端同时与多个客户端进行沟通
  2. 写一个简易的socketserver
  3. 通过线程池做爬虫,通过回调函数来清洗爬取回来的数据,简单写,就是将爬取回来的网页内容,通过正则来匹配一些其中的内容,匹配其中的链接网址

  

明天默写:

  1. 线程池的方法
  2. Gevent模块的写法

猜你喜欢

转载自www.cnblogs.com/liangxiaoji/p/10267656.html
今日推荐