python之线程和进程


在这里插入图片描述

__all__ = ['get_ident', 'active_count', 'Condition', 'current_thread',
           'enumerate', 'main_thread', 'TIMEOUT_MAX',
           'Event', 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread',
           'Barrier', 'BrokenBarrierError', 'Timer', 'ThreadError',
           'setprofile', 'settrace', 'local', 'stack_size',
           'excepthook', 'ExceptHookArgs', 'gettrace', 'getprofile']

线程对象

Thread 类 代表一个在独立控制线程中运行的活动。 指定活动有两种方式:向构造器传递一个可调用对象,或在子类中重载 run() 方法。 其他方法不应在子类中重载(除了构造器)。 换句话说,只能重载这个类的 init() 和 run() 方法。

当线程对象一旦被创建,其活动必须通过调用线程的 start() 方法开始。 这会在独立的控制线程中发起调用 run() 方法。

一旦线程活动开始,该线程会被认为是 ‘存活的’ 。当它的run()方法终结了(不管是正常的还是抛出未被处理的异常),就不是’存活的’。 is_alive() 方法用于检查线程是否存活。

其他线程可以调用一个线程的join()方法。这会阻塞调用该方法的线程,直到被调用join()方法的线程终结。

线程有名字。名字可以传递给构造函数,也可以通过 name 属性读取或者修改。

如果 run() 方法引发了异常,则会调用threading.excepthook()来处理它。 在默认情况下,threading.excepthook() 会静默地忽略 SystemExit

一个线程可以被标记成一个守护线程。 这个标识的意义是,当剩下的线程都是守护线程时,整个 Python 程序将会退出。 初始值继承于创建线程。 这个标识可以通过 daemon 特征属性或者 daemon 构造器参数来设置。

守护线程在程序关闭时会突然关闭。他们的资源(例如已经打开的文档,数据库事务等等)可能没有被正确释放。如果你想你的线程正常停止,设置他们成为非守护模式并且使用合适的信号机制,例如: Event。
有个 “主线程” 对象;这对应Python程序里面初始的控制线程。它不是一个守护线程。

锁对象

原始锁

Lock原始锁是一个在锁定时不属于特定线程的同步基元组件。在Python中,它是能用的最低级的同步基元组件。

原始锁处于 “锁定” 或者 “非锁定” 两种状态之一。它被创建时为非锁定状态。它有两个基本方法, acquire()release() 。当状态为非锁定时, acquire() 将状态改为锁定并立即返回。当状态是锁定时, acquire() 将阻塞至其他线程调用 release() 将其改为非锁定状态,然后 acquire() 调用重置其为锁定状态并返回。release()只在锁定状态下调用; 它将状态改为非锁定并立即返回。如果尝试释放一个非锁定的锁,则会引发RuntimeError异常。

锁支持上下文管理协议

当多个线程在 acquire() 等待状态转变为未锁定被阻塞,然后 release() 重置状态为未锁定时,只有一个线程能继续执行;至于哪个等待线程继续执行没有定义,并且会根据实现而不同。

所有方法的执行都是原子性的。

递归锁

RLock递归锁是一个可以被同一个线程多次获取的同步基元组件。在内部,它在基元锁的锁定/非锁定状态上附加了 “所属线程” 和 “递归等级” 的概念。在锁定状态下,某些线程拥有锁 ; 在非锁定状态下, 没有线程拥有它。

若要锁定锁,线程调用其 acquire() 方法;一旦线程拥有了锁,方法将返回。若要解锁,线程调用 release() 方法。 acquire()/release() 对可以嵌套;只有最终 release() (最外面一对的 release() ) 将锁解开,才能让其他线程继续处理 acquire() 阻塞。

递归锁也支持 上下文管理协议。

条件对象

Condition条件变量总是与某种类型的锁对象相关联,锁对象可以通过传入获得,或者在缺省的情况下自动创建。当多个条件变量需要共享同一个锁时,传入一个锁很有用。锁是条件对象的一部分,你不必单独地跟踪它。

条件变量遵循上下文管理协议 :使用 with 语句会在它包围的代码块内获取关联的锁。

其它方法必须在持有关联的锁的情况下调用。 wait() 方法释放锁,然后阻塞直到其它线程调用 notify() 方法或 notify_all() 方法唤醒它。一旦被唤醒, wait() 方法重新获取锁并返回。它也可以指定超时时间。

