【Python】编程笔记9

进程和线程

对于操作系统来说,一个任务就是一个进程(Process)

在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,将进程内的这些“子任务”称为线程(Thread)

==》如何同时执行多个任务?

  • 方法1:多进程模式。 启动多个进程,每个进程虽然只有一个线程,但是多个进程可以一块执行多个任务。
  • 方法2:多线程模式。 启动一个进程,在一个进程内启动多个线程,则多个线程可以一块执行多个任务。
  • 方法3:多进程+多线程模式。启动多个进程,每个进程再启动多个线程==》模型复杂,很少采用。
  • 注意:还需考虑相互通信和协调、同步、数据共享的问题。

一、基础知识

Unix/Linux系统中的 fork() 系统调用,fork() 调用一次,返回两次。(操作系统自动将当前进程(父进程)复制一份(子进程),然后分别在父进程和子进程内返回。)

**子进程永远返回 0,而父进程返回子进程的 ID。**子进程调用 getppid() 就可以获得父进程的 ID。

import os

print('Process (%s) start...' % os.getpid())
## 仅在Unix/Linux系统下
pid = os.fork()
if pid == 0:
    print("I am child process (%s) and my parent is %s." % (os.getpid(), os.getppid()))
else:
    print("I (%s) just created a child process (%s)." % (os.getpid(), pid))

输出结果

Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.
  • os.getpid()——获取当前进程的 ID
  • os.getppid()——获得当前进程的父进程的 ID

==》fork 调用可以在一个进程在接到新任务时就可以复制出一个子进程来处理新任务。

二、多进程(multiprocessing)

模块:multiprocessing——跨平台

1、初体验

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
    ## 参数:执行函数、执行函数的参数
    p = Process(target=run_proc, args=('test',))
    print('Child process will start.')
    # 启动子进程 p
    p.start()
    p.join()   # 常用于进程间的同步
    print('Child process end.')

输出结果

Parent process 10808.
Child process will start.
Run child process test (13692)...
Child process end.

2、Pool(进程池)

用进程池(Pool)的方式批量创建子进程。

Pool 的默认大小是 CPU 的核数

(1)非阻塞
import multiprocessing, os
import time

def func(msg):
    print("msg:", msg, "(%s)" % os.getpid())
    start = time.time()
    time.sleep(3)
    end = time.time()
    print("Task %s end %f" % (msg, (end - start)))

if __name__ == '__main__':
    print('Parent process %s.' % os.getpid())
    pool = multiprocessing.Pool(processes = 3)
    for i in range(3):
        msg = "hello %d" % (i)
        pool.apply_async(func, (msg,))
    pool.close()
    pool.join()
    print("Sub-process(es) done.")

输出结果

Parent process 1000.
msg: hello 0 (4416)
msg: hello 1 (14240)
msg: hello 2 (828)
Task hello 0 end 3.000664
Task hello 1 end 3.000952
Task hello 2 end 3.000939
Sub-process(es) done.
(2)阻塞
import multiprocessing, time, os

def func(msg):
    print("msg:", msg, "(%s)" % os.getpid())
    start = time.time()
    time.sleep(3)
    end = time.time()
    print("Task %s end %f" % (msg, (end - start)))
if __name__ == '__main__':
    print('Parent process %s.' % os.getpid())
    pool = multiprocessing.Pool(processes = 3)
    for i in range(3):
        msg = "hello %d" % (i)
        pool.apply(func, (msg,))
    pool.close()
    pool.join()
    print("Sub-process(es) done.")

输出结果

Parent process 244.
msg: hello 0 (10280)
Task hello 0 end 3.000280
msg: hello 1 (15200)
Task hello 1 end 3.000413
msg: hello 2 (10140)
Task hello 2 end 3.000886
Sub-process(es) done.
(4)代码解读

对 Pool 对象调用 join() 方法会等待所有子进程执行完毕,调用 join() 之前必须先调用 close(),调用 close()之后就不能继续添加新的 Process 了。

