Python 多线程与多进程

Python 多线程 (multithred) 与多进程 (multiprocess)

OverView

多线程 多进程
概述 最小的执行单元 最小的资源管理单元 ,一个进程包含一至多个线程
内存 共享内存空间
因此当两个线程同时写的时候要特别注意(GIL 干的就是这个事情)
分离的内存空间
可靠性 一个线程挂掉会导致整个进程挂掉 进程之间不会相互影响
优点 1. 轻量,运行使用内存少且能共享
2. 可以更方便的制作 responsive 的 UI
3. C extension modules 可以解决 GIL 的问题
1. 分离的内存空间
2. 更好地利用多核 CPU
3. 子进程是 killable 的
缺点 1. 不够稳定,容易丢失数据
2. 受GIL限制,每一时刻其实只有一个线程可以获得 GIL 进行运行
3. Not killable
1. 进程间通信更复杂
2. 运行时使用的内存更大
使用场景 I/O 密集型
e.g. 当需要根据用户输入从一个数据库做出回应时,使用多线程可以使程序更 responsive,且由于共享内存多线程可以 access 到相同的数据结构
CPU 密集型
e.g. 如果你的程序需要连接六个数据库并且对每个数据库的数据做出一些复杂的矩阵运算的时候,多进程可以更好地利用 CPU
通信 最常见是使用 队列(Queue),也可以通过实现自己的数据结构并添加所需要的同步机制来实现 multiprocessing
module threding multiprocessing

通信

线程间通信

1. 使用队列

# A thread that produces data
def producer(out_q):
    i = 0
    while i<50:
        # Produce some data
        i += 1
        out_q.put(i)
    out_q.put(_sentinel)

# A thread that consumes data
def consumer(in_q):
    print('getting data')
    while True:
        # Get some data
        data = in_q.get()
        print('get', data)
        # in_q.task_done()
        
        if data is _sentinel:
            in_q.put(_sentinel)
            break
            

# Create the shared queue and launch both threads
q = Queue()
t1 = Thread(target=consumer, args=(q,))
t2 = Thread(target=producer, args=(q,))
t1.start()
t2.start()
print('q', q)
q.join()

2. 自己实现

import threading

class SharedCounter:
    '''
    A counter object that can be shared by multiple threads.
    '''
    def __init__(self, initial_value = 0):
        self._value = initial_value
        self._value_lock = threading.Lock()

    def incr(self,delta=1):
        '''
        Increment the counter with locking
        '''
        with self._value_lock:
             self._value += delta

    def decr(self,delta=1):
        '''
        Decrement the counter with locking
        '''
        with self._value_lock:
             self._value -= delta
  • Lock 对象和 with 语句块一起使用可以保证互斥执行,就是每次只有一个线程可以执行 with 语句包含的代码块。with 语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁
  • 解决死锁问题的一种方案是为程序中的每一个锁分配一个唯一的id,然后只允许按照升序规则来使用多个锁,这个规则使用上下文管理器 是非常容易实现的,示例如下:
import threading
from contextlib import contextmanager
# Thread-local state to stored information on locks already acquired
_local = threading.local()
@contextmanager
def acquire(*locks):
    # Sort locks by object identifier
    locks = sorted(locks, key=lambda x: id(x))
    # Make sure lock order of previously acquired locks is not violated
    acquired = getattr(_local,'acquired',[])
    if acquired and max(id(lock) for lock in acquired) >= id(locks[0]):
        raise RuntimeError('Lock Order Violation')

    # Acquire all of the locks
    acquired.extend(locks)
    _local.acquired = acquired

    try:
        for lock in locks:
            lock.acquire()
        yield
    finally:
        # Release locks in reverse order of acquisition
        for lock in reversed(locks):
            lock.release()
        del acquired[-len(locks):]
  • 通过排序,使得不管用户以什么样的顺序来请求锁,这些锁都会按照固定的顺序被获取。
  • 为了避免死锁,所有的加锁操作必须使用 acquire() 函数。如果代码中的某部分绕过acquire 函数直接申请锁,那么整个死锁避免机制就不起作用了。

进程间通信

1. 使用 Queue 或者 Pipe 通信

Queue:

# -*- coding:utf-8 -*-
from multiprocessing import Process, Queue
import os
import time
import random


# 写数据进程执行的代码:
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())


# 读数据进程执行的代码:
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get()
        print('Get %s from queue.' % value)


if __name__ == '__main__':
    # 父进程创建Queue,并传给各个子进程:
    print(os.getpid())
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()

Pipes:
Python 多线程与多进程_第1张图片

2. 共享内存

使用 multiprocess.Value, multiprocess.Array 来开辟共享内存
【未完】

tips:

  • fork 方法仅能在linux系统上运行,跨平台就用 multiprocess
  • 有了 fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求
  • fork() 调用一次,返回两次, 操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回
  • join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步

参考连接

  1. 【stackoverflow】 python
    中的多线程与多进程
  2. 【python cookbook】线程间通信
  3. 【cnblogs】进程间通信

你可能感兴趣的:(基础知识)