您的位置:首页 > 编程语言 > Python开发

Python并发编程

2016-07-29 00:00 232 查看

简介

我们将一个正在运行的程序称为进程。每个进程都有它自己的系统状态,包含内存状态、打开文件列表、追踪指令执行情况的程序指针以及一个保存局部变量的调用栈。通常情况下,一个进程依照一个单序列控制流顺序执行,这个控制流被称为该进程的主线程。在任何给定的时刻,一个程序只做一件事情。

一个程序可以通过Python库函数中的os或subprocess模块创建新进程(例如os.fork()或是subprocess.Popen())。然而,这些被称为子进程的进程却是独立运行的,它们有各自独立的系统状态以及主线程。因为进程之间是相互独立的,因此它们同原有的进程并发执行。这是指原进程可以在创建子进程后去执行其它工作。

虽然进程之间是相互独立的,但是它们能够通过名为进程间通信(IPC)的机制进行相互通信。一个典型的模式是基于消息传递,可以将其简单地理解为一个纯字节的缓冲区,而send()或recv()操作原语可以通过诸如管道(pipe)或是网络套接字(network socket)等I/O通道传输或接收消息。还有一些IPC模式可以通过内存映射(memory-mapped)机制完成(例如mmap模块),通过内存映射,进程可以在内存中创建共享区域,而对这些区域的修改对所有的进程可见。

多进程能够被用于需要同时执行多个任务的场景,由不同的进程负责任务的不同部分。然而,另一种将工作细分到任务的方法是使用线程。同进程类似,线程也有其自己的控制流以及执行栈,但线程在创建它的进程之内运行,分享其父进程的所有数据和系统资源。当应用需要完成并发任务的时候线程是很有用的,但是潜在的问题是任务间必须分享大量的系统状态。

当使用多进程或多线程时,操作系统负责调度。这是通过给每个进程(或线程)一个很小的时间片并且在所有活动任务之间快速循环切换来实现的,这个过程将CPU时间分割为小片段分给各个任务。例如,如果你的系统中有10个活跃的进程正在执行,操作系统将会适当的将十分之一的CPU时间分配给每个进程并且循环地在十个进程之间切换。当系统不止有一个CPU核时,操作系统能够将进程调度到不同的CPU核上,保持系统负载平均以实现并行执行。

利用并发执行机制写的程序需要考虑一些复杂的问题。复杂性的主要来源是关于同步和共享数据的问题。通常情况下,多个任务同时试图更新同一个数据结构会造成脏数据和程序状态不一致的问题(正式的说法是资源竞争的问题)。为了解决这个问题,需要使用互斥锁或是其他相似的同步原语来标识并保护程序中的关键部分。举个例子,如果多个不同的线程正在试图同时向同一个文件写入数据,那么你需要一个互斥锁使这些写操作依次执行,当一个线程在写入时,其他线程必须等待直到当前线程释放这个资源。

Python中的并发编程

Python长久以来一直支持不同方式的并发编程,包括线程、子进程以及其他利用生成器(generator function)的并发实现。

Python在大部分系统上同时支持消息传递和基于线程的并发编程机制。虽然大部分程序员对线程接口更为熟悉,但是Python的线程机制却有着诸多的限制。Python使用了内部全局解释器锁(GIL)来保证线程安全,GIL同时只允许一个线程执行。这使得Python程序就算在多核系统上也只能在单个处理器上运行。Python界关于GIL的争论尽管很多,但在可预见的未来却没有将其移除的可能。

Python提供了一些很精巧的工具用于管理基于线程和进程的并发操作。即使是简单地程序也能够使用这些工具使得任务并发进行从而加快运行速度。subprocess模块为子进程的创建和通信提供了API。这特别适合运行与文本相关的程序,因为这些API支持通过新进程的标准输入输出通道传送数据。signal模块将UNIX系统的信号量机制暴露给用户,用以在进程之间传递事件信息。信号是异步处理的,通常有信号到来时会中断程序当前的工作。信号机制能够实现粗粒度的消息传递系统,但是有其他更可靠的进程内通讯技术能够传递更复杂的消息。threading模块为并发操作提供了一系列高级的,面向对象的API。Thread对象们在一个进程内并发地运行,分享内存资源。使用线程能够更好地扩展I/O密集型的任务。multiprocessing模块同threading模块类似,不过它提供了对于进程的操作。每个进程类是真实的操作系统进程,并且没有共享内存资源,但multiprocessing模块提供了进程间共享数据以及传递消息的机制。通常情况下,将基于线程的程序改为基于进程的很简单,只需要修改一些import声明即可。