(3)分析

区别主要是 apply_async和 apply函数,前者是非阻塞的,后者是阻塞。非阻塞多个子进程可以同时进行,而阻塞子进程依次进行。

3、子进程

子进程是一个外部进程,需要控制其输入和输出。主要功能是执行外部的命令和程序。
==》模块:subprocess

subprocess包中定义有数个创建子进程的函数,这些函数分别以不同的方式创建子进程,所以我们可以根据需要来从中选取一个使用。另外subprocess还提供了一些管理标准流(standard stream)和管道(pipe)的工具,从而在进程间使用文本通信。

使用subprocess包中的函数创建子进程的时候,要注意:

  • 在创建子进程之后,父进程是否暂停,并等待子进程运行。
  • 函数返回什么
  • 当returncode不为0时,父进程如何处理。
(1)开启子进程
  • subprocess.call()
  • subprocess.check_call()
  • subprocess.check_output()

参数:命令字符串,eg:([‘ping’,‘www.baidu.com’,’-c’,‘3’]) 或 (“ping www.baidu.com -c 3”) 两种形式。在Windows环境下,最好添加 shell=True 参数,使得可以顺利地执行dos命令。

区别:返回值。子进程的执行返回码;若返回码是0则返回0,否则出错的话raise起CalledProcessError,可以用except处理之;若返回码是0则返回子进程向stdout输出的结果,否则也raise起CalledProcessError。

三种方法均会让父进程挂起等待,在子进程结束之前,父进程不会继续执行下去。

本质:对 subprocess.Popen 方法的封装,Popen 开启的子进程不会让父进程等待其完成的,除非调用 wait() 方法。

import subprocess
print('$ nslookup www.python.org')
r = subprocess.call(['nslookup','www.python.org'])
print('Exit coe:' ,r)

结果输出

$ nslookup www.python.org
��Ȩ��Ӧ��:
������:  UnKnown
Address:  192.168.43.1

����:    dualstack.python.map.fastly.net
Addresses:  2a04:4e42:6::223
	  151.101.24.223
Aliases:  www.python.org

Exit coe: 0
(2)添加输入——communicate()方法
## 有输入的子进程
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('gbk'))
# returncode 子进程的退出状态
print('Exit code:', p.returncode)

结果输出

$ nslookup
默认服务器:  UnKnown
Address:  192.168.43.1

> > 服务器:  UnKnown
Address:  192.168.43.1

python.org	MX preference = 50, mail exchanger = mail.python.org
> 
Exit code: 0

stdin, stdout 和 stderr:指定了执行程序的标准输入,标准输出和标准错误的文件句柄。它们的值可以是PIPE, 一个存在的文件描述符(正整数),一个存在的文件对象,或 None。

4、进程间通信

多种机制:Queue、Pipes等方式交换数据。

示例:Queue为例,在父进程中创建两个子进程,一个往 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: 3564
Put A to queue...
Process to read: 2500
Get A from queue.
Put B to queue...
Get B from queue.
Put C to queue...
Get C from queue.

三、多线程

一个进程内包含若干线程(至少有一个线程)。

线程是操作系统直接支持的执行单元 ==》高级语言内置多线程支持。Python中为真正的 Posix Thread

模块:_thread 和 threading

  • _thread:低级模块
  • threading:对_thread的封装,最常用。

1、启动

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

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.

分析

  • 任何进程默认就会启动一个线程(主线程,name:MainThread),主线程又可以启动新的线程。
  • threading.current_thread()函数,返回当前线程的实例。
  • 子线程的名字在创建时指定,eg:LoopThread。若不指定,则自动给线程命名为 Thread-1、Thread-2…

2、Lock

进程与线程最大的区别

  • 多进程中,同一变量,各自有一份拷贝存于每个进程,互不影响;
  • 多线程中,所有变量都由所有线程共享。==》任何一个变量都可以被任何一个线程修改。