notify()方法唤醒等待条件变量的线程之一(如果有线程正在等待)。notify_all()方法唤醒所有等待条件变量的线程。选择 notify() 还是 notify_all() ,取决于一次状态改变是只能被一个还是能被多个等待线程所用。

注意: notify() 方法和 notify_all() 方法并不会释放锁,这意味着被唤醒的线程不会立即从它们的 wait() 方法调用中返回,而是会在调用了 notify() 方法或 notify_all() 方法的线程最终放弃了锁的所有权后返回。

使用条件变量的典型编程风格是将锁用于同步某些共享状态的权限,那些对状态的某些特定改变感兴趣的线程,它们重复调用 wait() 方法,直到看到所期望的改变发生;而对于修改状态的线程,它们将当前状态改变为可能是等待者所期待的新状态后,调用 notify() 方法或者 notify_all() 方法。例如,下面的代码是一个通用的无限缓冲区容量的生产者-消费者情形:

# Consume one item
with cv:
    while not an_item_is_available():
        cv.wait()
    get_an_available_item()

# Produce one item
with cv:
    make_an_item_available()
    cv.notify()

使用 while 循环检查所要求的条件成立与否是有必要的,因为 wait() 方法可能要经过不确定长度的时间后才会返回,而此时导致 notify() 方法调用的那个条件可能已经不再成立。这是多线程编程所固有的问题。

# Consume an item
with cv:
    cv.wait_for(an_item_is_available,timeout=1 )
    get_an_available_item()

wait_for(predicate, timeout=None)方法可自动化条件检查,这个方法会重复地调用 wait() 直到满足判断式或者发生超时。返回值是判断式最后一个返回值,而且如果方法发生超时会返回 False 。

while not predicate():
    cv.wait()

信号量对象

Semaphore 一个信号量管理一个内部计数器,该计数器因 acquire() 方法的调用而递减,因release()方法的调用而递增。 计数器的值永远不会小于零;当 acquire() 方法发现计数器为零时,将会阻塞,直到其它线程调用 release() 方法。

信号量对象也支持 上下文管理协议

信号量通常用于保护数量有限的资源,例如数据库服务器。在资源数量固定的任何情况下,都应该使用有界信号量。在生成任何工作线程前,应该在主线程中初始化信号量。

maxconnections = 5
# ...
pool_sema = BoundedSemaphore(value=maxconnections)

工作线程生成后,当需要连接服务器时,这些线程将调用信号量的 acquire 和 release 方法:

with pool_sema:
    conn = connectdb()
    try:
        # ... use connection ...
    finally:
        conn.close()

使用有界信号量能减少这种编程错误:信号量的释放次数多于其请求次数。

事件对象

Event这是线程之间通信的最简单机制之一:一个线程发出事件信号,而其他线程等待该信号。

一个事件对象管理一个内部标识,调用 set() 方法可将其设置为 true ,调用 clear() 方法可将其设置为 false ,调用 wait() 方法将进入阻塞直到标识为 true 。这个标识初始时为 false 。

定时器对象

Timer此类表示一个操作应该在等待一定的时间之后运行 — 相当于一个定时器。 Timer 类是 Thread 类的子类,因此可以像一个自定义线程一样工作。

与线程一样,定时器也是通过调用其 Timer.start 方法来启动的。 定时器可以通过调用 cancel() 方法来停止(在其动作开始之前)。 定时器在执行其行动之前要等待的时间间隔可能与用户指定的时间间隔不完全相同。

def hello():
    print("hello, world")

t = Timer(30.0, hello,args=None, kwargs=None)
t.start()  # after 30 seconds, "hello, world" will be printed

栅栏对象

Barrier栅栏类提供一个简单的同步原语,用于应对固定数量的线程需要彼此相互等待的情况。线程调用wait() 方法后将阻塞,直到所有线程都调用了wait()方法。此时所有线程将被同时释放。

栅栏对象可以被多次使用,但进程的数量不能改变。

Barrier(parties, action=None, timeout=None)
创建一个需要 parties 个线程的栅栏对象。如果提供了可调用的action 参数,它会在所有线程被释放时在其中一个线程中自动调用。timeout是默认的超时时间,如果没有在wait() 方法中指定超时时间的话。