Threading模块示例

以threading模块为例,思考这样一个简单的问题:如何使用分段并行的方式完成一个大数的累加。

import threading

class SummingThread(threading.Thread):
def __init__(self, low, high):
super(SummingThread, self).__init__()
self.low = low
self.high = high
self.total = 0

def run(self):
for i in range(self.low, self.high):
self.total += i

thread1 = SummingThread(0, 500000)
thread2 = SummingThread(500000, 1000000)
thread1.start() # This actually causes the thread to run
thread2.start()
thread1.join()  # This waits until the thread has completed
thread2.join()
# At this point, both threads have completed
result = thread1.total + thread2.total
print(result)


自定义Threading类库

我写了一个易于使用threads的小型Python类库,包含了一些有用的类和函数。

关键参数:

* do_threaded_work – 该函数将一系列给定的任务分配给对应的处理函数(分配顺序不确定)
* ThreadedWorker – 该类创建一个线程,它将从一个同步的工作队列中拉取工作任务并将处理结果写入同步结果队列
* start_logging_with_thread_info – 将线程id写入所有日志消息。(依赖日志环境)
* stop_logging_with_thread_info – 用于将线程id从所有的日志消息中移除。(依赖日志环境)

import threading
import logging
import Queue

def do_threaded_work(work_items, work_func, num_threads=None, per_sync_timeout=1, preserve_result_ordering=True):
""" Executes work_func on each work_item. Note: Execution order is not preserved, but output ordering is (optionally).

Parameters:
- num_threads               Default: len(work_items)  --- Number of threads to use process items in work_items.
- per_sync_timeout          Default: 1                --- Each synchronized operation can optionally timeout.
- preserve_result_ordering  Default: True             --- Reorders result_item to match original work_items ordering.

Return:
--- list of results from applying work_func to each work_item. Order is optionally preserved.

Example:

def process_url(url):
# TODO: Do some work with the url
return url

urls_to_process = ["http://url1.com", "http://url2.com", "http://site1.com", "http://site2.com"]

# process urls in parallel
result_items = do_threaded_work(urls_to_process, process_url)

# print(results)
print(repr(result_items))
"""
global wrapped_work_func
if not num_threads:
num_threads = len(work_items)

work_queue = Queue.Queue()
result_queue = Queue.Queue()

index = 0
for work_item in work_items:
if preserve_result_ordering:
work_queue.put((index, work_item))
else:
work_queue.put(work_item)
index += 1

if preserve_result_ordering:
wrapped_work_func = lambda work_item: (work_item[0], work_func(work_item[1]))

start_logging_with_thread_info()

#spawn a pool of threads, and pass them queue instance
for _ in range(num_threads):
if preserve_result_ordering:
t = ThreadedWorker(work_queue, result_queue, work_func=wrapped_work_func, queue_timeout=per_sync_timeout)
else:
t = ThreadedWorker(work_queue, result_queue, work_func=work_func, queue_timeout=per_sync_timeout)
t.setDaemon(True)
t.start()

work_queue.join()
stop_logging_with_thread_info()

logging.info('work_queue joined')

result_items = []
while not result_queue.empty():
result = result_queue.get(timeout=per_sync_timeout)
logging.info('found result[:500]: ' + repr(result)[:500])
if result:
result_items.append(result)

if preserve_result_ordering:
result_items = [work_item for index, work_item in result_items]

return result_items

class ThreadedWorker(threading.Thread):
""" Generic Threaded Worker
Input to work_func: item from work_queue

Example usage:

import Queue

urls_to_process = ["http://url1.com", "http://url2.com", "http://site1.com", "http://site2.com"]

work_queue = Queue.Queue()
result_queue = Queue.Queue()

def process_url(url):
# TODO: Do some work with the url
return url

def main():
# spawn a pool of threads, and pass them queue instance
for i in range(3):
t = ThreadedWorker(work_queue, result_queue, work_func=process_url)
t.setDaemon(True)
t.start()

# populate queue with data
for url in urls_to_process:
work_queue.put(url)

# wait on the queue until everything has been processed
work_queue.join()

# print results
print repr(result_queue)

main()
"""

def __init__(self, work_queue, result_queue, work_func, stop_when_work_queue_empty=True, queue_timeout=1):
threading.Thread.__init__(self)
self.work_queue = work_queue
self.result_queue = result_queue
self.work_func = work_func
self.stop_when_work_queue_empty = stop_when_work_queue_empty
self.queue_timeout = queue_timeout

def should_continue_running(self):
if self.stop_when_work_queue_empty:
return not self.work_queue.empty()
else:
return True

