多线程补充以及协程

多线程补充以及协程

1.线程队列

线程队列用法与进程队列一样

import queue     #先进先出
q = queue.Queue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())   # 1
print(q.get())   # 2
print(q.get())   # 3
print(q.get(block=False))   # 取不到值直接报错
q.get(timeout=2)   # 阻塞2秒,还没有值直接报错


import queue  # 后进先出 LiFo 堆栈
q = queue.LifoQueue(3)
q.put(1)
q.put(2)
q.put('alex')
print(q.get())   # 'alex'
print(q.get())   # 2
print(q.get())   # 1


import queue   # 优先级队列
q = queue.PriorityQueue
q.put((5,'alex'))
q.put((-2,'七七'))
q.put((0,'赫赫'))
print(q.get())   # (-2,'七七')
print(q.get())   # (0,'赫赫')
print(q.get())   # (5,'alex')
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
#如果两个值的优先级一样,那么按照后面的值的acsii码顺序来排序,如果字符串第一个数元素相同,比较第二个元素的acsii码顺序.优先级相同的两个数据,他们后面的值必须是相同的数据类型才能比较,可以是元组,也是通过元素的ascii码顺序来排序

2.事件

开启两个线程,一个线程运行到中间的某个阶段,触发另个线程执行.两个线程增加了耦合性.

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。
为了解决这些问题,我们需要使用threading库中的Event对象。对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象,而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真.一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行.

event.is_set():返回event的状态值;

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。


版本一:
如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作
from threading import Thread,current_thread
import time

flag = False
def check():
    print(f'{current_thread().name} 检测服务器是否开启..')
    time.sleep(3)
    global flag
    flag = True
    print("服务器已经开启..")
    
def connect():
    while 1:
        print(f'{current_thread().name} 等待连接..')
        time.sleep(0.5)
        if flag:
            print(f'{current_thread().name} 连接成功..')
            break
            
t1 = Thread(target = check,)
t2 = Thread(target = connect,)
t1.start()
t2.start()


版本二:事件event
    
from threading import Thread,current_thread,Event
import time

event = Event()
def check():
    print(f'{current_thread().name} 检测服务器是否开启..')
    time.sleep(3)
    #print(event.is_set())  返回event的状态
    event.set()
    #print(event.is_set())
    print('服务器已经开启...')
    
def connect():
    print(f'{current_thread().name} 等待连接..')
    #event.wait()  #阻塞,直到event.set() 方法之后
    event.wait(1) #只阻塞1秒,1秒后还没进行set,直接进行下一步操作
    print(f'{current_thread().name} 连接成功')
    
t1 = Thread(target = check,)
t2 = Thread(target = connect,)
t1.start()
t2.start()


小测试:
一个线程检测服务器是否开启,另一个线程判断如果开始了,则显示连接成功,此线程只尝试连接3次,1s一次,超过3次,还没有连接成功,则显示连接失败

from threading import Thread,current_thread,Event
import time

event = Event()
def check():
    print(f'{current_thread().name} 检测服务器是否开启..')
    time.sleep(3)
    event.set()
    print('服务器已经开启..')
    
def connect():
    count = 1
    while not event.is_set():
        if count == 4:
            print('连接次数过多,已断开..')
            break
        event.wait(1)
        print(f'{current_thread().name} 尝试连接{count}次')
        count += 1
    else:
        print(f'{current_thread().name} 连接成功..')

3.协程

串行:一个线程执行一个任务,执行完毕后执行下一个任务
并行:多个cpu执行多个任务
并发:一个cpu执行多个任务
并发真正的核心:切换并且保留状态

单个cpu并发的执行10个任务:
1.方式一:开启多进程,并发执行,操作系统切换+保持状态
2.方式二:开启多线程,并发执行,操作系统切换+保持状态
3.方式三:开启协程并发的执行,程序把控cpu在任务之间来回的切换+保持状态
    
什么是协程?
是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

协程的本质就是在单线程下,由用户自己控制一个任务遇到IO阻塞后,切换另一个任务去执行,以此来提高效率.

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

对比操作系统控制线程的切换,用户在单线程内控制协程的切换
优点:
1.协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2.单线程内就可以实现并发的效果,最大限度的利用cpu
缺点:
1.协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程开启多个线程,每个线程内开启协程.

协程的特点:
1.必须在只有一个单线程内实现并发
2.修改共享数据不需加锁
3.用户程序内自己保存多个控制流的上下文栈(保持状态)
4.一个协程遇到IO操作自动切换到其它协程

安装:
    pip3 install greenlet
    
真正的协程模块就是使用greenlet完成的切换

版本1:切换+保留状态(遇到IO不会主动切换)
from greenlet import greenlet
import time

def eat(name):
    print('%s eat 1' %name)          #2
    g2.switch('taibai')              #3
    #time.sleep(3) 遇到阻塞不会主动切换
    print('%s eat 2' %name)          #6
    g2.switch()                      #7
def play(name):
    print('%s play 1' %name)         #4
    g1.switch()                      #5
    print('%s play 2' %name)         #8

g1=greenlet(eat)
g2=greenlet(play)

g1.switch('taibai')#可以在第一次switch时传入参数,以后都不需要
    
版本2:协程,模拟的阻塞,不是真正的阻塞

from threading import current_thread
import time,gevent

def eat(name):
    print('%s eat 1' %name)
    print(current_thread().name)
    gevent.sleep(2)  #模拟阻塞
    # time.sleep(2)  #真正阻塞,不会主动切换
    print('%s eat 2' %name)

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


g1 = gevent.spawn(eat,'egon')
g2 = gevent.spawn(play,'egon')
print(f'主{current_thread().name}')
g1.join()
g2.join()


版本3:
  
import gevent,time
from gevent import monkey

monkey.patch_all()  # 打补丁: 将下面的所有的任务的阻塞都打上标记
def eat(name):
    print('%s eat 1' %name)
    time.sleep(2)
    print('%s eat 2' %name)

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


g1 = gevent.spawn(eat,'egon')
g2 = gevent.spawn(play,'egon')
gevent.joinall([g1,g2])
gevent介绍
安装:
    pip3 install gevent
    
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

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

g1.join() #等待g1结束

g2.join() #等待g2结束  有人测试的时候会发现,不写第二个join也能执行g2,是的,协程帮你切换执行了,但是你会发现,如果g2里面的任务执行的时间长,但是不写join的话,就不会执行完等到g2剩下的任务了

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

​ 单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2...如此,才能提高效率,这就用到了Gevent模块。

posted @ 2019-08-27 22:16  Bugbiss  阅读(151)  评论(0编辑  收藏  举报