多进程与多线程

我们都知道,操作系统中所有的程序都是以进程的方式来运行的,或者说我们把运行着的程序称为进程(Process)。例如运行记事本程序就是启动一个记事本进程,运行两个记事本就是启动两个记事本进程。

很多时候,进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。由于每个进程至少要干一件事,所以,一个进程至少有一个线程。

进程和线程的区别主要有:

  • 进程之间是相互独立的,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,且互不影响;而同一个进程的多个线程是内存共享的,所有变量都由所有线程共享。
  • 由于进程间是独立的,因此一个进程的崩溃不会影响到其他进程;而线程是包含在进程之内的,线程的崩溃就会引发进程的崩溃,继而导致同一进程内的其他线程也奔溃。

真正的多进程或者多线程都需要多核 CPU 才可能实现。现在虽然多核 CPU 已经非常普及,但是任务数量远远多于 CPU 的核心数量,所以,操作系统会自动地把进程或线程轮流调度到每个核心上执行。例如对于核心 a,进程 1 执行 0.01 秒,切换到进程 2,进程 2 执行 0.01 秒,再切换到进程 3……每个进程都是交替执行的,但是由于 CPU 的执行速度实在是太快了,我们感觉就像这些进程都在同时执行一样。多线程的执行方式类似,每一个核心都在操作系统的调度下,在多个线程之间快速切换,让每个线程都短暂地交替运行。

目前,要同时完成多个任务通常有以下几种解决方案:

  • 一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
  • 还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
  • 当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了。但是这种模型很复杂,实际很少采用。

总结一下就是,多任务的实现有3种方式:

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

同时执行多个任务通常各个任务之间并不是没有关联的,而是需要相互通信和协调,有时,任务 1 必须暂停等待任务 2 完成后才能继续执行,有时,任务 3 和任务 4 又不能同时执行。所以,多进程和多线程的程序的复杂度要远远高于单进程单线程的程序。

Python 多进程

对于 Unix/Linux 操作系统来说,系统本身就提供了一个 fork() 系统调用,可以非常方便地创建多进程。Python的os 模块封装了常见的系统调用,其中就包括 fork

