cook book:12:并发编程

1:启动与停止线程  threading模块

复制代码
# threading 库可以在单独的线程中执行任何的在 Python 中可以调用的对象。
# 可以创建一个 Thread 对象并将你要执行的对象以 target 参数的形式提供给该对象
# 要在独立线程中执行的代码
import time
from threading import Thread

def countdown(n):
    while n > 0:
        print('T-minus', n)
        n -= 1
        time.sleep(5)

# 创建并启动一个线程
t = Thread(target=countdown, args=(10,))
t.start()

target 参数传递需要执行的对象
args参数传递需要执行对象的参数
t.start启动一个线程
复制代码
复制代码
# 创建好一个线程对象后,该对象并不会立即执行,需要调用它的 start() 方法
(当你调用 start() 方法时,它会调用你传递进来的函数,并把你传递进来的参数传递给该函数)。
Python中的线程会在一个单独的系统级线程中执行(比如说一个 POSIX 线程或者一个 Windows 线程),
这些线程将由操作系统来全权管理 # 线程一旦启动,将独立执行直到目标函数返回。t.is_alive函数可以查询一个线程对象的状态,看它是否还在执行 if t.is_alive(): print('Still running') else: print('Completed') # 可以将一个线程加入到当前线程,并等待它终止:
t.join 在主线程里执行,t代表子线程,t这个子线程不运行完成主线程就一直阻塞,直到t运行结束主线程才继续运行
t.join()
复制代码
复制代码
# Python解释器直到所有线程都终止前仍保持运行。需要长时间运行的线程或者需要一直运行的后台任务,使用后台线程(daemon是设置守护线程)
# 守护线程就是不重要的线程,默认threading模块创建的子线程主线程都会等待子线程运行结束主线程才结束,

设置某个子线程为守护线程后,主线程运行完毕子线程就销毁,主线程不会等待子线程运行完毕才结束 t = Thread(target=countdown, args=(10,), daemon=True) t.start() # 后台线程无法等待,不过,这些线程会在主线程终止时自动销毁。 除了如上所示的两个操作,并没有太多可以对线程做的事情。
你无法结束一个线程,无法给它发送信号,无法调整它的调度,也无法执行其他高级操作。如果需要这些特性,
需要自己添加。比如说,如果你需要终止线程,那么这个线程必须通过编程在某个特定点轮询来退出。 # 把线程放入一个类中
class CountdownTask: def __init__(self): self._running = True def terminate(self): self._running = False def run(self, n): while self._running and n > 0: print('T-minus', n) n -= 1 time.sleep(5) c = CountdownTask() t = Thread(target=c.run, args=(10,)) t.start() c.terminate() # 信号终端 t.join() # 等待实际终止(如果需要)
# 把task任务放到一个类方法里面,然后可以通过改变类属性来控制这个类方法的走向
这里while循环有self._running这个变量,主线程通过控制输入的这个变量来操作
线程里面run函数的走向,让run函数执行一次循环后不满足下次循环条件而终止
复制代码
复制代码
# 如果线程执行一些像I/O这样的阻塞操作,那么通过轮询来终止线程将使得线程之间的协调变得非常棘手。
比如,如果一个线程一直阻塞在一个I/O操作上,它就永远无法返回,也就无法检查自己是否已经被结束了。
要正确处理这些问题,可以利用超时循环来小心操作线程
class IOTask: def terminate(self): self._running = False def run(self, sock): # sock是一个套接字 sock.settimeout(5) # 设置超时时间5s while self._running: # 执行带超时的阻塞I/O操作 try: data = sock.recv(8192) break except socket.timeout: continue
# except这里如果捕获到socket这个I/O超时,不会阻塞,跳过这次循环继续下一次

继续走while循环来判断_running这个变量状态来决定线程是不是已经被结束了 # 继续进程运行 ... # 结束 return
复制代码
复制代码
# 由于全局解释锁(GIL),Python 的线程被限制到同一时刻只允许一个线程执行这样一个执行模型。
# 所以,Python 的线程更适用于处理I/O和其他需要并发执行的阻塞操作(比如等待I/O、等待从数据库获取数据等等),
而不上需要多处理器并行的计算密集型任务 # 通过继承 Thread 类来实现的线程
from threading import Thread import time class CountdownThread(Thread): def __init__(self, n): super().__init__() self.n = n def run(self): while self.n > 0: print('T-minus', self.n) self.n -= 1 time.sleep(5) c = CountdownThread(5) c.start() # CountdownThread类继承Thread类,重构run函数,然后CountdownThread类实例化并且c.start的时候就会自动执行run函数里面的逻辑 # 这样也可以工作,但这使得你的代码依赖于 threading 库,所以你的这些代码只能在线程上下文中使用。
上文所写的那些代码、函数都是与 threading 库无关的,这样就使得这些代码可以被用在其他的上下文中,可能与线程有关,也可能与线程无关 # 可以通过 multiprocessing 模块在一个单独的进程中执行你的代码
import multiprocessing c = CountdownTask(5) p = multiprocessing.Process(target=c.run) p.start() # 这段代码仅适用于 CountdownTask 类是以独立于实际的并发手段(多线程、多进程等等)实现的情况。
就是
CountdownTask 类没有继承Thread等哪些线程和进程类,
直接外部使用
multiprocessing.Process(target=c.run)或者Thread(target=countdown, args=(10,))
来启动多线程或者多进程
复制代码

 2:判断线程是否已经启动  

复制代码
# 线程的一个关键特性是每个线程都是独立运行且状态不可预测。
如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。
为了解决这些问题,我们需要使用 threading 库中的 Event 对象。 Event 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
在初始情况下,event 对象中的信号标志被设置为假。如果有线程等待一个 event 对象,而这个 event 对象的标志为假,
那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个 event 对象的信号标志设置为真,
它将唤醒所有等待这个 event 对象的线程。如果一个线程等待一个已经被设置为真的 event 对象,那么它将忽略这个事件,继续执行 # 如何使用 Event 来协调线程的启动
from threading import Thread, Event import time # 要在独立线程中执行的代码 def countdown(n, started_evt): print('countdown starting') started_evt.set() while n > 0: print('T-minus', n) n -= 1 time.sleep(5) # 创建将用于发出启动信号的事件对象 started_evt = Event() # 启动线程并传递启动事件 print('Launching countdown') t = Thread(target=countdown, args=(10,started_evt)) t.start() # 等待线程启动 started_evt.wait() print('countdown is running') # 执行这段代码,“countdown is running” 总是显示在 “countdown starting” 之后显示。
这是由于使用 event 来协调线程,使得主线程要等到 countdown() 函数输出启动信息后,才能继续执行
复制代码
复制代码
# 多线程Event:https://www.cnblogs.com/yanshw/p/10863479.html
started_evt = Event()定义一个全局内置标志Flag,如果Flag为False,
执行到 event.wait 时程序就会阻塞,如果Flag为True,event.wait 便不会阻塞 接口   set()   将标志设置为True,并通知所有处于阻塞状态的线程恢复运行   clear() 将标志设置为False   isSet()   获取内置标志的状态,返回 True 或者 False   wait(timeout)   如果标志为False,将使得线程阻塞,如果为True,继续运行,默认为False
event = threading.Event()创建event对象的时候默认这个Flag就处于False状态
复制代码
复制代码
# event 对象最好单次使用,如果创建一个 event 对象,让某个线程等待这个对象,一旦这个对象被设置为真,
你就应该丢弃它。尽管可以通过 clear() 方法来重置 event 对象,但是很难确保安全地清理 event 对象并对它重新赋值。
很可能会发生错过事件、死锁或者其他问题(特别是,你无法保证重置 event 对象的代码会在线程再次等待这个 event 对象之前执行)。 # 如果一个线程需要不停地重复使用 event 对象,最好使用 Condition 对象来代替 # 使用 Condition 对象实现了一个周期定时器,每当定时器超时的时候,其他线程都可以监测到:
# 计时器线程定制一个时间,当时间超时得时候就唤醒任务线程运行
import threading import time class PeriodicTimer: def__init__(self, interval): self._interval = interval self._flag = 0 self._cv = threading.Condition() def start(self): t = threading.Thread(target=self.run) t.daemon = True t.start() def run(self): ''' 运行计时器并在每个间隔后通知等待的线程 '''
    
while True: time.sleep(self._interval) with self._cv: self._flag ^= 1 self._cv.notify_all() def wait_for_tick(self): ''' 等待计时器的下一个滴答声 ''' with self._cv: last_flag = self._flag while last_flag == self._flag: self._cv.wait() # 定时器的使用示例 ptimer = PeriodicTimer(5) ptimer.start()
# 开一个线程运行PeriodicTimer类里面的run函数,这个线程还设置了守护线程,当主线程结束的时间子线程也结束 # 这个线程不停的执行run函数里面的while循环 # interval传递进去的间隔参数=5,运行run函数的时候sleep睡眠5s
# 在计时器上同步的两个线程
def countdown(nticks): while nticks > 0: ptimer.wait_for_tick() print('T-minus', nticks) nticks -= 1 def countup(last): n = 0 while n < last: ptimer.wait_for_tick() print('Counting', n) n += 1 threading.Thread(target=countdown, args=(10,)).start() threading.Thread(target=countup, args=(5,)).start()
^=:^是按位异或运算符,当两对应的二进位相异时,结果为1
self._flag ^= 1等同self._flag = self._flag ^1
self._flag=0,0和1按位异或=1,1和1按位异或等于0,一直这样轮询
所以self._flag这个标志参数在0和1之间来回跳动
上面的脚本启动4个线程,一个主线程,一个
PeriodicTimer计时器线程和两个工作线程
一开始启动PeriodicTimer计时器线程运行run函数,走while死循环,睡眠5s
然后启动两个任务线程countdown和countup,调用wait_for_tick函数

此时last_flag 和 self._flag相等,走while里面的代码,condition对象wait()
释放锁并且把自己放入condition对象的等待池里面,两个任务线程都是这样释放锁把自己放入等待池里面
等待5s后等到计时器线程的睡眠时间结束然后给每个任务线程发个消息解除休眠,任务线程继续运行
此时self._flag = 0了,然后任务线程继续sleep5秒,两个任务线程继续卡在wait等任务线程睡眠结束后得通知

来运行所有得任务线程

