python进程与线程(多进程、多线程、ThreadLocal、进程VS线程、分布式进程)

学习目标:

python学习十三、


学习内容:

线程是最小的执行单元,而进程由至少一个线程组成
多任务的实现有3种方式:

  • 多进程模式
  • 多线程模式
  • 多进程+多线程模式

1、多进程
2、多线程
3、ThreadLocal
4、进程VS线程
5、分布式进程


1、多进程

1、multiprocessing
multiprocessing模块就是跨平台版本的多进程模块,multiprocessing模块提供了一个Process类来代表一个进程对象

  • 创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动
  • join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步
from multiprocessing import Process
import os

# 子进程要执行的代码
def run_proc(name):
    print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    p.start()
    p.join()
    print('Child process end.')
    输出:
    Parent process 7064.
Child process will start.
Run child process test (9080)...
Child process end.

2、pool
启动大量的子进程,可以用进程池的方式批量创建子进程

  • 调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process
  • Pool的默认大小是CPU的核数
from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')
    输出:
    Parent process 669.
Waiting for all subprocesses done...
Run task 0 (671)...
Run task 1 (672)...
Run task 2 (673)...
Run task 3 (674)...
Task 2 runs 0.14 seconds.
Run task 4 (673)...
Task 1 runs 0.27 seconds.
Task 3 runs 0.86 seconds.
Task 0 runs 1.41 seconds.
Task 4 runs 1.91 seconds.
All subprocesses done.

3、子进程
很多时候,子进程并不是自身,而是一个外部进程,我们创建了子进程后,还需要控制子进程的输入和输出

  • subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出
import subprocess

print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)
输出:
$ nslookup www.python.org
Server:		192.168.19.4
Address:	192.168.19.4#53

Non-authoritative answer:
www.python.org	canonical name = python.map.fastly.net.
Name:	python.map.fastly.net
Address: 199.27.79.223

Exit code: 0

子进程还需要输入,则可以通过communicate()方法输入

import subprocess

print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('utf-8'))
print('Exit code:', p.returncode)
输出:
$ nslookup
Server:		192.168.19.4
Address:	192.168.19.4#53

Non-authoritative answer:
python.org	mail exchanger = 50 mail.python.org.

Authoritative answers can be found from:
mail.python.org	internet address = 82.94.164.166
mail.python.org	has AAAA address 2001:888:2000:d::a6


Exit code: 0

4、进程间通信
Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据,完成进程间的通信

  • 在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据
from multiprocessing import Process, Queue
import os, time, 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(True)
        print('Get %s from queue.' % value)

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    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()
 输出:
 Process to write: 50563
Put A to queue...
Process to read: 50564
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.   
  • 注:Windows没有fork调用,因此,multiprocessing需要“模拟”出fork的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所以,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了

2、多线程

  • 线程是操作系统直接支持的执行单元,高级语言(包括python)通常都内置多线程的支持,Python的线程是真正的Posix Thread
  • Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块

1、启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行

主线程可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例

import time, threading

# 新线程执行的代码:
def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name)

print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)
输出:
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

2、lock
多进程与多线程的最大的区别是:在进程中变量不是共享的,而线程中是,这样就容易造成内容的混乱

  • 当一个线程需要修改时,就给它上一把锁这样就不会影响到其他线程了
  • 锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,就不会造成修改的冲突

lock也有缺点:

  • 包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降
  • 可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止

创建一个锁就是通过threading.Lock()来实现

用try…finally来确保锁一定会被释放

balance = 0
lock = threading.Lock()

def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了一定要释放锁:
            lock.release()

3、多核CPU
GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核


3、ThreadLocal

多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁
1、局部变量在函数调用的时候,传递起来很麻烦

def process_student(name):
    std = Student(name)
    # std是局部变量,但是每个函数都要用它,因此必须传进去:
    do_task_1(std)
    do_task_2(std)