multiprocessing

multiprocessing,你可以轻松完成从单进程到并发执行的转换。multiprocessing支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。这些组件的导入都是来源于DefaultContext 上下文对象。

#
# multiprocessing/__init__.py
#
import sys
from . import context

__all__ = [x for x in dir(context._default_context) if not x.startswith('_')]
globals().update((name, getattr(context._default_context, name)) for name in __all__)

创建管理进程模块:

Process(用于创建进程)
Pool(用于创建管理进程池)
Queue(用于进程通信,资源共享)
Value,Array(用于进程通信,资源共享)
Pipe(用于管道通信)
Manager(用于资源共享)

同步子进程模块:

Condition(条件变量)
Event(事件)
Lock(互斥锁)
RLock(可重入的互斥锁(同一个进程可以多次获得它,同时不会造成阻塞)
Semaphore(信号量)
BoundedSemaphore(有界的信号量)

# multiprocessing/synchronize.py
__all__ = [
    'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Condition', 'Event'
    ]

Process 类

在 multiprocessing 中,通过创建一个 Process 对象然后调用它的 start() 方法来生成进程。 Process 和 threading.Thread API 相同。

上下文和启动方法

根据不同的平台, multiprocessing 支持三种启动进程的方法。这些启动方法有

  • spawn
    父进程会启动一个新的 Python 解释器进程。 子进程将只继承那些运行进程对象的 run() 方法所必须的资源。 特别地,来自父进程的非必需文件描述符和句柄将不会被继承。 使用此方法启动进程相比使用 fork 或 forkserver 要慢上许多。
  • fork
    父进程使用 os.fork() 来产生 Python 解释器分叉。子进程在开始时实际上与父进程相同。父进程的所有资源都由子进程继承。请注意,安全分叉多线程进程是棘手的。
  • forkserver
    当程序启动并选择 forkserver 启动方法时,将产生一个服务器进程。 从那时起,每当需要一个新进程时,父进程就会连接到该服务器并请求它分叉一个新进程。 分叉服务器进程是单线程的,除非因系统库或预加载导入的附带影响改变了这一点,因此使用 os.fork() 通常是安全的。 没有不必要的资源被继承。

在 POSIX 上使用 spawn 或 forkserver 启动方法将同时启动一个资源追踪器进程,负责追踪当前程序的进程产生的已取消连接的命名系统资源(如命名信号量或 SharedMemory 对象)。 当所有进程退出后资源追踪器会负责取消链接任何仍然被追踪的对象。 在通常情况下应该是没有此种对象的,但是如果一个子进程是被某个信号杀掉的则可能存在一些“泄露”的资源。 (泄露的信号量或共享的内存段不会被自动取消链接直到下一次重启。 对于这两种对象来说会有问题因为系统只允许有限数量的命名信号量,而共享的内存段在主内存中占用一些空间。)

要选择一个启动方法,你应该在主模块的 if __name__ == '__main__' 子句中调用 set_start_method() ,在程序中 set_start_method() 不应该被多次调用。例如:

import multiprocessing as mp

def foo(q):
    q.put('hello')

if __name__ == '__main__':
    mp.set_start_method('spawn')
    q = mp.Queue()
    p = mp.Process(target=foo, args=(q,))
    p.start()
    print(q.get())
    p.join()

或者,你可以使用 get_context() 来获取上下文对象。上下文对象与 multiprocessing 模块具有相同的API,并允许在同一程序中使用多种启动方法。:

import multiprocessing as mp

def foo(q):
    q.put('hello')

if __name__ == '__main__':
    ctx = mp.get_context('spawn')
    q = ctx.Queue()
    p = ctx.Process(target=foo, args=(q,))
    p.start()
    print(q.get())
    p.join()

请注意,对象在不同上下文创建的进程间可能并不兼容。 特别是,使用 fork 上下文创建的锁不能传递给使用 spawn 或 forkserver 启动方法启动的进程。

想要使用特定启动方法的库应该使用 get_context() 以避免干扰库用户的选择。

from multiprocessing import Queue

在进程之间交换对象

在使用多进程的过程中,最好不要使用共享资源。普通的全局变量是不能被子进程所共享的,只有通过Multiprocessing组件构造的数据结构可以被共享。multiprocessing 支持进程之间的两种通信通道:管道和队列。

使用多进程时,一般使用消息机制实现进程间通信,尽可能避免使用同步原语,例如锁。

消息机制包含: 管道Pipe可以用于在两个进程间传递消息,以及队列Queue 能够在多个生产者和消费者之间通信。

队列

Queue 类是一个近似 queue.Queue 的克隆. Queue是用来创建进程间资源共享的队列的类,使用Queue可以达到多进程间数据传递的功能(缺点:只适用Process类,不能在Pool进程池中使用)。

另外还可以通过使用一个管理器对象创建一个共享队列,详见 管理器 。
JoinableQueue就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。

JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:

task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止.

Queue, SimpleQueue 以及 JoinableQueue 都是多生产者,多消费者,并且实现了 FIFO 的队列类型,其表现与标准库中的 queue.Queue 类相似。 不同之处在于 Queue 缺少标准库的 queue.Queue 从 Python 2.5 开始引入的 task_done() 和 join() 方法。

如果你使用了 JoinableQueue ,那么你必须对每个已经移出队列的任务调用 JoinableQueue.task_done()。 不然的话用于统计未完成任务的信号量最终会溢出并抛出异常。

from multiprocessing import Process, JoinableQueue
import time, random
def consumer(q):
    while True:
        res = q.get()
        print('消费者拿到了 %s' % res)
        q.task_done()
        
def producer(seq, q):
    for item in seq:
        time.sleep(random.randrange(1,2))
        q.put(item)
        print('生产者做好了 %s' % item)
    q.join()
    
if __name__ == "__main__":
    q = JoinableQueue()
    seq = ('产品%s' % i for i in range(5))
    p = Process(target=consumer, args=(q,))
    p.daemon = True  # 设置为守护进程,在主线程停止时p也停止,但是不用担心,producer内调用q.join保证了consumer已经处理完队列中的所有元素
    p.start()
    producer(seq, q)
    print('主线程')

队列是线程和进程安全的。

管道

Pipe() 函数返回一个由管道连接的连接对象,默认情况下是双工(双向)。例如:

from multiprocessing import Process, Pipe

def f(conn):
    conn.send([42, None, 'hello'])
    conn.close()

if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())   # prints "[42, None, 'hello']"
    p.join()