import threading import time class PeriodicTimer: def __init__(self, interval): self._interval = interval self._cv = threading.Condition() def start(self): t = threading.Thread(target=self.run) t.daemon = True t.start() def run(self): while True: time.sleep(self._interval) with self._cv: self._cv.notify_all() def wait_for_tick(self): with self._cv: self._cv.wait() ptimer = PeriodicTimer(5) ptimer.start() def countdown(nticks): while nticks > 0: ptimer.wait_for_tick() print('T-minus', nticks) nticks -= 1 def countup(last): n = 0 while n < last: ptimer.wait_for_tick() print('Counting', n) n += 1 threading.Thread(target=countdown, args=(10,)).start() threading.Thread(target=countup, args=(5,)).start()

# 这样最简单得写法运行得逻辑也和上面一样
因为
self._cv条件锁,四个线程同时只能运行一个
而且任务线程一定需要计时器线程唤醒

一开始运行计时器线程,睡眠,然后就运行任务线程,任务线程awit等待
然后计时器解除所有线程得等待,
此时如果运行计时器线程那么继续等待,任务线程执行,继续被计时器线程唤醒
如果此时任务线程运行,那么任务线程也会阻塞等待,释放锁,等计时器线程唤醒,都是一样得
last_flag 和 self._flag回一直相等,因为假如先运行计时器线程,等待5s,任务线程的两个flag肯定相等没有变化
如果先运行任务线程,flag也是没有变化的,不存在赋值了last_flag,然后又去计时器线程改变了self._flag
因为任务线程运行last_flag = self._flag的时候是占据这个condition锁的,计时器线程想运行self._flag ^= 1改变标志
必须等任务线程把condition锁释放,锁释放的时候早就进行判断了last_flag == self._flag,所以
last_flag 会一直等于 self._flag,不可能不相等,
复制代码
Python threading模块condition原理及运行流程详解:http://www.deiniu.com/article/196968.htm
Python condition实现线程通信(详解版):http://c.biancheng.net/view/2622.html
复制代码
# event对象的一个重要特点是当它被设置为真时会唤醒所有等待它的线程
# 如果只想唤醒单个线程,最好是使用信号量或者 Condition 对象来替代
# 使用信号量实现的代码
import threading

# 工作线程
def worker(n, sema):
    # 等待信号
    sema.acquire()
    # 执行一些任务
    print('Working', n)

# 创建一些线程
sema = threading.Semaphore(0)
# 计数器传入0,当计数器为0时候acquire将阻塞线程直到其他线程调用release函数 nworkers
= 10 for n in range(nworkers): t = threading.Thread(target=worker, args=(n, sema,)) t.start() # 上面会启动一个线程池,单纯运行没有什么事情发生,所有的线程都在等待获取信号量。每次信号量被释放,只有一个线程会被唤醒并执行 sema.release() # 运行一个线程,打印Working 0或者Working 9 sema.release() sema.release() sema.release() sema.release() sema.release() sema.release() sema.release() sema.release() sema.release()
复制代码
复制代码
# 线程(三):Lock(互斥锁)、RLock( 递归锁)、Semaphore(信号量)、Event(事件)、Condition(条件)、Timer(定时器)、queue(队列)
# https://blog.csdn.net/qq_41922768/article/details/84107806

condition:就是一把锁+一个等待池塘,锁的原因只能同时一个线程运行,例如生产者消费者模型,同一时刻只能运行一个生产者或者消费者
accquire获得锁返回True就是取得代码运行权力,如果别人加锁了那么运行accquire就会阻塞等待别人运行完了才自己才能运行
比如生产者线程,accquire获取运行代码的权力,然后进行条件判断,条件判断不满足就阻塞自己然后释放让锁别人运行,条件满足就运行自己的生产代码
比如消费者线程,和生产者一样,accquire获取运行代码的权力,然后进行条件判断,条件不满足阻塞自己释放锁,条件满足运行自己的消费代码
同一时刻只有一个生产者或者消费者运行,然后进行条件判断
这里加锁是为了获取代码运行权力,后面的waiti阻塞和notify唤醒,notify随机唤醒一个线程运行
复制代码

 3:线程间通信  queue队列来在多个线程中安全的交换信息和数据

 

复制代码
# 一个线程向另一个线程发送数据最安全的方式就是使用 queue 库中的队列
# 创建一个被多个线程共享的 Queue 对象,这些线程通过使用 put() 和 get() 操作来向队列中添加或者删除元素
# 最简单的生产者消费者模型,一个生产线程往队列丢数据,一个消费线程往队列里拿出数据
from queue import Queue
from threading import Thread

# 产生数据的线程
def producer(out_q):
    index = 0
    while True:
        data = index + 1
        index += 1
        # 产生一些数据
        out_q.put(data)

# 使用数据的线程,也就是消费者线程
def consumer(in_q):
    while True:
        # 获取一些数据
        data = in_q.get()
        # 操作数据
        print(data)

# 创建共享队列q
q = Queue()
# 启动两个线程
t1 = Thread(target=consumer, args=(q,))
t2 = Thread(target=producer, args=(q,))
t1.start()
t2.start()

# Queue 对象已经包含了必要的锁,可以通过它在多个线程间多安全地共享数据

# 使用队列时,协调生产者和消费者的关闭问题可能会有一些麻烦
# 通用的解决方法是在队列中放置一个特殊的值,当消费者读到这个值的时候,终止执行
from queue import Queue
from threading import Thread

# 表示关闭停止的对象
_sentinel = object()

# 生产者线程
def producer(out_q):
    while running:
        # 制造数据
        out_q.put(data)
    # 将标识停止的对象放在队列上以指示完成
    out_q.put(_sentinel)

# 消费者线程
def consumer(in_q):
    while True:
        # 获取数据
        data = in_q.get()
        # 检查是否终止
        if data is _sentinel:
            in_q.put(_sentinel)
            break

# 上面代码消费者在读到这个特殊值之后立即又把它放回到队列中,将之传递下去,所有监听这个队列的消费者线程就可以全部关闭了
# 队列是最常见的线程间通信机制,仍然可以自己通过创建自己的数据结构并添加所需的锁和同步机制来实现线程间通信,
常见的方法是使用 Condition 变量来包装你的数据结构
复制代码
复制代码
# 使用condition条件+heapq来创建一个线程安全的优先级队列
# put时候传递的priority值越高的优先级越高,调用get函数的时候第一个取出来 import heapq import threading class PriorityQueue: def __init__(self): self._queue = [] self._count = 0 self._cv = threading.Condition() def put(self, item, priority): with self._cv: heapq.heappush(self._queue, (-priority, self._count, item)) self._count += 1 self._cv.notify() def get(self): with self._cv: while len(self._queue) == 0: self._cv.wait() return heapq.heappop(self._queue)[-1]

heapq:堆模块
heapq.heappush:往self._queue这个列表里添加元素,第一个参数传递的是self._queue这个列表
第二个参数是往堆里面丢的元素,第二个参数是一个元组,这个元组就代表优先级
后面堆模块的
heappop取值和heappush往堆列表里添加值都会根据这个第一个元素为优先级进行堆排序
堆排序为数字最小的在最前面,也就是最小的优先级最高,根据这个来实现优先级队列
创建一个列表,使用堆模块往里面丢值,维护这个列表的堆特性,最小的元素在最前面,拿元素出去的时候
也从最小的最前面的元素开始拿出去,拿出去元素后继续维护堆特性,最小的在最前面
还需要加锁,因为对这个堆列表进行存数据和拿出数据的时候不加锁可能造成不安全的并发,而且这个还是个有条件condition锁
当队列长度为空的时候需要阻塞代码让别人拿不出数据类,除非put函数往里面丢了元素才能解除get函数的阻塞重新判断是不是
列表长度为空
复制代码
复制代码
# 队列来进行线程间通信是一个单向、不确定的过程,通过没有办法知道接收数据的线程是什么时候接收到的数据并开始工作的
# 队列对象提供一些基本完成的特性
# Queue队列模块的task_done() 和 join()使用
from queue import Queue
from threading import Thread

def producer(out_q):
    while running:
        out_q.put(data)

def consumer(in_q):
    while True:
        data = in_q.get()
        # 表示完成,在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
        in_q.task_done()

q = Queue()
t1 = Thread(target=consumer, args=(q,))
t2 = Thread(target=producer, args=(q,))
t1.start()
t2.start()

# 等待消耗所有生产的项目
q.join()  # 意味着等到队列为空,这里才解除阻塞,之后再执行别的操作

q.task_done()  本质上就是传递信号让队列里未完成的任务减1,每次从队列里取出来一个元素
未完成的任务就减少1,当队列里的未完成任务为0也就是计数为0的时候q.join这里就会解除阻塞继续运行
只要队列里有未完成的任务,q.join就会一直阻塞住
q.join和q.task_done就是一个完成特性,标识着当队列里所有未完成任务清零后join这边解除阻塞
复制代码
复制代码
# 如果一个线程需要在一个“消费者”线程处理完特定的数据项时立即得到通知,
可以把要发送的数据和一个 Event 放到一起使用,这样“生产者”就可以通过这个Event对象来监测处理的过程了
from queue import Queue from threading import Thread, Event def producer(out_q): while running: # 制作(数据、事件)对并将其交给消费者 evt = Event() out_q.put((data, evt)) ... # 等待消费者处理商品 evt.wait() def consumer(in_q): while True: data, evt = in_q.get() evt.set()
# 这里制造者制造数据后就马上阻塞,然后消费者消费完这个数据后生产者才解除阻塞,
复制代码
复制代码
# 基于简单队列编写多线程程序在多数情况下是一个比较明智的选择。
# 从线程安全队列的底层实现来看,无需在代码中使用锁和其他底层的同步机制,
# 使用队列这种基于消息的通信机制可以被扩展到更大的应用范畴,比如可以把程序放入多个进程甚至是分布式系统而无需改变底层的队列结构。 
# 使用线程队列有一个要注意的问题,向队列中添加数据项时并不会复制此数据项,线程间通信实际上是在线程间传递对象引用。
  如果你担心对象的共享状态,那最好只传递不可修改的数据结构(如:整型、字符串或者元组)或者一个对象的深拷贝
from queue import Queue
from threading import Thread
import copy

def producer(out_q):
    while True:
        # Produce some data
        ...
        out_q.put(copy.deepcopy(data))

def consumer(in_q):
    while True:
        # Get some data
        data = in_q.get()
        # Process the data
        ...
