pyhton - parallel - programming - cookbook(THREAD)
基于线程的并行
通常,一个应用有一个进程,分成多个独立的线程,并行运行、互相配合,执行不同类型的任务。
线程是独立的处理流程,可以和系统的其他线程并行或并发地执行。多线程可以共享数据和资源,利用所谓的共享内存空间。线程和进程的具体实现取决于你要运行的操作系统。但是总的来说,线程是包含在进程中的,同一进程的多个不同的线程可以共享相同的资源,对于而言,进程之间不会共享资源。
每一个线程基本上包含3个元素:程序计数器,寄存器和栈。与同一进程的其他线程共享的资源基本上包括数据和系统资源。每一个线程也有自己的运行状态,可以和其他线程同步,这点和进程一样。线程的状态大体上可以分为ready, running, blocked. 线程的典型应用是应用软件的并行化 -- 为例充分利用现代化的多核处理器,使每个核心可以运行单个线程。相比于进程,使用线程的优势主要是性能。相比之下,在进程之间切换上下文要比在统一进程的多线程之间切换上下文要重的多。
多线程编程一般使用共享内容空间进行线程间的通讯。这就使得管理内容空间成为多线程编程的重点和难点。
使用python的线程模块
python通过标准库的threading模块来管理线程。
线程模块的主要组件:
-
线程对象
-
Lock对象
-
Rlock对象
-
信号对象
-
条件对象
-
事件对象
定义一个线程
使用线程最简单的方式是,用一个目标函数实例化一个Thread然后调用start()方法启动它。
-
Target:当线程启动的时候要执行的函数
-
name:线程的名字,默认会分配一个唯一的名字
-
args: 传递target的参数,要适用tuple类型
-
1 import threading 2 #导入内置的threading模块 3 4 5 def function(i): 6 print("function called by thread %i\n" % i) 7 return 8 9 threads = [] 10 11 for i in range(5): 12 t = threading.Thread(target=function, args=(i,)) 13 #使用目标函数function初始化一个线程对象Thread, 14 #还传入用于打印的一个参数 15 #线程被创建之后并不会马上运行,需要手动调用start(), join()让调用它的线程一直 16 #等待直到执行结束(即阻塞调用它的主线程,t线程执行结束,主线程才会继续执行) 17 18 threads.append(t) 19 t.start() 20 21 for thread in threads: 22 thread.join()
如何确定当前的线程
每一个Thread实例创建的时候都有一个带默认的名字,并且可以修改的。在服务端通常一个服务进程都有多个线程服务,负责不同的操作,这时命名线程是很有用的。
1 import threading 2 import time 3 4 def first_function(): 5 print(threading.currentThread().getName() + str(' is Starting ')) 6 time.sleep(2) 7 print(threading.currentThread().getName() + str(' is Exiting ')) 8 return 9 def second_function(): 10 print(threading.currentThread().getName() + str(' is Starting ')) 11 time.sleep(2) 12 print(threading.currentThread().getName() + str(' is Exiting ')) 13 return 14 def thrid_function(): 15 print(threading.currentThread().getName() + str(' is Starting ')) 16 time.sleep(2) 17 print(threading.currentThread().getName() + str(' is Exiting ')) 18 return 19 20 if __name__ == "__main__": 21 t1 = threading.Thread(name='first_function', target=first_function) 22 t2 = threading.Thread(name='second_function', target=second_function) 23 t3 = threading.Thread(target=thrid_function) 24 # 使用目标函数实例化线程,同时传入name参数,作为线程的名字,如果不传入这个参数,将使用默认的参数。 25 # 默认输出的线程的名字是Thread-1 26 t1.start() 27 t2.start() 28 t3.start() 29 t1.join() 30 t2.join() 31 t3.join()
实现一个线程
当创建了新的Thread子类的时候,可以实例化这个类,调用start()方法来启动它。
# 如何实现一个线程 # 使用threading 模块实现一个新的线程,需要三步: # 定义一个Thread类 # 重写__init__(self, [,args])方法,可以添加额外的参数 # 需要重写run(self, [,args])方法来实现线程要做的事情
#threading 模块是创建和管理线程的首选形式,每一个线程都通过一个继承Thread类, #重写run()方法,来实现逻辑,这个方法就是线程的入口,在主程序中,我们创建来多个myThread #的类型,然后执行start()方法,启动它们。 #调用Thread.__init__构造方法是必须的,通过ta我们可以给线程定义 #一些名字或分组之类的属性,调用start()之后线程变为活跃状态。并且持续直到run()结束。或者中间出现异常。 #所有的线程都执行完成之后,程序结束。 import threading import time import _thread exitFlag = 0 class myThread(threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threading self.name = name self.counter = counter def run(self): print("Starting " + self.name) self.print_time(self.name, self.counter, 5) print("Exiting " + self.name) @staticmethod def print_time(threadName, delay, counter): while counter: if exitFlag: _thread.exit() time.sleep(delay) print("%s: %s" % (threadName, time.ctime(time.time()))) counter -= 1 thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) thread1.start() thread2.start() thread1.join() thread2.join() print("Exiting Main Thread")
threading 模块是创建和管理线程的首选形式,每一个线程都通过一个继承Thread类,
重写run()方法,来实现逻辑,这个方法就是线程的入口,在主程序中,我们创建来多个myThread
的类型,然后执行start()方法,启动它们。调用Thread. __ init __构造方法是必须的,通过它我们可以给线程定义一些名字或分组之类的属性,调用start()之后线程变为活跃状态。并且持续直到run()结束。或者中间出现异常。所有的线程都执行完成之后,程序结束。
join()命令控制主线程的终止。
使用lock进行线程同步
当两个或以上那对共享内存的操作发生在并发线程中,并且至少有一个可以改变数据,又没有同步机制的条件下,就会产生竞争条件,可能会导致执行无效代码,bug,或异常行为。
竞争条件最简单的解决方法是使用锁。锁的操作非常简单,当一个线程需要访问部分共享内存时,它必须先获得锁才能访问。此线程对这部分共享资源使用完成之后,该线程必须释放锁。然后其他线程就可以拿到这个锁并访问这部分资源了。
需要保证,在同一时刻只有一个线程允许访问共享内存。
缺点,当不同的线程要求得到一个锁时,死锁就会发生,这时程序不可能继续执行,因为它们互相拿着对方需要的锁。
两个并发的线程(A, B),需要资源1和资源2,假设线程A需要资源1,线程B需要资源2.在这种情况下,两个线程都使用各自的锁,目前为止没有冲突。现在假设,在对方释放锁之前,线程A需要资源2的锁,线程B需要资源1的锁,没有资源线程不会继续执行。鉴于目前两个资源都是被占用的。而且在对方的锁释放之前都处于等待且不释放锁的状态。这是死锁的典型情况。
因此使用锁解决同步问题是一个存在潜在问题的方案。
通过它我们可以将共享资源某一时刻的访问限制在单一线程或单一类型的线程上
线程必须得到锁才能使用资源,并且之后必须允许其他线程使用相同的资源。
import threading # 锁有两种状态: locked和unlocked # 有两个方法来操作锁: acquire() release() # 如果状态是unlocked,可以调用acquire()将状态改为locked # 如果状态是locked,acquire()会被block直到另一个线程调用release()释放锁 # 如果状态是unlocked,调用release()将导致runtimeError异常 # 如果状态是locked,可以调用release()将状态改为unlocked shared_resource_with_lock = 0 shared_resource_with_no_lock = 0 COUNT = 100000 shared_resource_lock = threading.Lock() def increment_with_lock(): global shared_resource_with_lock for i in range(COUNT): shared_resource_lock.acquire() shared_resource_with_lock += 1 shared_resource_lock.release() def decrement_with_lock(): global shared_resource_with_lock for i in range(COUNT): shared_resource_lock.acquire() shared_resource_with_lock -= 1 shared_resource_lock.release() def increment_without_lock(): global shared_resource_with_no_lock for i in range(COUNT): shared_resource_with_no_lock += 1 def decrement_without_lock(): global shared_resource_with_no_lock for i in range(COUNT): shared_resource_with_no_lock -= 1 if __name__ == "__main__": t1 = threading.Thread(target=increment_with_lock) t2 = threading.Thread(target=decrement_with_lock) t3 = threading.Thread(target=increment_without_lock) t4 = threading.Thread(target=decrement_without_lock) t1.start() t2.start() t3.start() t4.start() t1.join() t2.join() t3.join() t4.join() print("the value of shared variable with lock management is %s" % shared_resource_with_lock) print("the value of shared variable with race condition is %s" % shared_resource_with_no_lock)
使用Rlock进行线程同步
想让只有拿到锁的线程才能释放该锁,那么应该使用Rlock()对象。
当你需要在外面保证线程安全,又要在类内使用同样方法的时候Rlock()就很实用了。
1 # 只有拿到锁的线程才能释放该锁,那么应该使用Rlock() 2 # acquire() release() 3 # 特点: 4 # 1, 谁拿到谁释放, 如果线程A拿到锁,线程B无法释放这个锁,只有A可以释放, 5 # 2, 同一线程可以多次拿到该锁,即可以acquire多次 6 # 3, acquire多少次就必须release多少次,只有最后一次release才能改变Rlock的状态为unlocked 7 8 import threading 9 import time 10 11 class Box(object): 12 lock = threading.RLock() 13 14 def __init__(self): 15 self.total_items = 0 16 17 def execute(self, n): 18 # Box.lock.acquire() 19 self.total_items += n 20 # Box.lock.release() 21 22 def add(self): 23 Box.lock.acquire() 24 self.execute(1) 25 Box.lock.release() 26 27 def remove(self): 28 Box.lock.acquire() 29 self.execute(-1) 30 Box.lock.release() 31 32 # These two functions run n in separate 33 # threads and call the Box's methods 34 def adder(box, items): 35 while items > 0: 36 print("adding 1 item in the box") 37 box.add() 38 time.sleep(1) 39 items -= 1 40 41 def remover(box, itmes): 42 while items > 0: 43 print("remove 1 item in the box") 44 box.remove() 45 time.sleep(1) 46 itmes -= 1 47 48 # the main program build some 49 # threads and make sure it works 50 if __name__ == "__main__": 51 items = 5 52 print("putting %s items in the box" % items) 53 box = Box() 54 t1 = threading.Thread(target=adder, args=(box, items)) 55 t2 = threading.Thread(target=remover, args=(box, items)) 56 t1.start() 57 t2.start() 58 59 t1.join() 60 t2.join() 61 print("%s items still remain in the box" % box.total_items)
使用信号量进行线程同步
信号量由Dijkstra发明并第一次应用在操作系统中,信号量是由操作系统管理的一种抽象数据类型,用于在多线程中同步对共享资源的使用。本质上说,信号量是一个内部数据,用于标明当前的共享资源可以有多少并发读取。
-
每当线程想要读取关联了信号量的共享资源时,必须调用acquire(),此操作减少信号量的内部变量,如果此变量的值非负,那么分配该资源的权限。如果是负值,那么线程被挂起, 直到有其他的线程释放资源。
-
当线程不再需要该共享资源,必须通过release()释放,这样,信号量的内部变量增加,在信号量等待队列中排在最前面的线程会拿到共享资源的权限。
虽然表面上看信号机制没有什么明显的问题,如果信号量的等待和通知都是原子的,确实没什么问题,但如果不是,或者两个操作有一个终止了,就会导致糟糕的情况。
例如, 假如有两个并发的线程,都在等待一个信号量,目前信号量的内部值为1,假设线程A将信号量的值从1减到0,这时候控制权切换到了线程B,线程B将信号量的值从0减到-1, 并且在这里被挂起等待,这时控制权回到线程A,信号量已经成为了负值,于是第一个线程也在等待。
这样的话,尽管当时的信号量是可以让线程访问资源的,但是因为非原子操作导致了所有线程都在等待状态。
1 ''' 2 Using a Semaphore to synchronize threads 3 ''' 4 import threading 5 import time 6 import random 7 8 # The optional argument gives the initial value for the 9 # internal counter; 10 # it default to 1; 11 # if the value given is less than 0, ValueError is raised. 12 semaphore = threading.Semaphore(0) 13 14 #信号量被初始化为0,此信号量唯一的目的是同步两个 15 #或多个线程。 16 # item = 0 17 def consumer(): 18 print("consumer is waiting") 19 # Acquire a semaphore 20 semaphore.acquire() 21 # 如果信号量的计数器到量0, 就会阻塞acuire() 方法,直到得到另一个线程的通知。 22 # 如果信号量的计数器大于0, 就会对这个值减1,然后分配资源。 23 24 # The consumer have access to the shared resource 25 print("Consumer notify: consumed item number %s" % item) 26 27 def produce(): 28 global item 29 time.sleep(10) 30 # create a random item 31 item = random.randint(0, 1000) 32 print("producer notify: produced item number %s " % item) 33 34 # Release a semaphore, incrementing the 35 # internal counter by one. 36 # when it is zero on entry and another thread is waiting for it 37 # to become lagrer than zero again, wake up 38 # that thread. 39 semaphore.release() 40 # 信号量的release()可以提高计数器然后通知其他线程。 41 42 43 if __name__ == "__main__": 44 for _i in range(0, 5): 45 t1 = threading.Thread(target=produce) 46 t2 = threading.Thread(target=consumer) 47 t1.start() 48 t2.start() 49 t1.join() 50 t2.join() 51 print("program terminated")
信号量的一个特殊用法是互斥量,互斥量是初始值为1的信号量,可以实现数据、资源的互斥访问。
可能发生死锁,例如,现在有一个线程t1先等待信号量s1,然后等待信号量s2,而线程t2会先等待信号s2,然后在等待信号量s1,这样就可能发生死锁,导致t1等待t2,但是t2在等待s1.
使用条件进行线程同步
条件是指应用程序状态的改变。这是另一种同步机制。其中某些线程在等待某一条件发生,其他的线程会在该条件发生的时候进行通知。一旦条件发生,线程会拿到共享资源的唯一权限。
生产者-消费者
只要缓存不满,生产者一直向缓存生产,只要缓存不空,消费者一直从缓存取出。当缓存队列不为空的时候。生产者将通知消费者;当缓冲队列不满的时候,消费者将通知生产者。
1 from threading import Thread, Condition 2 import time 3 4 items = [] 5 condition = Condition() 6 7 class consumer(Thread): 8 9 def __init__(self): 10 Thread.__init__(self) 11 12 def consume(self): 13 global condition 14 global items 15 # 消费者通过拿到锁来修改共享的资源 16 condition.acquire() 17 18 if len(items) == 0: 19 # 如果list长度为0,那么消费者就会进入等待状态 20 condition.wait() 21 print("Consumer notify: no item to consume") 22 23 items.pop() 24 25 print("Consumer notify : consumed 1 item") 26 print("Consumer notify : items to consume are " + str(len(items))) 27 28 # 消费者的状态呗通知给生产者,同时共享资源释放 29 condition.notify() 30 condition.release() 31 32 def run(self): 33 for _i in range(0, 20): 34 time.sleep(2) 35 self.consume() 36 37 class producer(Thread): 38 39 def __init__(self): 40 Thread.__init__(self) 41 42 def produce(self): 43 global condition 44 global items 45 46 #生产者拿到共享资源,然后确定缓存队列是否已满, 47 #如果已经满了,那么生产者进入等待状态,直到呗唤醒 48 condition.acquire() 49 if len(items) == 10: 50 condition.wait() 51 print("Produce notify : items producted are " + str(len(items))) 52 print("Produce notify : stop the production!") 53 54 items.append(1) 55 56 print("Produce notify : total items produced " + str(len(items))) 57 58 # 通知状态释放资源 59 condition.notify() 60 condition.release() 61 62 def run(self): 63 for _i in range(0, 20): 64 time.sleep(1) 65 self.produce() 66 67 if __name__ == "__main__": 68 producer = producer() 69 consumer = consumer() 70 producer.start() 71 consumer.start() 72 producer.join() 73 consumer.join()
Condition.acquire()之后就在.wait()了,好像会一直持有锁。其实.wait()会将锁释放,然后等待其他线程.notify()之后会重新获得锁。但要注意的.notify()并不会自动释放,所以代码中有两行,先.notify()然后再.release()
开三个线程按照顺序打印ABC 10次
1 """ 2 Three threads print A B C in order 3 """ 4 5 from threading import Thread, Condition 6 7 condition = Condition() 8 current = "A" 9 10 class ThreadA(Thread): 11 def run(self): 12 global current 13 for _ in range(10): 14 with condition: 15 while current != "A": 16 condition.wait() 17 print("A", end=" ") 18 current = "B" 19 condition.notify_all() 20 21 class ThreadB(Thread): 22 def run(self): 23 global current 24 for _ in range(10): 25 with condition: 26 while current != "B": 27 condition.wait() 28 print("B", end= " ") 29 current = "C" 30 condition.notify_all() 31 32 class ThreadC(Thread): 33 def run(self): 34 global current 35 for _ in range(10): 36 with condition: 37 while current != "C": 38 condition.wait() 39 print("C", end= " ") 40 current = "A" 41 condition.notify_all() 42 43 a = ThreadA() 44 b = ThreadB() 45 c = ThreadC() 46 47 a.start() 48 b.start() 49 c.start() 50 51 a.join() 52 c.join() 53 b.join()
使用事件进行线程同步
事件是线程之间用于通讯的对象。有的线程等待信号。有的线程发出信号。
基本上事件对象会维护一个内部变量。可以通过set()方法设置为true.也可以通过clear()方法设置为false,wait()方法将会阻塞线程。直到内部变量为true.
1 import time 2 from threading import Thread, Event 3 import random 4 5 items = [] 6 event = Event() 7 8 class consumer(Thread): 9 def __init__(self, items, event): 10 Thread.__init__(self) 11 self.items = items 12 self.event = event 13 14 def run(self): 15 while True: 16 time.sleep(2) 17 self.event.wait() 18 # wait()方法将会阻塞线程。直到内部变量为true 19 item = self.items.pop() 20 print("Consumer notify : %d popped from list by %s" % (item, self.name)) 21 22 class producer(Thread): 23 def __init__(self, items, event): 24 Thread.__init__(self) 25 self.items = items 26 self.event = event 27 28 def run(self): 29 global item 30 for _i in range(100): 31 time.sleep(2) 32 item = random.randint(0, 256) 33 self.items.append(item) 34 print("Producer notify : item N %d appended to list by %s" % (item, self.name)) 35 print("Producer notify : event set by %s " % self.name) 36 # producer 类将新item 添加到list末尾然后发出事件通知。使用事件有两步, 37 # 第一: self.event.set() 38 # 第二步: self.event.clear() 39 self.event.set() 40 # set()方法设置为true 41 print("Produce notify : event cleared by %s" % self.name) 42 self.event.clear() 43 44 if __name__ == "__main__": 45 t1 = producer(items, event) 46 t2 = consumer(items, event) 47 t1.start() 48 t2.start() 49 t1.join() 50 t2.join()
使用with语法
在有两个相关的操作需要在一部分代码块前后分别执行的时候,可以使用with语句自动完成。
同时使用with语句可以在特定的地方分配和释放资源,因此,with语法也叫做“上下文管理器”。
在threading模块中,所有带有acquire()方法和release()方法的对象都可以使用上下文管理器。
可以使用with语法:
-
Lock
-
Rlock
-
Condition
-
Semaphore
使用queue进行线程通信
使用队列可以将资源的使用通过单线程进行完全控制,并且允许使用更加整洁和可读性更高的设计模式。
queue常用的方法有以下四个:
-
Put():往queue中放一个item
-
Get(): 从queue中删除一个item,并返回删除的这个item
-
Task_done():每次被处理的时候需要调用这个方法
-
join():所有item都被处理之前一直阻塞
生产者使用queue.put(item, [,block[, timeout]])来往queue中插入数据。queue是同步的,在插入数据之前内部有一个内置的锁机制。
可能发生两种情况:
-
如果block为true, timeout为None(这也是默认的选项)那么可能会阻塞掉,直到出现可用的位置。如果timeout是正整数,那么阻塞直到这个时间,就会抛出一个异常。
-
如果block为false, 如果队列有闲置那么会立即插入,否则就立即抛出异常(timeout将会被忽略)。put()检查队列是否已满,然后调用wait()开始等待。
消费者从队列中取出蒸熟然后用task_done()方法将其标记为任务已处理。
消费者使用Queue.get([block[, timeout]])从队列中取出数据,queue内部也会经过锁的处理。如果队列为空,消费者阻塞。
1 from threading import Thread, Event 2 from queue import Queue 3 import time 4 import random 5 6 class produce(Thread): 7 def __init__(self, queue): 8 Thread.__init__(self) 9 self.queue = queue 10 11 def run(self): 12 for i in range(10): 13 item = random.randint(0, 256) 14 self.queue.put(item) 15 print("Producer notify item N %d appeded to queue by %s " % (item, self.name)) 16 time.sleep(1) 17 18 class consumer(Thread): 19 def __init__(self, queue): 20 Thread.__init__(self) 21 self.queue = queue 22 23 def run(self): 24 while True: 25 # if queue.empty(): 26 # break 27 print("queue is empty ", queue.empty()) 28 item = self.queue.get() 29 print("Consumer notify : %d popped from queue by %s" % (item, self.name)) 30 self.queue.task_done() 31 32 if __name__ == "__main__": 33 queue = Queue() 34 t1 = produce(queue) 35 t2 = consumer(queue) 36 t3 = consumer(queue) 37 t4 = consumer(queue) 38 39 t1.start() 40 t2.start() 41 t3.start() 42 t4.start() 43 t1.join() 44 t2.join() 45 t3.join() 46 t4.join() 47
评估多线程应用的性能
GIL是Cpython解释器引入的锁,GIL在解释器层阻止了真正的并行运行。解释器在执行任何线程之前。必须等待当前正在运行的线程释放GIL。事实上,解释器会强迫想要运行的线程必须拿到GIL才能访问解释器的任何资源,例如栈或python对象。这也是GIL的目的 --- 阻止不同的线程并发访问Python对象。这样GIL可以保护解释器的内存。让垃圾回收工作正常。但事实上,这却造成了程序员无法通过并行执行多线程来提高程序的性能。如果去掉Cpython的GIL,就可以让多线程真正并行执行。GIL并没有影响多线程处理器并行的线程。只是限制了一个解释器只能有一个线程运行。
1 from threading import Thread 2 3 class threads_object(Thread): 4 def run(self): 5 function_to_run() 6 7 class nothreads_object(object): 8 def run(self): 9 function_to_run() 10 11 def non_threaded(num_iter): 12 funcs = [] 13 for i in range(int(num_iter)): 14 funcs.append(nothreads_object()) 15 for i in funcs: 16 i.run() 17 18 def threaded(num_iter): 19 funcs = [] 20 for i in range(int(num_iter)): 21 funcs.append(threads_object()) 22 for i in funcs: 23 i.start() 24 for i in funcs: 25 i.join() 26 27 def function_to_run(): 28 # test 1 29 pass 30 # test 2 31 # a, b = 0, 1 32 # for i in range(10000): 33 # a, b = b, a + b 34 # test 3 35 # fh = open("./queue_test.py", "rb") 36 # size = 1024 37 # for i in range(1000): 38 # fh.read(size) 39 40 #test 4 41 # import urllib.request 42 # for i in range(10): 43 # with urllib.request.urlopen("https://www.baidu.com") as f: 44 # f.read(1024) 45 46 def show_results(func_name, results): 47 print("%-23s %4.6f seconds" % (func_name, results)) 48 49 if __name__ == "__main__": 50 import sys 51 from timeit import Timer 52 repeat = 100 53 number = 1 54 num_threads = [1, 2, 4, 8] 55 print("Starting tests") 56 for i in num_threads: 57 t = Timer("non_thread(%s)" % i, "from __main__ import non_thread") 58 best_result = min(t.repeat(repeat=repeat, number=number)) 59 t = Timer("threaded(%s)" % i, "from __main__ import threaded") 60 best_result = min(t.repeat(repeat=repeat, number=number)) 61 show_results("thread (%s threads)" %i , best_result) 62 print("Iterations complete")