返回的两个连接对象 Pipe() 表示管道的两端。每个连接对象都有 send() recv()方法(相互之间的)。请注意,如果两个进程(或线程)同时尝试读取或写入管道的同一端,则管道中的数据可能会损坏。当然,在不同进程中同时使用管道的不同端的情况下不存在损坏的风险。``

Value,Array

用于进程通信,资源共享。multiprocessing 中Value和Array的实现原理都是在共享内存中创建ctypes()对象来达到共享数据的目的,两者实现方法大同小异,只是选用不同的ctypes数据类型而已。

注意:Value和Array只适用于Process类。

from multiprocessing import Process, Value, Array

def f(n, a):
    n.value = 3.1415927
    for i in range(len(a)):
        a[i] = -a[i]

if __name__ == '__main__':
    num = Value('d', 0.0)
    arr = Array('i', range(10))

    p = Process(target=f, args=(num, arr))
    p.start()
    p.join()

    print(num.value)
    print(arr[:])

管理器

管理器提供了一种创建共享数据的方法,从而可以在不同进程中共享,甚至可以通过网络跨机器共享数据。管理器维护一个用于管理共享对象的服务。管理器对象对应了一个已经启动的子进程,并且拥有一系列方法可以用于创建共享对象、返回对应的代理。其他进程可以通过代理访问这些共享对象。

自定义管理器例子:

from multiprocessing import freeze_support
from multiprocessing.managers import BaseManager, BaseProxy
import operator

class Foo:
    def f(self):
        print('you called Foo.f()')
    def g(self):
        print('you called Foo.g()')
    def _h(self):
        print('you called Foo._h()')

# A simple generator function
def baz():
    for i in range(10):
        yield i*i

# Proxy type for generator objects
class GeneratorProxy(BaseProxy):
    _exposed_ = ['__next__']
    def __iter__(self):
        return self
    def __next__(self):
        return self._callmethod('__next__')

# Function to return the operator module
def get_operator_module():
    return operator


class MyManager(BaseManager):
    pass

# register the Foo class; make `f()` and `g()` accessible via proxy
MyManager.register('Foo1', Foo)