复制代码
复制代码
# Queue 对象提供一些在当前上下文很有用的附加特性。
比如在创建 Queue 对象时提供可选的 size 参数来限制可以添加到队列中的元素数量。
对于“生产者”与“消费者”速度有差异的情况,为队列中的元素数量添加上限是有意义的。
比如,一个“生产者”产生项目的速度比“消费者” “消费”的速度快,
那么使用固定大小的队列就可以在队列已满的时候阻塞队列,以免未预期的连锁效应扩散整个程序造成死锁或者程序运行失常。
在通信的线程之间进行“流量控制”是一个看起来容易实现起来困难的问题。
如果发现自己曾经试图通过摆弄队列大小来解决一个问题,这也许就标志着程序可能存在脆弱设计或者固有的可伸缩问题。
# get() 和 put() 方法都支持非阻塞方式和设定超时
import queue q = queue.Queue() try: data = q.get(block=False)  # get获取队列q中的元素,非阻塞获取,获取不到报错 except queue.Empty: ... try: q.put(item, block=False)  # put往队列q里面添加元素,非阻塞添加,队列满了添加不进去就报错 except queue.Full: ... try: data = q.get(timeout=5.0)  # 设置获取队列里面元素的超时时间5s,超时还是获取不到就报错 except queue.Empty: ... # 这些操作都可以用来避免当执行某些特定队列操作时发生无限阻塞的情况,
比如,一个非阻塞的 put() 方法和一个固定大小的队列一起使用,这样当队列已满时就可以执行不同的代码 # 比如输出一条日志信息并丢弃
# 往队列里丢日志,如果日志满了丢不进去了就打印日志,然后把日志丢弃不添加到队列里了 def producer(q): ... try: q.put(item, block=False) except queue.Full: log.warning('queued item %r discarded!', item) # 让消费者线程在执行像 q.get() 这样的操作时,
超时自动终止以便检查终止标志_running,使用 q.get() 的可选参数 timeout
_running = True def consumer(q): while _running: try: item = q.get(timeout=5.0) # Process item ... except queue.Empty: pass # q.qsize() , q.full() , q.empty() 等实用方法可以获取一个队列的当前大小和状态。
但这些方法都不是线程安全的。可能你对一个队列使用 empty() 判断出这个队列为空,
但同时另外一个线程可能已经向这个队列中插入一个数据项。所以最好不要在代码中使用这些方法。

# Queue模块函数详解
https://www.cnblogs.com/sjfgod/p/7747086.html
复制代码

 4:给关键部分加锁  lock同步锁(互斥锁):对多线程程序中的临界区加锁以避免竞争条件。

复制代码
# 要在多线程程序中安全使用可变对象,你需要使用 threading 库中的 Lock 对象
import threading

class SharedCounter:
    """可由多个线程共享的计数器对象。"""
    def __init__(self, initial_value=0):
        self._value = initial_value
        self._value_lock = threading.Lock()

    def incr(self, delta=1):
        """用锁增加计数器"""
        with self._value_lock:
            self._value += delta

    def decr(self, delta=1):
        """通过锁定来减小计数器"""
        with self._value_lock:
            self._value -= delta

# Lock 对象和 with 语句块一起使用可以保证互斥执行,
就是每次只有一个线程可以执行 with 语句包含的代码块。
with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁
复制代码
复制代码
# 线程调度本质上是不确定的,在多线程程序中错误地使用锁机制可能会导致随机数据损坏或者其他的异常行为,
我们称之为竞争条件。为了避免竞争条件,最好只在临界区(对临界资源进行操作的那部分代码)使用锁 # 在一些“老的” Python 代码中,显式获取和释放锁是很常见的
import threading class SharedCounter: """可由多个线程共享的计数器对象。""" def __init__(self, initial_value=0): self._value = initial_value self._value_lock = threading.Lock() def incr(self, delta=1): """用锁增加计数器""" self._value_lock.acquire() self._value += delta self._value_lock.release() def decr(self, delta=1): """通过锁定来减小计数器""" self._value_lock.acquire() self._value -= delta self._value_lock.release() # 相比于这种显式调用的方法,with 语句更加优雅,也更不容易出错,
特别是程序员可能会忘记调用 release() 方法或者程序在获得锁之后产生异常这两种情况(使用 with 语句可以保证在这两种情况下仍能正确释放锁)。
为了避免出现死锁的情况,使用锁机制的程序应该设定为每个线程一次只允许获取一个锁。
如果不能这样做的话,就需要更高级的死锁避免机制,在 threading 库中还提供了其他的同步原语,比如 RLock 和 Semaphore 对象。
根据以往经验,这些原语是用于一些特殊的情况,如果你只是需要简单地对可变对象进行锁定,不应该使用它们。
一个 RLock (可重入锁)可以被同一个线程多次获取,主要用来实现基于监测对象模式的锁定和同步。
在使用这种锁的情况下,当锁被持有时,只有一个线程可以使用完整的函数或者类中的方法
# 实现一个带有Rlock可重入锁的 SharedCounter 类 import threading class SharedCounter: """可由多个线程共享的计数器对象。""" _lock = threading.RLock() def __init__(self, initial_value=0): self._value = initial_value def incr(self, delta=1): """用锁增加计数器""" with SharedCounter._lock: self._value += delta def decr(self, delta=1): """通过锁定来减小计数器""" with SharedCounter._lock: self.incr(-delta) # 如上:没有对每一个实例对象加锁(实例化时候这个对象就生产一把锁,每个对象的锁都不用),
取而代之的是一个被所有实例共享的类级锁。这个锁用来同步类方法,
具体来说就是,这个锁可以保证一次只有一个线程可以调用这个类方法。
与一个标准的锁不同的是,已经持有这个锁的方法在调用同样使用这个锁的方法时,无需再次获取锁。比如 decr 方法。
这种实现方式的一个特点是,无论这个类有多少个实例都只用一个锁。
因此在需要大量使用计数器的情况下内存效率更高。
不过这样做也有缺点,就是在程序中使用大量线程并频繁更新计数器时会有争用锁的问题。

Semaphore:信号量对象是一个建立在共享计数器基础上的同步原语。
如果计数器不为0,with 语句将计数器减1,线程被允许执行。with 语句执行结束后,计数器加1。
如果计数器为0,线程将被阻塞,直到其他线程结束将计数器加1。尽管可以在程序中像标准锁一样使用信号量来做线程同步,
但是这种方式并不被推荐,因为使用信号量为程序增加的复杂性会影响程序性能。
相对于简单地作为锁使用,信号量更适用于那些需要在线程之间引入信号或者限制的程序 # 需要限制一段代码的并发访问量,可以像下面这样使用信号量完成
from threading import Semaphore import urllib.request # 最多允许同时运行五个线程 _fetch_url_sema = Semaphore(5) def fetch_url(url): with _fetch_url_sema: return urllib.request.urlopen(url)
# 超过五个线程那么使用with的时候进入会阻塞,
_fetch_url_sema.acquire 类似获取锁,计数-1,当计数为0阻塞,表示当前已经有五个线程运行了,需要这个线程等待一下
_fetch_url_sema.release     类似释放锁,让计数+1
复制代码

 5:防止死锁的加锁机制    写一个多线程程序,其中线程需要一次获取多个锁,如何避免死锁

复制代码
# 多线程中,死锁问题很大一部分是由于线程同时获取多个锁造成的
# 比如:一个线程获取了第一个锁,然后在获取第二个锁的时候发生阻塞,
那么这个线程就可能阻塞其他线程的执行,从而导致整个程序假死 # 解决死锁问题的一种方案是为程序中的每一个锁分配一个唯一的id。
然后只允许按照升序规则来使用多个锁,这个规则使用上下文管理器是非常容易实现的
import threading from contextlib import contextmanager # 线程本地状态以存储有关已获取锁的信息 _local = threading.local()
# _local是一个全局的对象,使用这个全局对象可以在每个线程里创建线程变量(变量只属于单个线程而不是全局拥有) @contextmanager
def acquire(*locks): # 按对象标识符对锁进行排序
# 按对象id(内存地址)对锁进行排序,电脑分大端小端,大端新创建得锁在前面,后创建得锁在后面,小端相反 # 当前我的电脑创建了两个锁x_lock和y_lock,x_lock先创建,我的是小端优先,先创建得内存地址大在后面 # 本质上acquire函数就是一个上下文管理器,两种情况加锁和解锁多个锁 # 情况一:with acquire(x_lock, y_lock, z_lock):如果这样一次性传多个锁进去 # 第一次进去按照id顺序把锁进行排序 # 然后getattr(_local, 'acquired', []),因为想从_local里面找到这个线程的线程局部变量acquired未解开的锁列表,找不到返回一个[]空列表 # 然后如果是一次性传递多个锁的这周情况不会走if语句不会报错什么的,把当前传进来的锁和未解开的锁添加到acquired未解开的锁列表 # 然后把传进来的锁acquire全加上锁,with语句运行完后走yield后半部分,把锁按照上锁的反序释放,然后释放了的锁从acquired未解开的锁列表去除 # 情况二:这种多重锁的情况,if里面的判断正是针对这种情况 # with acquire(x_lock): # with acquire(y_lock): # with acquire(z_lock): # 第一次进去with锁,acquired未解开的锁的列表为空,然后把x_lock上锁,丢到acquired未解开的锁的列表,此时并没有解锁 # 第二次进入with,acquired未解开的锁的列表里有第一把锁x_lock,进行if判断,找到acquired列表里面id最大的锁的id # 判断这个最大锁的id是不是大于传递进来第一个锁locks[0]的id,如果旧的在列表里的比新传进来的id还大就报错 # (小端电脑,先创键的锁id内存小,后传递进来创键的锁id大,违背这个规律报错) # 同理第三次进入with,进行if判断,acquired列表有两把锁[x_lock, y_lock]如果新传进来的锁z_lock的id比这个小就报错 # 因为我的电脑大端优先,锁的创键顺序是x,y,z,内存从小到到是z,y,x,如下这样去加锁我的才不会报错,换了顺序就要报错了 # with acquire(z_lock): # with acquire(y_lock): # with acquire(x_lock): # 这种情况就先加锁z,y,x,然后调用yield里面的finally去按照反序x,y,z口进行解锁,解锁ok的就从 # 定义外部只允许使用id的升序规则使用多个锁,这里的升序规则是z,y,x,如果打乱顺序x,y,z或者其他顺序with锁就会报错 # 强制规定只能按照id升序去使用锁 locks = sorted(locks, key=lambda x: id(x)) # 确保未违反先前获取的锁的锁顺序 acquired = getattr(_local, 'acquired', []) if acquired and max(id(lock) for lock in acquired) >= id(locks[0]): raise RuntimeError('Lock Order Violation') # 获取所有锁 acquired.extend(locks) _local.acquired = acquired try: for lock in locks: lock.acquire() yieldfinally: # 按与采集相反的顺序释放锁for lock in reversed(locks): lock.release() del acquired[-len(locks):]