原因:高级语言的一条语句在CPU执行时是若干条语句,而执行这几条语句中时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。
==》解决方法:threading.Lock() 函数。

import time, threading

# 假定这是你的银行存款
balance = 0
lock = threading.Lock()

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

def change_it(n):
    # 先存后取,结果应该为 0
    global balance
    balance = balance + n
    balance = balance - n

t1 = threading.Thread(target=run_thread, args=(5, ))
t2 = threading.Thread(target=run_thread, args=(8, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

结果输出:0
分析:当多个线程同时执行 lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

注意:获得锁的线程使用完要释放锁,否则会造成死线程,推荐使用 try…finally 方式。

缺点

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

3、多核CPU

多核CPU遇到死循环时:

  • CPU使用率:一个死循环线程会100%占用一个CPU;两个则会占到200%CPU(两个CPU核心)==》要想把 N 核 CPU 的核心全部跑满,就必须启动 N 个死循环线程。

在Python中,当解释器执行代码时,有个 GIL锁(Global Interpreter Lock)。任何 Python 线程执行前,先必须获得GIL锁,然后,每执行100条字节码,解释器就自动释放 GIL 锁,让其他的线程有机会执行。
==》多线程在Python中只能交替运行,也只能用到一个核。
==》可以使用多线程,但不能有效利用多核,除非通过C扩展实现。
==》Python 可利用多进程实现多任务。多个进程有自己独立的 GIL 锁,互不影响。

4、ThreadLocal

ThreadLocal 最常用于 为每个线程绑定一个数据库连接、HTTP请求、用户身份信息等。
==》一个线程的所有调用到的处理函数都可以访问这些资源。

import threading

# 创建全局 ThreadLocal 对象
local_school = threading.local()

def process_student():
    # 获取当前线程关联的 student,
    # 每个Thread对它都可读写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)

四、进程 vs. 线程

1、多任务实现

通常设计 Master-Worker 模式,Master 负责分配任务,Worker 负责执行任务。
==》一个 Master、多个 Worker

若用多进程实现 Master-Worker,主进程是 Master,其他进程是 Worker;
==》稳定性高。(一个子进程崩溃了,不会影响主进程和其他子进程。当然主进程挂了所有进程就全挂了,但是Master 进程只负责分配任务,挂掉的概率低,eg:Apache)
==》创建进程的代价大。 Windows下开销巨大。

若用多线程实现 Master-Worker,主线程是 Master,其他线程是 Worker。
==》稍快,效率高(Windows下IIS服务区默认采用多线程模式)
==》致命缺点:任何一个线程挂掉都可能直接造成整个进程崩溃(所有线程共享进程的内存)。

2、线程切换

单任务模式(批处理任务模式):处理完任务A,再处理任务 B …

多任务模式:涉及到任务的切换。操作系统在切换进程或
者线程时也是一样的,它需要先保存当前执行的现场环境( CPU 寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。
==》任务过多,会造成系统处于假死的状态。

3、计算密集型 vs. IO 密集型

计算密集型任务

  • 需要大量的计算,消耗CPU资源。==》强调:代码效率(C语言)
  • 虽可以用多任务完成,但任务越多,切换所需的时间就越多,CPU执行任务的效率就越低 ==》高效:计算密集型任务同时进行的数量等于CPU的数量。

IO 密集型任务

  • 涉及网络、磁盘IO的任务均为IO 密集型任务;
  • CPU 消耗少,大多数时间(99%)是等待IO操作完成。
  • 任务越多,CPU效率越高==》强调:开发效率(Python)

4、异步IO

可实现:用单进程单线程模型执行多任务,称为事件驱动模型

五、分布式进程

Thread vs. Process

  • Process 更稳定,可分布到多台机器上;
  • Thread 最多只能分布到同一台机器的多个 CPU 上。

multiprocessing.managers 模块

  • 支持把多进程分布到多台机器上。一个服务进程为调度者,通过网络通信将任务分布到其他多个进程中。

待完善。。。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值