def do_task_1(std):
    do_subtask_1(std)
    do_subtask_2(std)

def do_task_2(std):
    do_subtask_2(std)
    do_subtask_2(std)

2、ThreadLocal
根据上面的内容,用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象

  • 全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响
  • local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理
  • ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源
import threading
    
# 创建全局ThreadLocal对象:
local_school = threading.local()

def process_student():
    # 获取当前线程关联的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))

def process_thread(name):
    # 绑定ThreadLocal的student:
    local_school.student = name
    process_student()

t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
输出:
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)

4、进程VS线程

多任务的实现常用:多进程、多线程、多进程加多线程

  • 实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务

  • 多任务环境下,通常是一个Master,多个Worker

  • 多进程实现Master-Worker,主进程就是Master,其他进程就是Worker(稳定性较高一个子进程挂了不会导致其他子进程和主进程挂掉(主进程挂了全部进程都会挂))
  • 多线程实现Master-Worker,主线程就是Master,其他线程就是Worker(稳定性低,所有线程共享进程的内存,一个线程挂掉就导致所属的那个进程挂掉)

1、进程/线程切换

  • 进程/线程的切换,暂停进程/线程保存现场(CPU寄存器状态、内存页等),切换到其他进程/线程(恢复上次的寄存器状态,切换内存页等),当进程/线程特别多时,操作系统就会忙着切换任务大大减少执行任务的时间,最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态,所以所任务也会有个限度

2、计算密集型vsIO密集型
任务类型:计算密集型和IO密集型

  • 计算密集型:需要cpu资源大量的计算,所以需要运算速率高的代码像C语言(计算圆周率、高清视频解码),进程数目应该与CPU核心数相同
  • IO密集型:cpu资源会等待IO的输入,然而这样的任务99%的时间都在IO输入的过程,不需要高速率的编程语言python就比较合适(web应用)

3、异步IO
事件驱动模型:充分利用操作系统提供的异步IO支持,单进程单线程模型来执行多任务
Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序


5、分布式进程

  • Thread和Process中,应当优选Process,Process更稳定而且Process可以分布到多台机器上,Thread最多只能分布到同一台机器的多个CPU上
  • Python的multiprocessing模块支持多进程,managers子模块支持把多进程分布到多台机器上,有一个服务进程,依靠网络通信,将任务发布出去
    1、保持原有Queue正常运行,通过managers模块把Queue通过网络暴露出去,让其他机器的进程访问Queue
  • 服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务
  • 在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但在分布式多进程环境添加任务到Queue不可以直接对原始的task_queue进行操作,manager.get_task_queue()获得的Queue接口添加
# task_master.py

import random, time, queue
from multiprocessing.managers import BaseManager

# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()

# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
    pass

# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('', 5000), authkey=b'abc')
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
    n = random.randint(0, 10000)
    print('Put task %d...' % n)
    task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
    r = result.get(timeout=10)
    print('Result: %s' % r)
# 关闭:
manager.shutdown()
print('master exit.')
  • 另一台机器上启动任务进程(本机上启动也可以)
  • 任务进程要通过网络连接到服务进程,所以要指定服务进程的IP
# task_worker.py

import time, sys, queue
from multiprocessing.managers import BaseManager

# 创建类似的QueueManager:
class QueueManager(BaseManager):
    pass

# 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

# 连接到服务器,也就是运行task_master.py的机器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 端口和验证码注意保持与task_master.py设置的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 从网络连接:
m.connect()
# 获取Queue的对象:
task = m.get_task_queue()
result = m.get_result_queue()
# 从task队列取任务,并把结果写入result队列:
for i in range(10):
    try:
        n = task.get(timeout=1)
        print('run task %d * %d...' % (n, n))
        r = '%d * %d = %d' % (n, n, n*n)
        time.sleep(1)
        result.put(r)
    except Queue.Empty:
        print('task queue is empty.')
# 处理结束:
print('worker exit.')

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值