# 上面acquired和_local.acquired指向同一个id内存地址,指向同一个列表,
# 所以del acquired[-len(locks):]删除acquired列表的元素也会影响到_local.acquired这个属性的值
# 因为两个都是指向同一个列表

# 多个with下嵌套with的情况线程多次调用acquire函数,需要创键一个列表保存多个锁,但是函数又一直调用多次
_local = threading.local() @contextmanager def acquire(*locks): locks = sorted(locks, key=lambda x: id(x)) _local.list = getattr(_local, 'acquired', []) _local.list.extend(locks) _local.acquired = _local.list print(_local.list) print(_local.acquired) yield pass
如上可以勉强实现
_local.acquired和_local.list保存三个锁的列表而且_local.acquired和_local.list本质上指向同一个线程列表(只限本线程使用)
线程变量_local.acquired和_local.list,_local.list获取_local.acquired的值赋值给_local.list,一开始没有_local.acquired的话返回空[]
然后往_local.list添加值赋值给_local.acquired,然后调用acquire函数的时候继续使用getattr方法获取_local.acquired赋值给_local.list

# 使用上面这个上下文管理器,可以按照正常途径创建一个锁对象,但不论是单个锁还是多个锁中都使用 acquire() 函数来申请锁
import threading x_lock = threading.Lock() y_lock = threading.Lock() def thread_1(): while True: with acquire(x_lock, y_lock): print('Thread-1') def thread_2(): while True: with acquire(y_lock, x_lock): print('Thread-2') t1 = threading.Thread(target=thread_1) t1.daemon = True t1.start() t2 = threading.Thread(target=thread_2) t2.daemon = True t2.start()

# 线程1
acquire(x_lock, y_lock)锁按照id排序后按照x,y进行上锁,然后按照y,x进行解锁,没有问题
# 线程2
acquire(y_lock, x_lock)锁按照id排序后也和线程1一样x,y顺序上锁,y,x顺序解锁,所以线程1和线程2运行没有问题
# 执行这段代码,你会发现它即使在不同的函数中以不同的顺序获取锁也没有发生死锁。 
其关键在于,在第一段代码中,我们对这些锁进行了排序。通过排序,使得不管用户以什么样的顺序来请求锁,这些锁都会按照固定的顺序被获取。
如果有多个 acquire() 操作被嵌套调用,可以通过线程本地存储(TLS)来检测潜在的死锁问题

# 代码如下这样写运行必定会有一个线程发生崩溃
import threading from contextlib import contextmanager _local = threading.local() @contextmanager def acquire(*locks): locks = sorted(locks, key=lambda x: id(x)) acquired = getattr(_local, 'acquired', []) if acquired and max(id(lock) for lock in acquired) >= id(locks[0]): raise RuntimeError('Lock Order Violation') acquired.extend(locks) _local.acquired = acquired try: for lock in locks: lock.acquire() yieldfinally: for lock in reversed(locks): lock.release() del acquired[-len(locks):] x_lock = threading.Lock() y_lock = threading.Lock() def thread_1(): while True: with acquire(x_lock): with acquire(y_lock): print('Thread-1') def thread_2(): while True: with acquire(y_lock): with acquire(x_lock): print('Thread-2') t1 = threading.Thread(target=thread_1) t1.daemon = True t1.start() t2 = threading.Thread(target=thread_2) t2.daemon = True t2.start() 报错信息可能如下: Exception in thread Thread-1: Traceback (most recent call last): File "/usr/local/lib/python3.3/threading.py", line 639, in _bootstrap_inner self.run() File "/usr/local/lib/python3.3/threading.py", line 596, in run self._target(*self._args, **self._kwargs) File "deadlock.py", line 49, in thread_1 with acquire(y_lock): File "/usr/local/lib/python3.3/contextlib.py", line 48, in __enter__ return next(self.gen) File "deadlock.py", line 15, in acquire raise RuntimeError("Lock Order Violation") RuntimeError: Lock Order Violation # 发生崩溃的原因在于,每个线程都记录着自己已经获取到的锁。 acquire() 函数会检查之前已经获取的锁列表,
由于锁是按照升序排列获取的,所以函数会认为之前已获取的锁的id必定小于新申请到的锁,如果不是这时就会触发异常
复制代码
# [Python 多线程] threading.local类 (六):https://www.cnblogs.com/i-honey/p/8051668.html
复制代码
# 死锁是每一个多线程程序都会面临的一个问题
# 经验来讲,尽可能保证每一个 线程只能同时保持一个锁,这样程序就不会被死锁问题所困扰
。一旦有线程同时申请多个锁,一切就不可预料了 # 死锁的检测与恢复是一个几乎没有优雅的解决方案的扩展话题。 # 一个比较常用的死锁检测与恢复的方案是引入看门狗计数器。当线程正常运行的时候会每隔一段时间重置计数器,
在没有发生死锁的情况下,一切都正常进行。一旦发生死锁,由于无法重置计数器导致定时器超时,这时程序会通过重启自身恢复到正常状态 # 避免死锁是另外一种解决死锁问题的方式,在进程获取锁的时候会严格按照对象id升序排列获取,
经过数学证明,这样保证程序不会进入死锁状态。
避免死锁的主要思想是,单纯地按照对象id递增的顺序加锁不会产生循环依赖,而循环依赖是死锁的一个必要条件,从而避免程序进入死锁状态。 # 线程死锁的经典问题:“哲学家就餐问题” # 五位哲学家围坐在一张桌子前,每个人面前有一碗饭和一只筷子。
在这里每个哲学家可以看做是一个独立的线程,而每只筷子可以看做是一个锁。
每个哲学家可以处在静坐、 思考、吃饭三种状态中的一个。需要注意的是,每个哲学家吃饭是需要两只筷子的,
这样问题就来了:如果每个哲学家都拿起自己左边的筷子, 那么他们五个都只能拿着一只筷子坐在那儿,直到饿死。此时他们就进入了死锁状态 # 一个简单的使用死锁避免机制解决“哲学家就餐问题”的实现
import threading from contextlib import contextmanager _local = threading.local() @contextmanager def acquire(*locks): locks = sorted(locks, key=lambda x: id(x)) acquired = getattr(_local, 'acquired', []) if acquired and max(id(lock) for lock in acquired) >= id(locks[0]): raise RuntimeError('Lock Order Violation') acquired.extend(locks) _local.acquired = acquired try: for lock in locks: lock.acquire() yield finally: for lock in reversed(locks): lock.release() del acquired[-len(locks):] # 哲学家的线索 def philosopher(left, right): while True: with acquire(left, right): print(threading.currentThread(), 'eating') # 筷子(以锁为代表) NSTICKS = 5 chopsticks = [threading.Lock() for n in range(NSTICKS)]
# 五把锁,必须获得其中两把才能吃饭
# 创造所有的哲学家 for n in range(NSTICKS): t = threading.Thread(target=philosopher, args=(chopsticks[n], chopsticks[(n + 1) % NSTICKS])) t.start() # 为了避免死锁,所有的加锁操作必须使用 acquire() 函数。
如果代码中的某部分绕过acquire 函数直接申请锁,那么整个死锁避免机制就不起作用了

#
五把锁,必须获得其中两把才能吃饭,args传参得时候把0,1,2,3,4五位哲学家左右两边的锁都传递进去才能吃饭
哲学家0运行,传递0,1两把锁,哲学家1运行传递1,2两把锁,....哲学家4运行传递4,0两把锁
然后使用上面的锁管理器
acquire打开两个锁,内部线程运行的时候会根据id把五把锁0,1,2,3,4进行排序后然后进行加锁和解锁
五把锁,每个线程按照顺序进行加锁和解锁不会造成死索问题,
比如0被锁了,
复制代码

 6:保存线程的状态信息  保存正在运行线程的状态,这个状态对于其他的线程是不可见的     threading.local():线程变量,一个线程才能使用,不和其他线程共享

 

复制代码
# 多线程编程中,只保存当前运行线程的状态,使用 thread.local() 创建一个本地线程存储对象,
对这个对象的属性的保存和读取操作都只会对执行线程可见,而其他线程并不可见 # 重构上下文管理器使得它可以适用于多线程:
from socket import socket, AF_INET, SOCK_STREAM import threading class LazyConnection: def __init__(self, address, family=AF_INET, type=SOCK_STREAM): self.address = address self.family = AF_INET self.type = SOCK_STREAM self.local = threading.local()

      # self.local这个类实例化得到一个全局对象,
      # 但是不同的线程使用这个对象存储的数据其它线程不可见(本质上就是不同的线程使用这个对象时为其创建一个独立的字典)
      # 所以外部conn = LazyConnection(('www.python.org', 80))类的实例化的时候会创键一个全局对象
      # 并且调用self.local = threading.local()创键一个local对象方便后面给每个线程去创键线程变量
      # 后面每个线程里调用with conn的时候会调用__enter__函数里面的self.local.sock = socket(self.family, self.type)
      # 会在当前线程里创键一个线程变量sock,只属于当前线程和其他线程不冲突
      # 所以多个线程运行with conn的时候会在多个线程里创键sock套接字,所以每个线程都有自己的套接字,就拥有多个套接字
      # 但是每个线程只能有一个self.local.sock,
      # 线程里如果已经有一个self.local.sock线程变量的话再调用with进入__enter__函数的时候会进入if里面报错

def __enter__(self):
        if hasattr(self.local, 'sock'):
            raise RuntimeError('Already connected')
        self.local.sock = socket(self.family, self.type)
        self.local.sock.connect(self.address)
        return self.local.sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.local.sock.close()
        del self.local.sock