import osprint('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
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 (1806) start...
I (1806) just created a child process (1809).
I am child process (1809) and my parent is 1806.

普通的函数调用,调用一次,返回一次,但是 fork() 调用一次,返回两次,因为操作系统自动把当前进程(父进程)复制了一份(子进程),然后,分别在父进程和子进程内返回。子进程永远返回 0,而父进程返回子进程的 ID。

multiprocessing 模块

由于 Windows 系统没有 fork 调用,因而 Python 提供了一个跨平台的多进程模块multiprocessing,模块中使用 Process 类来代表一个进程对象。下面的例子演示了启动一个子进程并等待其结束:

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 928.
Process will start.
Run child process test (929)...
Process end.

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

进程池 Pool

如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

from multiprocessing import Pool
import os, time, randomdef 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.

在上面的代码中,Pool 用于生成进程池,对 Pool 对象调用 apply_async() 方法可以使每个进程异步执行任务,也就说不用等上一个任务执行完才执行下一个任务。对 Pool 对象调用join() 方法会等待所有子进程执行完毕,调用 join() 之前必须先调用 close() 以关闭进程池,确保没有新的进程加入。

输出的结果中,task 4 要等待前面某个 task 完成后才执行,这是因为我们把 Pool 的大小设置成了 4,因此,最多同时执行 4 个进程。如果改成 p = Pool(5),就可以同时跑5个进程。Pool 的默认大小是 CPU 的核数。

进程间通信

Process 之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python 的 multiprocessing 模块包装了底层的机制,提供了队列(Queue)、管道(Pipes)等多种方式来交换数据。

我们以队列(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__':q = Queue() # 父进程创建 Queue,并传给各个子进程:pw = Process(target=write, args=(q,))pr = Process(target=read, args=(q,))pw.start() # 启动子进程 pw,写入pr.start() # 启动子进程 pr,读取pw.join() # 等待pw结束pr.terminate() # pr进程里是死循环,无法等待其结束,只能强行终止:

运行结果如下:

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.

Python 多线程

多任务可以由多进程完成,也可以由一个进程内的多线程完成。我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python 也不例外,并且,Python 的线程是真正的 Posix Thread,而不是模拟出来的线程。

Python 的标准库提供了两个模块:_thread 和 threading。其中,_thread 是低级模块,threading 是高级模块,对_thread 进行了封装。绝大多数情况下,我们只需要使用 threading 这个高级模块。

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

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

由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程。Python 的 threading 模块有个current_thread() 函数,它永远返回当前线程的实例。主线程实例的名字叫 MainThread,子线程的名字在创建时指定。上例中,我们用 LoopThread 命名子线程,如果不起名字 Python 就自动给线程命名为 Thread-1,Thread-2……

Lock

多线程和多进程最大的不同在于:多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响;而多线程中,所有变量由所有线程共享。因为任何一个线程都可以修改任何一个变量,所以线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

来看看多个线程同时操作一个变量怎么把内容给改乱了:

import time, threadingbalance = 0 # 假定这是你的银行存款:# 先存后取,结果应该为0
def change_it(n):global balancebalance = balance + nbalance = balance - ndef run_thread(n):for i in range(100000):change_it(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)

我们定义了一个共享变量 balance,初始值为 0,并且启动两个线程,先存后取,理论上结果应该为 0。但是,由于线程的调度是由操作系统决定的,当 t1、t2 交替执行时,只要循环次数足够多,balance 的结果就不一定是 0 了。

原因是因为高级语言的一条语句在 CPU 执行时是若干条语句。例如一个简单的计算 balance = balance + n,也分两步:

  • 计算 balance + n,存入临时变量中;
  • 将临时变量的值赋给 balance。

如果操作系统以下面的顺序执行 t1、t2:

初始值 balance = 0t1: x1 = balance + 5  # x1 = 0 + 5 = 5t2: x2 = balance + 8  # x2 = 0 + 8 = 8
t2: balance = x2      # balance = 8t1: balance = x1      # balance = 5
t1: x1 = balance - 5  # x1 = 5 - 5 = 0
t1: balance = x1      # balance = 0t2: x2 = balance - 8  # x2 = 0 - 8 = -8
t2: balance = x2   # balance = -8结果 balance = -8

究其原因,是因为修改 balance 需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。

如果我们要确保 balance 计算正确,就要给 change_it() 上一把锁。当某个线程开始执行change_it() 时,由于该线程获得了锁,因此其他线程不能同时执行 change_it(),只能等待,直到锁被释放,这样就可以避免修改的冲突。创建一个锁可以通过 threading.Lock() 来实现:

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

当多个线程同时执行 lock.acquire() 时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程,所以我们用try...finally 来确保锁一定会被释放。

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多。首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

GIL 锁

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

GIL 是 Python 解释器设计的历史遗留问题,通常我们用的解释器是官方实现的 CPython,要真正利用多核,除非重写一个不带 GIL 的解释器。所以,在 Python 如果一定要通过多线程利用多核,那只能通过 C 扩展来实现。

因而,多线程的并发在 Python 中就是一个美丽的梦,如果想真正实现多核任务,还是通过多进程来实现吧。

python多线程 多进程相关推荐

  1. python多线程多进程

    一.线程&进程 对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程, ...

  2. Python多线程多进程应用场景

    IO密集型: 推荐使用多线程 CPU密集型: 推荐使用多进程 为什么Python多线程在CPU密集型中是鸡肋?通过一段代码来测试 单线程执行从1亿递减到0 import datetimedef run ...

  3. Python多线程多进程、异步、异常处理等高级用法

    文章目录 前言 多线程多进程 多线程 多进程 协程 总结 异步 基本概念 异步编程 asyncio aiohttp 异常 常见异常 异常处理 自定义异常 lambda表达式 lambda表达式用法 高 ...

  4. Python 多线程+多进程简单使用教程,如何在多进程开多线程

    一.Python多进程多线程 关于python多进程多线程的相关基础知识,在我之前的博客有写过,并且就关于python多线程的GIL锁问题,也在我的一篇博客中有相关的解释. 为什么python多线程在 ...

  5. Python 多线程 多进程 协程 yield

    python中多线程和多进程的最大区别是稳定性和效率问题 多进程互相之间不影响,一个崩溃了不影响其他进程,稳定性高 多线程因为都在同一进程里,一个线程崩溃了整个进程都完蛋 多进程对系统资源开销大,多线 ...

  6. python 多线程多进程logging系统写入同一日志文件处理方法

    多线程进程,logging写入日志到同一文件的处理方法 python logging系统切分问题 TimedRotatingFileHandler切分逻辑源码 解决方案 python logging系 ...

  7. python多线程多进程多协程_python 多进程、多线程、协程

    1.python的多线程 多线程就是在同一时刻执行多个不同的程序,然而python中的多线程并不能真正的实现并行,这是由于cpython解释器中的GIL(全局解释器锁)捣的鬼,这把锁保证了同一时刻只有 ...

  8. python学习笔记(十六)-Python多线程多进程

    一.线程&进程 对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程, ...

  9. python 多线程 多进程 zmq_研二硕, Python +pyqt,多进程问题求助

    51 60 天前 @knightdf import sys from PyQt5.QtWidgets import QMainWindow,QApplication,QWidget from unti ...

最新文章

  1. 【福利】IT学习视频免费送:思科/华为、Liunx、ORACLE、VMware等等
  2. 北斗导航 | 北斗三号之RDSS短报文之双向零值
  3. Stack Overflow 2016 最新架构探秘
  4. 数据湖元数据服务的实现和挑战
  5. mysql解压包安装出现 No such file or directory错误的解决办法
  6. 浅谈FTP服务的几个知识点
  7. 统计github本地仓库的代码行数
  8. 计算机cf编程,警察牧马人宏自定义编程计算机游戏鼠标有线大声笑/ cf英雄联盟光速质量保证....
  9. Python数据处理(一)
  10. caxa发生文件读写异常_文件和异常
  11. 服务器频繁重启怎么解决
  12. 华为软件编程规范和范例
  13. 2016年腾讯校招笔试题
  14. 位置不可用无法访问介质受写入保护怎样解决?
  15. java获取时分秒毫秒_java 中毫秒数转换成时分秒格式java中有什么方法可以把一个毫秒数格式化成”时:分:秒”...
  16. 【无标题】人工智能的定义
  17. Photoshop去除图片水印
  18. win10系统下,KMSpico安装过程中出现“无法完成操作,因为文件包含病毒或潜在的垃圾软件”
  19. 清除office多余的激活信息
  20. 7-51单片机ESP8266学习-AT指令(测试TCP服务器--51单片机程序配置8266,用手机TCP调试助手发信息给单片机控制小灯的亮灭)

热门文章

  1. 英伟达与 ARM 初携手,英国共建 AI 研究中心
  2. 女朋友想进高校当老师,其实中学老师更适合他
  3. 上海市消保委:春节长假期间 共受理消费者投诉4600件
  4. 钱大妈关闭所有北京门店:低估了北京市场的难度
  5. 爱奇艺CEO龚宇呼吁网络电影涨价:6块钱太低了
  6. Meta最快明年推出智能手表 挑战下一代Apple Watch
  7. 消息人士:苹果正与比亚迪宁德时代洽谈电动汽车电池供应事宜
  8. 腾讯:人们回归工作导致四季度游戏收入减缓
  9. 苹果推出iCloud照片转移服务 能轻松转到谷歌相册
  10. 618期间, “直播带货”翻车负面信息暴增