def run(self):
while self.should_continue_running():
try:
# grabs item from work_queue
work_item = self.work_queue.get(timeout=self.queue_timeout)

# works on item
work_result = self.work_func(work_item)

#place work_result into result_queue
self.result_queue.put(work_result, timeout=self.queue_timeout)

except Queue.Empty:
logging.warning('ThreadedWorker Queue was empty or Queue.get() timed out')

except Queue.Full:
logging.warning('ThreadedWorker Queue was full or Queue.put() timed out')

except:
logging.exception('Error in ThreadedWorker')

finally:
#signals to work_queue that item is done
self.work_queue.task_done()

def start_logging_with_thread_info():
try:
formatter = logging.Formatter('[thread %(thread)-3s] %(message)s')
logging.getLogger().handlers[0].setFormatter(formatter)
except:
logging.exception('Failed to start logging with thread info')

def stop_logging_with_thread_info():
try:
formatter = logging.Formatter('%(message)s')
logging.getLogger().handlers[0].setFormatter(formatter)
except:
logging.exception('Failed to stop logging with thread info')


使用示例

import Queue

urls_to_process = ["http://facebook.com", "http://pypix.com"]
work_queue = Queue.Queue()
result_queue = Queue.Queue()

def process_url(url):
# TODO: Do some work with the url
return url

def main():
# spawn a pool of threads, and pass them queue instance
for i in range(5):
t = ThreadedWorker(work_queue, result_queue, work_func=process_url)
t.setDaemon(False)
t.start()

# populate queue with data
for url in urls_to_process:
work_queue.put(url)

# wait on the queue until everything has been processed
work_queue.join()

# print results
print(repr(result_queue))

main()


下面是关于Python并发模型的介绍。

最近我注意到很多 Python 论坛上的问题在询问关于线程(Threads),微线程(Microthread)和绿色线程(Greenthread)这几个并发模型之间的具体差异是什么。问题诸如:

它们在实现上有何差异?

微线程/绿色线程 也有 常规线程 那样的数量上限吗?

它们每个与其它相比的优缺点是什么?我应该使用哪一个?

线程

离开并发模型这个集合,让我们先从最熟知的线程开始。在Python中,使用规范的Posix线程来实现线程。即是,在Python中每个线程映射到一个系统级的线程,并且系统内核察觉和负责维护这些线程。包括线程运行时的抢占,调度线程的下一个时隙,也包括处理上下文切换(和CPU另外一个寄存器切换线程状态等等。)

线程的特征就是,你运行得越多,内核调度器在同一时间应对的任务就越多。当你有太多的线程时,性能就会削弱,因为每个线程获得的执行时间片段,变得和线程间切换所需时间可以相比拟——切换开销成为了主要的瓶颈。使用线程,你需要保持运行的数量在一个合理的数量,100或者更少(运行一个线程池是一种用法例子)

因为每个Python线程都被内核所映射和管理,当一个上下文切换发生的时候,在用户空间(大多数用户程序花费它们时间的地方)到内核空间的来回切换中,存在额外的开销。同样这也是一个相对昂贵的操作,这也导致了同时运行太多线程的问题。

尽管线程被认为是轻量的,我们将会看到存在更加轻量的选择。

微线程Microthreads(小任务tasklets)

Stackless Python项目(重点修改了Python解释器的核心以形成兼容fork的Python)以小任务的名字(在介绍其他新特性中)介绍了微线程。

这个项目对线程的处理办法是对内核隐藏线程,并且由Python解释器自身处理所有的调度和上下文切换。从历史的观点来看,这对一个本身不支持线程的操作系统来说,通过对虚拟机或者解释器增加线程支持是非常有用的。(这种方法在Solaris 操作系统上的Java 1.1曾经用过,Solaris OS那时不支持线程)。甚至在操作系统本身支持线程的情况下,这种方法也有几个优点。即线程之间切换管理花费得到非常大的减少,不再执行用户空间到内核空间切换和反向切换。无栈Python通过解释器既处理了微线程调度也处理了其上下文切换。

尽管在性能方面有某些优点,无栈Python项目仍然是一个与主线代码无关的独立项目。这种情况出现有几个原因(你可以从这儿阅读到有关它的信息),原因之一就是其中的更改并不是很细小的,并且这种更改破坏了几个Python扩展。除非你的代码运行在你可以控制所用解释器的机器上,否则你就可能打算坚持使用Python的参考实现而避免使用微线程。不过,还有一种方法可以获得无栈Python的某些优点(相比于使用一般的Python解释器),继续向下阅读...