# register the Foo class; make `g()` and `_h()` accessible via proxy
MyManager.register('Foo2', Foo, exposed=('g', '_h'))

# register the generator function baz; use `GeneratorProxy` to make proxies
MyManager.register('baz', baz, proxytype=GeneratorProxy)

# register get_operator_module(); make public functions accessible via proxy
MyManager.register('operator', get_operator_module)

##

def test():
    manager = MyManager()
    manager.start()

    print('-' * 20)

    f1 = manager.Foo1()
    f1.f()
    f1.g()
    assert not hasattr(f1, '_h')
    assert sorted(f1._exposed_) == sorted(['f', 'g'])
    print('-' * 20)

    f2 = manager.Foo2()
    f2.g()
    f2._h()
    assert not hasattr(f2, 'f')
    assert sorted(f2._exposed_) == sorted(['g', '_h'])
    print('-' * 20)

    it = manager.baz()
    for i in it:
        print('<%d>' % i, end=' ')
    print()
    print('-' * 20)

    op = manager.operator()
    print('op.add(23, 45) =', op.add(23, 45))
    print('op.pow(2, 94) =', op.pow(2, 94))
    print('op._exposed_ =', op._exposed_)

if __name__ == '__main__':
    freeze_support()
    test()

concurrent.futures

Python标准库为我们提供了threading和multiprocessing模块编写相应的多线程/多进程代码。标准库还为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutorProcessPoolExecutor两个类,实现了对threading和multiprocessing的更高级的抽象,对编写线程池/进程池提供了直接的支持。 concurrent.futures基础模块是executorfuture

Executor

Executor是一个抽象类,它不能被直接使用。它为具体的异步执行定义了一些基本的方法。 ThreadPoolExecutorProcessPoolExecutor继承了Executor,分别被用来创建线程池和进程池的代码,两者的使用方法时相同的。

submit()方法
Executor中定义了submit()方法,这个方法的作用是提交一个可执行的回调task,并返回一个future实例。future对象代表的就是给定的调用。

from concurrent import futures
import time

def test(num):
    time.sleep(1)
    return num

if __name__ == '__main__':

    start = time.time()
    with futures.ThreadPoolExecutor(max_workers=1) as executor:
        future = executor.submit(test, 1)
        print(future.result()) # test函数的return值
    
    end = time.time()
    print("time:", end - start)

map()方法
除了submit,Exectuor还为我们提供了map方法,这个方法返回一个map(func, *iterables)迭代器,迭代器中的回调执行返回的结果有序的。其本质还是调用submit()函数。

from concurrent import futures
import time

def test(num):
    time.sleep(1)
    return num

if __name__ == '__main__':

    data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    start = time.time()
    with futures.ThreadPoolExecutor(max_workers=10) as executor:
        for future in executor.map(test, data):
            print(future) # test函数的return值
    
    end = time.time()
    print("time:", end - start)

Future

Future可以理解为一个在未来完成的操作,这是异步编程的基础。通常情况下,我们执行io操作,访问url时(如下)在等待结果返回之前会产生阻塞,cpu不能做其他事情,而Future的引入帮助我们在等待的这段时间可以完成其他的操作。

Future类封装了可调用的异步执行。Future 实例通过 Executor.submit()方法创建。

from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def test(num):
    time.sleep(1)
    return "Return of {}".format(num)

if __name__ == '__main__':

    start = time.time()

    pool = ThreadPoolExecutor(max_workers=10)
    futures = []
    for i,x in enumerate(range(5)):
        futures.append(pool.submit(test, x))
        # print(futures[i].result(timeout=None)) 此处会阻塞,直到futures[i]执行完毕,起不到并行作用
    print(1)
    for x in as_completed(futures): # 此处不会阻塞,因为as_completed(futures)会返回一个迭代器,每次迭代都会返回一个已经完成的futures
        print(x.result())
    print(2)
    
    end = time.time()
    print("time:", end - start)

参考链接:

  1. https://pythonhosted.org/futures/
  2. https://www.geeksforgeeks.org/difference-between-process-and-thread/
  3. https://stackoverflow.com/questions/1897993/what-is-the-difference-between-concurrent-programming-and-parallel-programming
  4. https://docs.python.org/3/library/multiprocessing.html#managers
  • 25
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值