# 对于 self.local 属性的使用。 它被初始化为一个 threading.local() 实例。 
其他方法操作被存储为 self.local.sock 的套接字对象。 有了这些就可以在多线程中安全的使用 LazyConnection 实例了
from functools import partial def test(conn): with conn as s: s.send(b'GET /index.html HTTP/1.0\r\n') s.send(b'Host: www.python.org\r\n') s.send(b'\r\n') resp = b''.join(iter(partial(s.recv, 8192), b''))

      # resp = s.recv(8192) # 这样写就可以
      # resp = partial(s.recv, 8192)() # 这样写也可以
      # iter() 函数一个鲜为人知的特性就是,传递一个可调用对象和一个标记值,它会创建一个迭代器
      # 这个迭代器会一直调用传入的可调用对象直到它返回标记值为止,这时候迭代终止
      # 所以这里使用iter(partial(s.recv, 8192), b'')会一直循环调用s.recv(8192)直到读取到返回值未b''就停止这个循环

print('Got {} bytes'.format(len(resp)))

if __name__ == '__main__':
    conn = LazyConnection(('www.python.org', 80))
    t1 = threading.Thread(target=test, args=(conn,))
    t2 = threading.Thread(target=test, args=(conn,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()

# 每个线程会创建一个自己专属的套接字连接(存储为self.local.sock)。 
因此,当不同的线程执行套接字操作时,由于操作的是不同的套接字,因此它们不会相互影响
复制代码
# 大部分程序中创建和操作线程特定状态并不会有什么问题
# 当出了问题的时候,通常是因为某个对象被多个线程使用到,用来操作一些专用的系统资源, 
比如一个套接字或文件。你不能让所有线程共享一个单独对象, 因为多个线程同时读和写的时候会产生混乱。
本地线程存储通过让这些资源只能在被使用的线程中可见来解决这个问题 # 使用 thread.local() 可以让 LazyConnection 类支持一个线程一个连接,
而不是对于所有的进程都只有一个连接 # 原理是,每个 threading.local() 实例为每个线程维护着一个单独的实例字典。
所有普通实例操作比如获取、修改和删除值仅仅操作这个字典。 每个线程使用一个独立的字典就可以保证数据的隔离了

7:创建一个线程池  创建一个工作者线程池,用来响应客户端请求或执行其他的工作  concurrent.futures 函数库有一个 ThreadPoolExecutor 类来实现

复制代码
# 使用concurrent.futures 函数库ThreadPoolExecutor 类来创键一个线程池
# tcp服务器,使用了一个线程池来响应客户端
from socket import AF_INET, SOCK_STREAM, socket
from concurrent.futures import ThreadPoolExecutor

def echo_client(sock, client_addr):
    """处理客户端连接"""
    print('Got connection from', client_addr)
    while True:
        msg = sock.recv(65536)
        if not msg:
            break
        sock.sendall(msg)
    print('Client closed connection')
    sock.close()

def echo_server(addr):
    pool = ThreadPoolExecutor(128)
    sock = socket(AF_INET, SOCK_STREAM)
    sock.bind(addr)
    sock.listen(5)
    while True:
        client_sock, client_addr = sock.accept()    # 循环等待客户端连接进来
        pool.submit(echo_client, client_sock, client_addr)
        # 然后进程池指定任务函数echo_client,参数是client_sock+client_addr

echo_server(('', 15000))

# 代码逻辑:一个server循环接收客户端连接进来,当没有客户端连接进来阻塞在accept()函数,
当有连接进来立马获取子套接字然后把收发逻辑丢给进程池去处理
复制代码
复制代码
# 想手动创建你自己的线程池, 可以使用一个Queue来轻松实现
from socket import socket, AF_INET, SOCK_STREAM
from threading import Thread
from queue import Queue

def echo_client(q):
    """处理客户端连接"""
    sock, client_addr = q.get()
    print('Got connection from', client_addr)
    while True:
        msg = sock.recv(65536)
        if not msg:
            break
        sock.sendall(msg)
    print('Client closed connection')
    sock.close()

def echo_server(addr, nworkers):
    # 启动客户端工作人员
    q = Queue()
    # 创键一个队列q,下面创建nworkers这么多的线程,
    # 并且把队列当初参数传递进去以便任务函数echo_client运行时候从队列q里拿到sock, client_addr进行收发逻辑
    # 主线程while 1死循环不断接收介乎段的消息往队列q里面丢就可以了
    # 这nworkers个线程能获取到sock, client_addr就运行收发逻辑,不能的就阻塞在q.get()这里等待队列里的数据
    for n in range(nworkers):
        t = Thread(target=echo_client, args=(q,))
        t.daemon = True     # 守护线程
        t.start()

    # 运行服务器
    sock = socket(AF_INET, SOCK_STREAM)
    sock.bind(addr)
    sock.listen(5)
    while True:
        client_sock, client_addr = sock.accept()
        q.put((client_sock, client_addr))

echo_server(('', 15000), 128)

# 本质逻辑是一个主线程和128个子线程,主进程复制等待客户端连接,
如果来了连接把子套接字等信息放到队列q,然后128个子线程复制从队列里拿到子套接字等信息去和客户端进行收发逻辑
复制代码
复制代码
# 使用 ThreadPoolExecutor 相对于手动实现的一个好处在于它使得任务提交者更方便的从被调用函数中获取返回值
from concurrent.futures import ThreadPoolExecutor
import requests

def fetch_url(url):
    res = requests.get(url)
    return res.content
    # 返回网页的二进制

pool = ThreadPoolExecutor(10)
# 向池中提交工作
a = pool.submit(fetch_url, 'http://www.python.org')
b = pool.submit(fetch_url, 'http://www.pypy.org')

# 把结果拿回来
x = a.result()
y = b.result()
print(x)
print(y)

# 返回的handle对象会处理所有的阻塞与协作,然后从工作线程中返回数据。 
特别的,a.result() 操作会阻塞进程直到对应的函数执行完成并返回一个结果
复制代码
复制代码
# 应该避免编写线程数量可以无限制增长的程序
from threading import Thread
from socket import socket, AF_INET, SOCK_STREAM

def echo_client(sock, client_addr):
    print('Got connection from', client_addr)
    while True:
        msg = sock.recv(65536)
        if not msg:
            break
        sock.sendall(msg)
    print('Client closed connection')
    sock.close()

def echo_server(addr):
    sock = socket(AF_INET, SOCK_STREAM)
    sock.bind(addr)
    sock.listen(5)
    while True:
        client_sock, client_addr = sock.accept()
        t = Thread(target=echo_client, args=(client_sock, client_addr))
        t.daemon = True
        t.start()

echo_server(('', 15000))

# 只要有用户进来就创建一个线程去处理
# 尽管这个也可以工作, 但是不能抵御有人试图通过创建大量线程让你服务器资源枯竭而崩溃的攻击行为。 
通过使用预先初始化的线程池,你可以设置同时运行线程的上限数量。
# 创建大量线程会有什么后果。 现代操作系统可以很轻松的创建几千个线程的线程池。
甚至,同时几千个线程等待工作并不会对其他代码产生性能影响。
当然了,如果所有线程同时被唤醒并立即在CPU上执行,那就不同了——特别是有了全局解释器锁GIL。
通常,应该只在I/O处理相关代码中使用线程池
# 创建大的线程池的一个可能需要关注的问题是内存的使用。
例如,如果在OS X系统上面创建2000个线程,系统显示Python进程使用了超过9GB的虚拟内存。
不过,这个计算通常是有误差的。当创建一个线程时,操作系统会预留一个虚拟内存区域来放置线程的执行栈(通常是8MB大小)。
但是这个内存只有一小片段被实际映射到真实内存中。 因此,Python进程使用到的真实内存其实很小
(比如,对于2000个线程来讲,只使用到了70MB的真实内存,而不是9GB)。
如果你担心虚拟内存大小,可以使用 threading.stack_size() 函数来降低它
import threading threading.stack_size(65536) # 如果你加上这条语句并再次运行前面的创建2000个线程试验,
你会发现Python进程只使用到了大概210MB的虚拟内存,而真实内存使用量没有变。
注意线程栈大小必须至少为32768字节,通常是系统内存页大小(4096、8192等)的整数倍
复制代码

8:简单的并行编程  有个程序要执行CPU密集型工作,想让他利用多核CPU的优势来运行的快一点  多进程

复制代码
# concurrent.futures 库提供了一个 ProcessPoolExecutor类,
可被用来在一个单独的Python解释器中执行计算密集型函数 # 要使用它,先要有一些计算密集型的任务 # 假设有个Apache web服务器日志目录的gzip压缩包如下
logs/ 20120701.log.gz 20120702.log.gz 20120703.log.gz 20120704.log.gz 20120705.log.gz 20120706.log.gz ... # 假设每个日志文件内容类似下面这样: 124.115.6.12 - - [10/Jul/2012:00:18:50 -0500] "GET /robots.txt ..." 200 71 210.212.209.67 - - [10/Jul/2012:00:18:51 -0500] "GET /ply/ ..." 200 11875 210.212.209.67 - - [10/Jul/2012:00:18:51 -0500] "GET /favicon.ico ..." 404 369 61.135.216.105 - - [10/Jul/2012:00:20:04 -0500] "GET /blog/atom.xml ..." 304 - ... # 在上面日志文件中查找出所有访问过robots.txt文件的主机:import gzip import io import glob def find_robots(filename): """在单个日志文件中查找访问robots.txt的所有主机""" robots = set() with gzip.open(filename) as f: for line in io.TextIOWrapper(f, encoding='ascii'):
      # 读取文件每一行,每一行按照空格进行切片,如果切片后的文件的第七个等于/robots.txt,就把主机名称添加进robots集合 fields
= line.split() if fields[6] == '/robots.txt': robots.add(fields[0]) return robots def find_all_robots(logdir): """查找整个文件序列中的所有主机""" files = glob.glob(logdir + '/*.log.gz') # logdir路径下找到.log.g结尾的文件生成一个列表
# glob()函数就像Linux中的find -name *.type一样将某目录下所有跟通配符模式相同的文件放到一个列表中 # 这里glob.glob(logdir + '/*.log.gz')就是获取logdir路径下的所有以.log.gz名字结尾的文件
all_robots
= set()
# 创建一个集合all_robots # map() 会根据提供的函数对指定序列做映射,第一个参数function,让第二个参数(序列)中的每一个元素都调用function 函数 # 返回包含每次 function 函数返回值的新列表 # 然后for循环遍历新列表把每个返回值放到all_robots集合里面
for robots in map(find_robots, files): all_robots.update(robots) return all_robots if__name__ == '__main__': robots = find_all_robots('logs') for ipaddr in robots: print(ipaddr)
# 前面的程序使用了通常的map-reduce风格来编写。
函数 find_robots() 在一个文件名集合上做map操作,并将结果汇总为一个单独的结果,
也就是 find_all_robots() 函数中的 all_robots 集合。
 现在,想要修改这个程序让它使用多核CPU。
很简单——只需要将map()操作替换为一个 concurrent.futures 库中生成的类似操作即可
import gzip import io import glob from concurrent import futures def find_robots(filename): """在单个日志文件中查找访问robots.txt的所有主机""" robots = set() with gzip.open(filename) as f: for line in io.TextIOWrapper(f, encoding='ascii'): fields = line.split() if fields[6] == '/robots.txt': robots.add(fields[0]) return robots def find_all_robots(logdir): """查找整个文件序列中的所有主机""" files = glob.glob(logdir + '/*.log.gz') all_robots = set() with futures.ProcessPoolExecutor() as pool:
   # map取代了for+submit,往进程池里提交任务,异步提交任务,然后返回的结果也是返回的一个迭代器可以遍历取值
for robots in pool.map(find_robots, files): all_robots.update(robots) return all_robots if__name__ == '__main__': robots = find_all_robots('logs') for ipaddr in robots: print(ipaddr) # 修改后,运行这个脚本产生同样的结果,但是在四核机器上面比之前快了3.5倍。
实际的性能优化效果根据你的机器CPU数量的不同而不同

使用
concurrent.futures里面的ProcessPoolExecutor()多进程库来提升效率
复制代码
复制代码
# Python标准模块--concurrent.futures(并发未来)
concurent.future模块需要了解的
1.concurent.future模块是用来创建并行的任务,提供了更高级别的接口,
为了异步执行调用
2.concurent.future这个模块用起来非常方便,它的接口也封装的非常简单
3.concurent.future模块既可以实现进程池,也可以实现线程池
4.模块导入进程池和线程池 from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor 还可以导入一个Executor,但是你别这样导,这个类是一个抽象类 抽象类的目的是规范他的子类必须有某种方法(并且抽象类的方法必须实现),但是抽象类不能被实例化
5. p = ProcessPoolExecutor(max_works)对于进程池如果不写max_works:默认的是cpu的数目,默认是4个 p = ThreadPoolExecutor(max_works)对于线程池如果不写max_works:默认的是cpu的数目*5

6.如果是进程池,得到的结果如果是一个对象。我们得用一个.get()方法得到结果 但是现在用了concurent.future模块,我们可以用obj.result方法 p.submit(task,i) #相当于apply_async异步方法 p.shutdown() #默认有个参数wite=True (相当于close和join) # https://www.cnblogs.com/haiyan123/p/7461294.html
复制代码
复制代码
# ProcessPoolExecutor多进程池的使用
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor() as pool:
    ...
    使用pool并行工作
    ...

# 使用with语句等同调用这个ProcessPoolExecutor类里面的__enter__和__exit__方法,
说明这个进程地址池类构造了这两个方法, # 多进程原理 # 一个 ProcessPoolExecutor 创建N个独立的Python解释器,
N是系统上面可用CPU的个数,可以通过提供可选参数给ProcessPoolExecutor(N)来修改处理器数量。
这个处理池会一直运行到with块中最后一个语句执行完成,然后处理池被关闭。
不过,程序会一直等待直到所有提交的工作被处理完成 # 被提交到池中的工作必须被定义为一个函数。有两种方法去提交。
想让一个列表推导或一个 map() 操作并行执行的话,可使用 pool.map() # 执行大量工作的函数
def work(x): ... return result # 非并行码 # results = map(work, data) 在主进程里穿行执行work函数,x参数遍历data这个可迭代对象取 # 并行实现,这个调用多进程往进程池里提交work任务,参数x遍历data这个可迭代对象取 with ProcessPoolExecutor() as pool: results = pool.map(work, data)
# 还可以使用 pool.submit() 来手动的提交单个任务: # 某些功能 def work(x): ... return result with ProcessPoolExecutor() as pool: ... # 将工作提交到池的示例 future_result = pool.submit(work, arg) # 获取结果(块,直到进程完成) r = future_result.result() ... # 如果手动提交一个任务,结果是一个 Future 实例。
要获取最终结果,你需要调用它的 result() 方法。 它会阻塞进程直到结果被返回来
# 如果不想阻塞,可以使用一个回调函数:如下 def when_done(r): print('Got:', r.result()) with ProcessPoolExecutor() as pool: future_result = pool.submit(work, arg) future_result.add_done_callback(when_done) 回调函数接受一个 Future 实例(上面的when_done回调函数接收r这个Future实例),被用来获取最终的结果(比如通过调用它的result()方法)。
进程处理池很容易使用,在设计大程序的时候还是有很多需要注意的地方,如下几点:   这种并行处理技术只适用于那些可以被分解为互相独立部分的问题。   被提交的任务必须是简单函数形式。对于方法、闭包和其他类型的并行执行还不支持。   函数参数和返回值必须兼容pickle,因为要使用到进程间的通信,所有解释器之间的交换数据必须被序列化   被提交的任务函数不应保留状态或有副作用。除了打印日志之类简单的事情, 一旦启动后就不能控制子进程的任何行为,因此最好保持简单和纯洁——函数不要去修改环境。 在Unix上进程池通过调用 fork() 系统调用被创建, 它会克隆Python解释器,包括fork时的所有程序状态。 而在Windows上,克隆解释器时不会克隆状态。
实际的fork操作会在第一次调用 pool.map() 或 pool.submit() 发生 混合使用进程池和多线程的时候要特别小心。 应该在创建任何线程之前先创建并激活进程池(比如在程序启动的main线程中创建进程池)。
复制代码
复制代码
# 多进程ProcessPoolExecutor使用回调函数的简单实例
from concurrent.futures import ProcessPoolExecutor
import os
import time
import random

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

def when_done(r):
    print('Got:', r.result())
    # r传递的是结果对象,回调函数这里可以r.result取值

if __name__ == '__main__':
    with ProcessPoolExecutor() as pool:
        future_result = pool.submit(task, 10)
        future_result.add_done_callback(when_done)
复制代码

9:Python的全局锁问题

复制代码
# 全局解释器锁GIL,是否会影响到python多线程程序的执行性能。
# Python完全支持多线程编程,但是解释器的C语言实现部分在完全并行执行时并不是线程安全的。
实际上,解释器被一个全局解释器锁保护着,它确保任何时候都只有一个Python线程执行。
GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势
(比如一个使用了多个线程的计算密集型程序只会在一个单CPU上面运行)。
# 在讨论普通的GIL之前,有一点要强调的是GIL只会影响到那些严重依赖CPU的程序(比如计算型的)。
如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用多线程就很合适, 因为它们大部分时间都在等待。
实际上,你完全可以放心的创建几千个Python线程, 现代操作系统运行这么多线程没有任何压力,没啥可担心的 # 而对于依赖CPU的程序,需要弄清楚执行的计算的特点。 例如,优化底层算法要比使用多线程运行快得多。
类似的,由于Python是解释执行的,如果你将那些性能瓶颈代码移到一个C语言扩展模块中,速度也会提升的很快。
如果你要操作数组,那么使用NumPy这样的扩展会非常的高效。
最后,还可以考虑下其他可选实现方案,比如PyPy,它通过一个JIT编译器来优化执行效率
# 线程不是专门用来优化性能的。
一个CPU依赖型程序可能会使用线程来管理一个图形用户界面、一个网络连接或其他服务。
这时候,GIL会产生一些问题,因为如果一个线程长期持有GIL的话会导致其他非CPU型线程一直等待。
事实上,一个写的不好的C语言扩展会导致这个问题更加严重, 尽管代码的计算部分会比之前运行的更快些。 # 我们有两种策略来解决GIL的缺点 # 如果完全工作于Python环境中,你可以使用 multiprocessing 模块来创建一个进程池,并像协同处理器一样的使用它 # 有如下的线程代码: # 执行大型计算(CPU受限)
def some_work(args): ... return result # 调用上述函数的线程 def some_thread(): while True: ... r = some_work(args) ...
# 修改代码,使用进程池:
# 处理池(请参见下面的初始化)
pool = None # 执行大型计算(CPU受限) def some_work(args): ... return result # 调用上述函数的进程 def some_thread(): while True: ... r = pool.apply(some_work, (args)) ... # 初始化池 if __name__ == '__main__': import multiprocessing pool = multiprocessing.Pool()
# 这个通过使用一个技巧利用进程池解决了GIL的问题。 当一个线程想要执行CPU密集型工作时,会将任务发给进程池。
然后进程池会在另外一个进程中启动一个单独的Python解释器来工作。 当线程等待结果的时候会释放GIL。
并且,由于计算任务在单独解释器中执行,那么就不会受限于GIL了。
在一个多核系统上面,你会发现这个技术可以让你很好的利用多CPU的优势 # 另外一个解决GIL的策略是使用C扩展编程技术,将计算密集型任务转移给C,
跟Python独立,在工作的时候在C代码中释放GIL,可以通过在C代码中插入下面这样的特殊宏来完成:
#include "Python.h" ... PyObject *pyfunc(PyObject *self, PyObject *args) { ... Py_BEGIN_ALLOW_THREADS // Threaded C code ... Py_END_ALLOW_THREADS ... } # 如果使用其他工具访问C语言,比如对于Cython的ctypes库,不需要做任何事。 例如,ctypes在调用C时会自动释放GIL。
复制代码
复制代码
# 多线程的网络编程中神秘的 stalls 可能是因为其他原因比如一个DNS查找延时,而跟GIL毫无关系。
需要先去搞懂代码是否真的被GIL影响到。 同时还要明白GIL大部分都应该只关注CPU的处理而不是I/O. # 使用一个进程处理器池,注意的是这样做涉及到数据序列化和在不同Python解释器通信。
被执行的操作需要放在一个通过def语句定义的Python函数中,不能是lambda、闭包可调用实例等,
并且函数参数和返回值必须要兼容pickle。 同样,要执行的任务量必须足够大以弥补额外的通信开销。 # 一个难点是当混合使用线程和进程池的时候会让你很头疼。
如果你要同时使用两者,最好在程序启动时,创建任何线程之前先创建一个单例的进程池。
然后线程使用同样的进程池来进行它们的计算密集型工作。 # C扩展最重要的特征是它们和Python解释器是保持独立的。
也就是说,如果你准备将Python中的任务分配到C中去执行,
需要确保C代码的操作跟Python保持独立,这就意味着不要使用Python数据结构以及不要调用Python的C API。
另外一个就是你要确保C扩展所做的工作是足够的,值得你这样做。
也就是说C扩展担负起了大量的计算任务,而不是少数几个计算。 # 这些解决GIL的方案并不能适用于所有问题。例如,某些类型的应用程序如果被分解为多个进程处理的话并不能很好的工作,
也不能将它的部分代码改成C语言执行。 对于这些应用程序,要自己需求解决方案了
(比如多进程访问共享内存区,多解析器运行于同一个进程等)。 或者,你还可以考虑下其他的解释器实现,比如PyPy。
复制代码

10:定义一个Actor任务    想定义跟actor模式中类似“actors”角色的任务

复制代码
# actor模式是一种最古老的也是最简单的并行和分布式计算解决方案
#  简单来讲,一个actor就是一个并发执行的任务,只是简单的执行发送给它的消息任务。 
响应这些消息时,它可能还会给其他actor发送更进一步的消息。 actor之间的通信是单向和异步的。
因此,消息发送者不知道消息是什么时候被发送, 也不会接收到一个消息已被处理的回应或通知 # 结合使用一个线程和一个队列可以很容易的定义actor
from queue import Queue from threading import Thread, Event # 用于关机的哨子(定义一个错误类型) class ActorExit(Exception): pass class Actor: def __init__(self): self._mailbox = Queue() def send(self, msg): ''' 向actor发送消息 ''' self._mailbox.put(msg) def recv(self): ''' 接收传入消息 ''' msg = self._mailbox.get() if msg is ActorExit: raise ActorExit() return msg def close(self): ''' 关闭actor,从而将其关闭 ''' self.send(ActorExit) def start(self): ''' 开始并发执行 ''' self._terminated = Event() t = Thread(target=self._bootstrap) t.daemon = True t.start() def _bootstrap(self): try: self.run() except ActorExit: pass finally: self._terminated.set() def join(self): self._terminated.wait() def run(self): ''' 运行要由用户实现的方法 ''' while True: msg = self.recv() # ActorTask示例 class PrintActor(Actor): def run(self): while True: msg = self.recv() print('Got:', msg) # 样本使用 p = PrintActor() p.start()
# 调用start方法启动多线程,调用一个self._terminated = Event()创建一个信号对象

# 然后线程启动run(),处于一个while True:死循环,循环的接收msg(从队列里获取元素)
# 当run函数完成后self._terminated.set()解除所有的信号阻塞
p.send('Hello')  # 往队列里添加'Hello'
p.send('World')  # 往队列里添加'World'
p.close()     # 往队列里添加ActorExit
p.join()     # 主进程调用p这个类里面的join方法,阻塞住主线程等待子线程的run运行完发一个set取消阻塞的信号
# 上面使用actor实例的 send()方法发送消息给它们。 其机制是,这个方法会将消息放入一个队列中, 
然后将其转交给处理被接受消息的一个内部线程。
close() 方法通过在队列中放入一个特殊的哨兵值(ActorExit)来关闭这个actor。
用户可以通过继承Actor并定义实现自己处理逻辑run()方法来定义新的actor。
ActorExit 异常的使用就是用户自定义代码可以在需要的时候来捕获终止请求 (异常被get()方法抛出并传播出去)。

# 上面实现一个外部控制一个线程运行(控制他的任务执行和结束)
1:子线程的运行需要从队列get出一个数据然后子线程调用run函数内部的运行逻辑,如果没有数据阻塞在get这里
2:外部传递一个错误异常标志进去,子线程调用recv从队列拿到这个元素,子线程会raise报错,报错层层返回到_bootstrap函数接收
被try except逻辑接收处理后执行最后的
finally把被信号量阻塞的其他线程全部取消阻塞,继续运行
3:线程的主要运行逻辑需要写在run函数print位置里面
4:本质上就是子线程一直while死循环接收外部的传入来判定继续执行任务还是终止线程运行
# 放宽对于同步和异步消息发送的要求, 类actor对象还可以通过生成器来简化定义 def print_actor(): while True: try: msg = yield # 得到一个消息 print('Got:', msg) except GeneratorExit: print('Actor terminating') # Sample use p = print_actor() next(p) # Advance to the yield (ready to receive) p.send('Hello') p.send('World') p.close()
# 定义一个
print_actor生成器,往生成器里丢数据让生成器里的函数(print)运行
复制代码
复制代码
# actor模式简单,仅仅只有一个核心操作 send(),
  对于在基于actor系统中的“消息”的泛化概念可以已多种方式被扩展
# actor可以以元组形式传递标签消息,让actor执行不同的操作
from queue import Queue
from threading import Thread, Event

class ActorExit(Exception):
    pass

class Actor:
    def __init__(self):
        self._mailbox = Queue()

    def send(self, msg):
        self._mailbox.put(msg)

    def recv(self):
        msg = self._mailbox.get()
        if msg is ActorExit:
            raise ActorExit()
        return msg

    def close(self):
        self.send(ActorExit)

    def start(self):
        self._terminated = Event()
        t = Thread(target=self._bootstrap)

        t.daemon = True
        t.start()

    def _bootstrap(self):
        try:
            self.run()
        except ActorExit:
            pass
        finally:
            self._terminated.set()

    def join(self):
        self._terminated.wait()

    def run(self):
        while True:
            msg = self.recv()

class TaggedActor(Actor):
    def run(self):
        while True:
            tag, *payload = self.recv()
            getattr(self, 'do_' + tag)(*payload)

    # 与不同消息标记对应的方法
    def do_A(self, x):
        print('Running A', x)

    def do_B(self, x, y):
        print('Running B', x, y)

a = TaggedActor()
a.start()
a.send(('A', 1))  # Invokes do_A(1)
a.send(('B', 2, 3))  # Invokes do_B(2,3)
# 往Actor线程里丢了两个任务A和B
a.close()
a.join()
# 丢完两个任务后往Actor线程里丢个结束标识
# 最后调用a.join()阻塞主线程等待子线程完成为止
复制代码
复制代码
# actor允许在一个工作者中运行任意的函数,并且通过一个特殊的Result对象返回结果
from queue import Queue
from threading import Thread, Event

class ActorExit(Exception):
    pass

class Actor:
    def __init__(self):
        self._mailbox = Queue()

    def send(self, msg):
        self._mailbox.put(msg)

    def recv(self):
        msg = self._mailbox.get()
        if msg is ActorExit:
            raise ActorExit()
        return msg

    def close(self):
        self.send(ActorExit)

    def start(self):
        self._terminated = Event()
        t = Thread(target=self._bootstrap)

        t.daemon = True
        t.start()

    def _bootstrap(self):
        try:
            self.run()
        except ActorExit:
            pass
        finally:
            self._terminated.set()

    def join(self):
        self._terminated.wait()

    def run(self):
        while True:
            msg = self.recv()

class Result:
    def __init__(self):
        self._evt = Event()
        self._result = None

    def set_result(self, value):
        self._result = value
        self._evt.set()

    def result(self):
        self._evt.wait()
        return self._result

class Worker(Actor):
    def submit(self, func, *args, **kwargs):
        res = Result()
        self.send((func, args, kwargs, res))
        return res

    def run(self):
        while True:
            func, args, kwargs, res = self.recv()
            res.set_result(func(*args, **kwargs))

worker = Worker()
worker.start()  # 启动线程
r = worker.submit(pow, 2, 3)    # pow函数计算x的y次方
# Result()让Result类实例化然后把(pow, 2, 3, Result())这个元组丢到Actor的队列里面去
# 然后返回res这个Result()类的实例化对象
# 然后因为队列为空阻塞的Actor线程拿到队列里的这个元组,
# 运行res.set_result(func(*args, **kwargs))
# 调用pow(2, 3)这个函数后得到结果然后把函数执行结果8调用Result().set_result(8)
# 内部把结果赋值给self._result=8,然后子线程调用self._evt.set()把外部主线程r.result()一直阻塞等待运行结果取消阻塞让主线程得到运行的结果
worker.close()
worker.join()
print(r.result())

# “发送”一个任务消息的概念可以被扩展到多进程甚至是大型分布式系统中去。 
  例如,一个类actor对象的 send() 方法可以被编程让它能在一个套接字连接上传输数据 
  或通过某些消息中间件(比如AMQP、ZMQ等)来发送。
复制代码

11:实现消息发布/订阅模型  有一个基于线程通信的程序,想让它们实现发布/订阅模式的消息通信。

 

复制代码
# 实现发布/订阅的消息通信模式,通常要引入一个单独的“交换机”或“网关”对象作为所有消息的中介
# 不直接将消息从一个任务发送到另一个,而是将其发送给交换机,
  然后由交换机将它发送给一个或多个被关联任务    
如下:
from collections import defaultdict  # 默认值字典,如果键不存在返回一个default默认值

class Exchange:
    """交换机"""
    def __init__(self):
        self._subscribers = set()  # 订户集合

    def attach(self, task):
        self._subscribers.add(task)  # 添加task任务

    def detach(self, task):
        self._subscribers.remove(task)  # 删除task任务

    def send(self, msg):
        for subscriber in self._subscribers:
            subscriber.send(msg)

# 所有已创建交换的字典,Exchange是一个交换机类
_exchanges = defaultdict(Exchange)

# 返回与给定名称关联的Exchange实例
def get_exchange(name):
    return _exchanges[name]

# 一个交换机就是一个普通对象,负责维护一个活跃的订阅者集合,并为绑定、解绑和发送消息提供相应的方法。
# 每个交换机通过一个名称定位,get_exchange() 通过给定一个名称返回相应的 Exchange 实例

# 使用一个交换机
# 任务的示例。具有send()方法的任何对象
class Task:
    ...

    def send(self, msg):
        print(f'test_send:{msg}')
        ...

task_a = Task()
task_b = Task()

# 获得交换的示例
exc = get_exchange('name')
# 默认字典,当没有name这个键的时候从默认字典里返回Exchange对象
(默认字典defaultdict(Exchange)里如果传入的是一个类的话自动实例化返回) # 向其订阅任务的示例
exc.attach(task_a) exc.attach(task_b) # 发送消息的示例 exc.send('msg1') exc.send('msg2') # 取消订阅的示例 exc.detach(task_a) exc.detach(task_b) # 对于这个问题有很多的变种,不过万变不离其宗。 消息会被发送给一个交换机,然后交换机会将它们发送给被绑定的订阅者。

将需要发送的消息msg1
exc.send('msg1')发送给Exchange交换机,
交换机自动把这个msg1消息调用for循环来遍历所有绑定的对象,使用对象调用send(msg1)
就是让所有绑定Exchange交换机的对象都调用同一个函数
这个同一个函数可以是任何行为,比如发送消息啊,或者处理同一任务逻辑都可以
复制代码
复制代码
# 通过队列发送消息的任务或线程的模式很容易被实现并且也非常普遍。 不过,使用发布/订阅模式的好处更加明显。
# 使用一个交换机可以简化大部分涉及到线程通信的工作。 
无需去写通过多进程模块来操作多个线程,你只需要使用这个交换机来连接它们。
某种程度上,这个就跟日志模块的工作原理类似。 实际上,它可以轻松的解耦程序中多个任务。 # 交换机广播消息给多个订阅者的能力带来了一个全新的通信模式。
例如,可以使用多任务系统、广播或扇出。 你还可以通过以普通订阅者身份绑定来构建调试和诊断工具。 # 例如,下面是一个简单的诊断类,可以显示被发送的消息:
from collections import defaultdict class Exchange: """交换机""" def __init__(self): self._subscribers = set() def attach(self, task): self._subscribers.add(task) def detach(self, task): self._subscribers.remove(task) def send(self, msg): for subscriber in self._subscribers: subscriber.send(msg) _exchanges = defaultdict(Exchange) def get_exchange(name): return _exchanges[name] class DisplayMessages: def __init__(self): self.count = 0 def send(self, msg): self.count += 1 print('msg[{}]: {!r}'.format(self.count, msg)) exc = get_exchange('name') d = DisplayMessages() exc.attach(d)
# 该实现的一个重要特点是它能兼容多个“task-like”对象。
例如,消息接受者可以是actor、协程、网络连接或任何实现了正确的 send() 方法的东西。 # 关于交换机的一个可能问题是对于订阅者的正确绑定和解绑。
为了正确的管理资源,每一个绑定的订阅者必须最终要解绑。 在代码中通常会是像下面这样的模式
exc = get_exchange('name') exc.attach(some_task) try: ... finally: exc.detach(some_task)
复制代码
复制代码
# 这个和使用文件、锁和类似对象很像。 通常很容易会忘记最后的 detach() 解绑步骤。 
为了简化这个,使用上下文管理器协议。 # 例如,在交换机对象上增加一个 subscribe() 方法,
from contextlib import contextmanager from collections import defaultdict class Exchange: def __init__(self): self._subscribers = set() def attach(self, task): self._subscribers.add(task) def detach(self, task): self._subscribers.remove(task) @contextmanager def subscribe(self, *tasks): for task in tasks: self.attach(task) try: yield finally: for task in tasks: self.detach(task) def send(self, msg): for subscriber in self._subscribers: subscriber.send(msg) # 所有已创建交换的字典 _exchanges = defaultdict(Exchange) # 返回与给定名称关联的Exchange实例 def get_exchange(name): return _exchanges[name] # 使用subscribe()方法的示例 exc = get_exchange('name') with exc.subscribe(task_a, task_b): ... exc.send('msg1') exc.send('msg2') ...
with这里attach绑定task任务后运行send最后调用上下文管理器yield后面的代码detach取消绑定
# 任务a和任务b在此分离  # 应该注意的是关于交换机的思想有很多种的扩展实现。 
例如,交换机可以实现一整个消息通道集合或提供交换机名称的模式匹配规则。
交换机还可以被扩展到分布式计算程序中(比如,将消息路由到不同机器上面的任务中去)
复制代码

 12:使用生成器代替线程  使用生成器(协程)替代系统线程来实现并发:协程称位用户级线程或绿色线程

 

复制代码
# 使用生成器实现自己的并发,要对生成器函数和 yield 语句有深刻理解
# yield 语句会让一个生成器挂起它的执行,这样就可以编写一个调度器,将生成器当做某种“任务”并使用任务协作切换来替换它们的执行
# 下面两个使用简单的 yield 语句的生成器函数:
# 两个简单的生成函数
def countdown(n):
    while n > 0:
        print('T-minus', n)
        yield
        n -= 1
    print('Blastoff!')

def countup(n):
    x = 0
    while x < n:
        print('Counting up', x)
        yield
        x += 1
复制代码

 

复制代码
# 这些函数在内部使用yield语句,实现一个简单任务调度器的代码:
from collections import deque

class TaskScheduler:
    def __init__(self):
        self._task_queue = deque()  # deque双端队列,队列先进先出

    def new_task(self, task):
        """向计划程序承认新启动的任务"""
        self._task_queue.append(task)

    def run(self):
        """运行,直到没有更多任务为止"""
        while self._task_queue:     # 当这个双端队列不为空,往左边拿出一个任务(就是最前面丢进去的元素)
            task = self._task_queue.popleft()
            try:
                # 运行到下一个yield语句
                next(task)
                self._task_queue.append(task)
            except StopIteration:
                # 生成器不再执行
                pass

def countdown(n):
    while n > 0:
        print('T-minus', n)
        yield
        n -= 1
    print('Blastoff!')

def countup(n):
    x = 0
    while x < n:
        print('Counting up', x)
        yield
        x += 1

sched = TaskScheduler()
sched.new_task(countdown(10))
sched.new_task(countdown(5))
sched.new_task(countup(15))
# 把三个生成器丢进
sched.run()

# TaskScheduler 类在一个循环中运行生成器集合——每个都运行到碰到yield语句为止
# 每个生成器是一个任务,运行到一个生成器的一个yiled循环了,把这个生成器丢到双端队列最后,
取出下一个生成器运行,同理运行到一个yield终止,然后丢进队列拿出下一个生成器 # 所有上面三个任务(三个生成器)每个执行一个yield循环切换到下一个,
循环遍历三个生成器每次执行三个生成器里面的一次yield就切换

# 上面的代码已经实现了一个“操作系统”的最小核心部分,
# 生成器函数就是任务,而yield语句是任务挂起的信号。调度器循环检查任务列表直到没有任务要执行为止。
复制代码
复制代码
# 使用生成器来实现简单的并发,实现actor或网络服务器的时候使用生成器来替代线程的使用。
# 使用生成器来实现一个不依赖线程的actor:
from collections import deque

class ActorScheduler:
    """任务调度器"""
    def __init__(self):
        self._actors = {}  # 将姓名映射到行动者
        self._msg_queue = deque()  # 消息队列

    def new_actor(self, name, actor):   # name:'printer', actor:printer()
        """允许新启动的参与者加入调度程序并为其命名"""
        self._msg_queue.append((actor, None))
        self._actors[name] = actor
        # 往双端队列_msg_queue里添加(printer(), None)和(counter(sched), None)
        # 往列表_actors里添加'printer': printer()  和'counter':counter(sched)

    def send(self, name, msg):
        """向指定的参与者发送消息"""
        actor = self._actors.get(name)
        if actor:
            self._msg_queue.append((actor, msg))

    def run(self):
        """只要有挂起的消息,就运行。"""
        while self._msg_queue:
            actor, msg = self._msg_queue.popleft()
            try:
                actor.send(msg)
            except StopIteration:
                pass

def printer():
    """打印机,任务函数"""
    while True:
        msg = yield
        print('Got:', msg)

def counter(sched):
    """计时器"""
    while True:
        # 接收当前计数
        n = yield
        if n == 0:
            break
        # 发送到打印机任务
        sched.send('printer', n)
        # 将下一个计数发送到计数器任务(递归)
        sched.send('counter', n - 1)

sched = ActorScheduler()
# 创建最初的参与者
sched.new_actor('printer', printer())
sched.new_actor('counter', counter(sched))

# 上面代码关键点在于收集消息的队列。
# 本质上,调度器在有需要发送的消息时会一直运行着。 计数生成器会给自己发送消息并在一个递归循环中结束

sched.send('counter', 10000)
# 向计数器发送初始消息以启动,像计时器counter发送10000计数
# actor = self._actors.get('counter')——>得到counter(sched)——> actor = counter(sched)
# 如果找到了actor,self._msg_queue.append((actor,msg))
# 向_msg_queue这个双端队列添加((counter(sched),10000))
# 那么现在_msg_queue有三个元素(printer(), None)+(counter(sched), None)+((counter(sched),10000))
sched.run()
# 这里run运行,while死循环只要_msg_queue这个队列不为空一直往里面左边取值,得到actor(生成器), msg(取到队列为空为止)然后调用actor.send(msg)
# 先调用printer生成器send一个None进去驱动生成器
# 调用counter生成器send一个None进去驱动生成器

# counter生成器  send   10000   给n
  # 现在counter生成器被触发了n接收到10000,进行if判断后
  # 往_msg_queue队列里丢一个(printer(), 10000)
  # 往_msg_queue队列里丢一个(counter(sched), 9999)

# 上面轮询了一次后队列里就只剩下(printer(), 10000)+(counter(sched), 9999)
# 后面继续while循环拿出来队列里面的元素,(printer(), 10000)+(counter(sched), 9999)
# 生成器printer  send  10000进去,打印:Got: 10000
# 生成器counter send  9999进去,如上_msg_queue队列里丢一个(printer(), 9999)+(counter(sched), 9998)

# 继续上面的循环9999次
# 最后队列里存在:(printer(), 1)+(counter(sched), 0)
# 生成器printer    send    1  进去,打印:Got: 1
# 生成器counter    send    0  进去进行if判断n == 0的时候break终止循环让这个迭代器终止

# 所有此时self._msg_queue这个队列为空了,取不出popleft()。
# while self._msg_queue:这个队列为空退出循环,主程序终止

一个actor就是一个任务,actor模式一个actor就是一个任务,一个actor就是一个线程,不断往线程里提交需要运行的函数就行
这里的例子
ActorScheduler是调度器,
printer打印机是一个actor任务函数,counter计数器也是一个actor任务函数,
这里需要运行打印机函数1w次,
完全使用yield生成器实现,类似应用调度器模拟两个任务线程的并发,
复制代码
# 使用生成器来实现一个并发网络应用程序:

 

 

 

 

 

 

 

 

 
posted @   至高无上10086  阅读(116)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示