Greenlets(协程)

对于Python解释器,微线程需要较大的修改,Greenlets是微线程的一个分支,并且能够通过Python扩展来安装(Stackless太复杂而无法成为一个扩展)。Greenlets的思想实际上是从Stackless项目中提取出来的,并且保留了相似的优势,例如相对于内核,在解释器中管理线程。然而也存在一个主要的区别——Greenlets的一个实例没有明显的调度安排。

缺少一个调度程序意味着,你能够完全控制一个Greenlets的实例何时转向另一个。这就是众所周知的协作并发模型(即协程),即是为了在不同的Greenlets的实例之间进行转换,每个Greenlets的实例都必须自愿地放弃它的执行。而对于微线程,当转换(也即是创建)的时候只有非常低的开销,因为这个原因,你可以有大量的,和线程相关的Greenlets的实例。

greenlet 包是 Stackless 的副产品,其将微线程称为 “tasklet” 。tasklet运行在伪并发中,使用channel进行同步数据交换。

一个”greenlet”,是一个更加原始的微线程的概念,但是没有调度,或者叫做协程。这在你需要控制你的代码时很有用。你可以自己构造微线程的调度器;也可以使用”greenlet”实现高级的控制流。例如可以重新创建构造器;不同于Python的构造器,我们的构造器可以嵌套的调用函数,而被嵌套的函数也可以 yield 一个值。Greenlet是作为一个C扩展模块提供给未修改的Python解释器。

协作并发模型的一个优点就是它的确定性的本质。你非常确定地知道一个Greenlet实例在哪里退出,另外一个在哪里开始。这容许你在处理竞态条件的时候,避免在共享数据结构上使用锁。

如果你思考一下,你会注意到Greenlets是伪并发(即使在一个没有GIL的解释器中)——这就意味着不可能存在多个greenlet在运行,并且它仅仅是程序中表示流程的一种方式。使用大量的if/else模块结构和一些循环,可以模拟仿真Greenlet的行为,但是这种方式显然不是非常简洁的。

附加注释:如果你的greenlet在转向另一个greenlet之前,碰到了一个阻塞方法的调用,那么将会发生什么?你是程序进程将会被迫暂停,直到阻塞调用返回。有许多非常不错的库,可以帮助你逃开上述问题。如果你存在I/O阻塞(套接字和文件)调用,你可以考虑使用GEvent,它提供greenlets并且能够将I/O调用修改为无阻塞。更多内容请猛戳这里

比较

在最后一节,我提到了Greenlets是伪并发的,亦即在一个给定的时间只有一个是实际在运行的。所以这就是说与微线程和线程比较起来,Greenlets处于劣势对吗?好吧理论上说是的,但对Python不是。关于Python和并发的问题就是声名狼藉的GIL(全局解释器锁),这使得多于一个的线程/微线程不能同时运行。当你的代码运行于Python解释器时,你无法获得多处理器系统的优势。所以那使Greenlets同微线程和常规的线程处于同样的基础之上。现在问题更多的变成了,当运行具有许多线程的大量进程切换时,你是否需要高性能?当你的greenlets放弃运行时,你是否准备好了动手并精确控制?Greenlets是你的解决方案。

另一方面,如果你没有较高的性能要求,希望运行多个线程并让系统为你调度它们(不要忘记你需要锁!),可以考虑使用线程。尽管因为 greenlets 更轻量而具有诱惑力,但在许多情况下,它让你感觉不出有什么实际意义。比如我目前的工作就在使用普通的线程,因为线程和抢先式切换模型也能很好解决这个问题,而且我并不需要很高的性能。

当然,若你愿意安装一个解析器的修改版,Stackless 是可选之一,通过它,你可以象隐式调度一样方便获得 Greenlets 的益处。

还有一个我之前没提到的潜在解决方案。如果你有一个多核处理器,在Python中利用它的优势的唯一途径是使用进程。Python对进程提供的API几乎和线程API是一样的,相似的地方结束。每个进程都由自己的Python解释器启动,这意味着你避免了 GIL 麻烦。想让你的系统最大化的使用,你需要启动与你CPU内核相同数量的进程。请注意,因为你的代码在不同的进程运行,他们不能访问到对方的变量,所以一些与顺序有关的通信方法需要额外的设计(有其自身性能缺陷)。

对于Python的并发,没有一个一刀切的选择。应根据你的实际情况仔细推敲每一个的好处,选择最适合你的方式。

英文原文:

http://pypix.com/python/concurrent-programming-python/

http://www.devmusings.com/blog/2013/05/23/python-concurrency/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: