python多线程与多进程及其区别详解


Posted in Python onAugust 08, 2019

前言

个人一直觉得对学习任何知识而言,概念是相当重要的。掌握了概念和原理,细节可以留给实践去推敲。掌握的关键在于理解,通过具体的实例和实际操作来感性的体会概念和原理可以起到很好的效果。本文通过一些具体的例子简单介绍一下python的多线程和多进程,后续会写一些进程通信和线程通信的一些文章。

python多线程

python中提供两个标准库thread和threading用于对线程的支持,python3中已放弃对前者的支持,后者是一种更高层次封装的线程库,接下来均以后者为例。

创建线程

python中有两种方式实现线程:

1.实例化一个threading.Thread的对象,并传入一个初始化函数对象(initial function )作为线程执行的入口;

2.继承threading.Thread,并重写run函数;

方式1:创建threading.Thread对象

import threading
import time
def tstart(arg):
 time.sleep(0.5)
 print("%s running...." % arg)
if __name__ == '__main__':
 t1 = threading.Thread(target=tstart, args=('This is thread 1',))
 t2 = threading.Thread(target=tstart, args=('This is thread 2',))
 t1.start()
 t2.start()
 print("This is main function")

结果:

This is main function
This is thread 2 running....
This is thread 1 running....

方式2:继承threading.Thread,并重写run

import threading
import time
class CustomThread(threading.Thread):
 def __init__(self, thread_name):
  # step 1: call base __init__ function
  super(CustomThread, self).__init__(name=thread_name)
  self._tname = thread_name
 def run(self):
  # step 2: overide run function
  time.sleep(0.5)
  print("This is %s running...." % self._tname)
if __name__ == "__main__":
 t1 = CustomThread("thread 1")
 t2 = CustomThread("thread 2")
 t1.start()
 t2.start()
 print("This is main function")

执行结果同方式1.

threading.Thread

上面两种方法本质上都是直接或者间接使用threading.Thread类

threading.Thread(group=None, target=None, name=None, args=(), kwargs={})

关联上面两种创建线程的方式:

import threading
import time
class CustomThread(threading.Thread):
 def __init__(self, thread_name, target = None):
  # step 1: call base __init__ function
  super(CustomThread, self).__init__(name=thread_name, target=target, args = (thread_name,))
  self._tname = thread_name
 def run(self):
  # step 2: overide run function
  # time.sleep(0.5)
  # print("This is %s running....@run" % self._tname)
  super(CustomThread, self).run()
def target(arg):
 time.sleep(0.5)
 print("This is %s running....@target" % arg)
if __name__ == "__main__":
 t1 = CustomThread("thread 1", target)
 t2 = CustomThread("thread 2", target)
 t1.start()
 t2.start()
 print("This is main function")

结果:

This is main function
This is thread 1 running....@target
This is thread 2 running....@target

上面这段代码说明:

1.两种方式创建线程,指定的参数最终都会传给threading.Thread类;

2.传给线程的目标函数是在基类Thread的run函数体中被调用的,如果run没有被重写的话。

threading模块的一些属性和方法可以参照官网,这里重点介绍一下threading.Thread对象的方法

下面是threading.Thread提供的线程对象方法和属性:

  • start():创建线程后通过start启动线程,等待CPU调度,为run函数执行做准备;
  • run():线程开始执行的入口函数,函数体中会调用用户编写的target函数,或者执行被重载的run函数;
  • join([timeout]):阻塞挂起调用该函数的线程,直到被调用线程执行完成或超时。通常会在主线程中调用该方法,等待其他线程执行完成。
  • name、getName()&setName():线程名称相关的操作;
  • ident:整数类型的线程标识符,线程开始执行前(调用start之前)为None;
  • isAlive()、is_alive():start函数执行之后到run函数执行完之前都为True;
  • daemon、isDaemon()&setDaemon():守护线程相关;

这些是我们创建线程之后通过线程对象对线程进行管理和获取线程信息的方法。

多线程执行

在主线程中创建若线程之后,他们之间没有任何协作和同步,除主线程之外每个线程都是从run开始被执行,直到执行完毕。

join

我们可以通过join方法让主线程阻塞,等待其创建的线程执行完成。

import threading
import time
def tstart(arg):
 print("%s running....at: %s" % (arg,time.time()))
 time.sleep(1)
 print("%s is finished! at: %s" % (arg,time.time()))
if __name__ == '__main__':
 t1 = threading.Thread(target=tstart, args=('This is thread 1',))
 t1.start()
 t1.join() # 当前线程阻塞,等待t1线程执行完成
 print("This is main function at:%s" % time.time())

结果:

This is thread 1 running....at: 1564906617.43
This is thread 1 is finished! at: 1564906618.43
This is main function at:1564906618.43

如果不加任何限制,当主线程执行完毕之后,当前程序并不会结束,必须等到所有线程都结束之后才能结束当前进程。

将上面程序中的t1.join()去掉,执行结果如下:

This is thread 1 running....at: 1564906769.52
This is main function at:1564906769.52
This is thread 1 is finished! at: 1564906770.52

可以通过将创建的线程指定为守护线程(daemon),这样主线程执行完毕之后会立即结束未执行完的线程,然后结束程序。

deamon守护线程

import threading
import time
def tstart(arg):
  print("%s running....at: %s" % (arg,time.time()))
  time.sleep(1)
  print("%s is finished! at: %s" % (arg,time.time()))
if __name__ == '__main__':
  t1 = threading.Thread(target=tstart, args=('This is thread 1',))
  t1.setDaemon(True)
  t1.start()
  # t1.join()  # 当前线程阻塞,等待t1线程执行完成
  print("This is main function at:%s" % time.time())

结果:

This is thread 1 running....at: 1564906847.85
This is main function at:1564906847.85

python多进程

相比较于threading模块用于创建python多线程,python提供multiprocessing用于创建多进程。先看一下创建进程的两种方式。

The multiprocessing package mostly replicates the API of the threading module.

—— python doc

创建进程

创建进程的方式和创建线程的方式类似:

1.实例化一个multiprocessing.Process的对象,并传入一个初始化函数对象(initial function )作为新建进程执行入口;

2.继承multiprocessing.Process,并重写run函数;

方式1:

from multiprocessing import Process 
import os, time
def pstart(name):
  # time.sleep(0.1)
  print("Process name: %s, pid: %s "%(name, os.getpid()))
if __name__ == "__main__": 
  subproc = Process(target=pstart, args=('subprocess',)) 
  subproc.start() 
  subproc.join()
  print("subprocess pid: %s"%subproc.pid)
  print("current process pid: %s" % os.getpid())

结果:

Process name: subprocess, pid: 4888 
subprocess pid: 4888
current process pid: 9912

方式2:

from multiprocessing import Process 
import os, time
class CustomProcess(Process):
  def __init__(self, p_name, target=None):
    # step 1: call base __init__ function()
    super(CustomProcess, self).__init__(name=p_name, target=target, args=(p_name,))

  def run(self):
    # step 2:
    # time.sleep(0.1)
    print("Custom Process name: %s, pid: %s "%(self.name, os.getpid()))
if __name__ == '__main__':
  p1 = CustomProcess("process_1")
  p1.start()
  p1.join()
  print("subprocess pid: %s"%p1.pid)
  print("current process pid: %s" % os.getpid())

这里可以思考一下,如果像多线程一样,存在一个全局的变量share_data,不同进程同时访问share_data会有问题吗?

由于每一个进程拥有独立的内存地址空间且互相隔离,因此不同进程看到的share_data是不同的、分别位于不同的地址空间,同时访问不会有问题。这里需要注意一下。

Subprocess模块

既然说道了多进程,那就顺便提一下另一种创建进程的方式。

python提供了Sunprocess模块可以在程序执行过程中,调用外部的程序。

如我们可以在python程序中打开记事本,打开cmd,或者在某个时间点关机:

>>> import subprocess
>>> subprocess.Popen(['cmd'])
<subprocess.Popen object at 0x0339F550>
>>> subprocess.Popen(['notepad'])
<subprocess.Popen object at 0x03262B70>
>>> subprocess.Popen(['shutdown', '-p'])

或者使用ping测试一下网络连通性:

>>> res = subprocess.Popen(['ping', 'www.cnblogs.com'], stdout=subprocess.PIPE).communicate()[0]
>>> print res
正在 Ping www.cnblogs.com [101.37.113.127] 具有 32 字节的数据:
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91 来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91
来自 101.37.113.127 的回复: 字节=32 时间=1ms TTL=91

101.37.113.127 的 Ping 统计信息:
  数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
  最短 = 1ms,最长 = 1ms,平均 = 1ms

python多线程与多进程比较

先来看两个例子:

开启两个python线程分别做一亿次加一操作,和单独使用一个线程做一亿次加一操作:

def tstart(arg):
  var = 0
  for i in xrange(100000000):
    var += 1

if __name__ == '__main__':
  t1 = threading.Thread(target=tstart, args=('This is thread 1',))
  t2 = threading.Thread(target=tstart, args=('This is thread 2',))
  start_time = time.time()
  t1.start()
  t2.start()
  t1.join()
  t2.join()
  print("Two thread cost time: %s" % (time.time() - start_time))
  start_time = time.time()
  tstart("This is thread 0")
  print("Main thread cost time: %s" % (time.time() - start_time))

结果:

Two thread cost time: 20.6570000648
Main thread cost time: 2.52800011635

上面的例子如果只开启t1和t2两个线程中的一个,那么运行时间和主线程基本一致。这个后面会解释原因。

使用两个进程进行上面的操作:

def pstart(arg):
  var = 0
  for i in xrange(100000000):
    var += 1
if __name__ == '__main__':
  p1 = Process(target = pstart, args = ("1", ))
  p2 = Process(target = pstart, args = ("2", ))
  start_time = time.time()
  p1.start()
  p2.start()
  p1.join()
  p2.join()
  print("Two process cost time: %s" % (time.time() - start_time))
  start_time = time.time()
  pstart("0")
  print("Current process cost time: %s" % (time.time() - start_time))

结果:

Two process cost time: 2.91599988937
Current process cost time: 2.52400016785

对比分析

双进程并行执行和单进程执行相同的运算代码,耗时基本相同,双进程耗时会稍微多一些,可能的原因是进程创建和销毁会进行系统调用,造成额外的时间开销。

但是对于python线程,双线程并行执行耗时比单线程要高的多,效率相差近10倍。如果将两个并行线程改成串行执行,即:

t1.start()
  t1.join()
  t2.start()
  t2.join()
  #Two thread cost time: 5.12199997902
  #Main thread cost time: 2.54200005531

可以看到三个线程串行执行,每一个执行的时间基本相同。

本质原因双线程是并发执行的,而不是真正的并行执行。原因就在于GIL锁。

GIL锁

提起python多线程就不得不提一下GIL(Global Interpreter Lock 全局解释器锁),这是目前占统治地位的python解释器CPython中为了保证数据安全所实现的一种锁。不管进程中有多少线程,只有拿到了GIL锁的线程才可以在CPU上运行,即时是多核处理器。对一个进程而言,不管有多少线程,任一时刻,只会有一个线程在执行。对于CPU密集型的线程,其效率不仅仅不高,反而有可能比较低。python多线程比较适用于IO密集型的程序。对于的确需要并行运行的程序,可以考虑多进程。

多线程对锁的争夺,CPU对线程的调度,线程之间的切换等均会有时间开销。

线程与进程区别

下面简单的比较一下线程与进程

  • 进程是资源分配的基本单位,线程是CPU执行和调度的基本单位;
  • 通信/同步方式:
    • 进程:
      • 通信方式:管道,FIFO,消息队列,信号,共享内存,socket,stream流;
      • 同步方式:PV信号量,管程
    • 线程:
      • 同步方式:互斥锁,递归锁,条件变量,信号量
      • 通信方式:位于同一进程的线程共享进程资源,因此线程间没有类似于进程间用于数据传递的通信方式,线程间的通信主要是用于线程同步。
  • CPU上真正执行的是线程,线程比进程轻量,其切换和调度代价比进程要小;
  • 线程间对于共享的进程数据需要考虑线程安全问题,由于进程之间是隔离的,拥有独立的内存空间资源,相对比较安全,只能通过上面列出的IPC(Inter-Process Communication)进行数据传输;
  • 系统有一个个进程组成,每个进程包含代码段、数据段、堆空间和栈空间,以及操作系统共享部分 ,有等待,就绪和运行三种状态;
  • 一个进程可以包含多个线程,线程之间共享进程的资源(文件描述符、全局变量、堆空间等),寄存器变量和栈空间等是线程私有的;
  • 操作系统中一个进程挂掉不会影响其他进程,如果一个进程中的某个线程挂掉而且OS对线程的支持是多对一模型,那么会导致当前进程挂掉;
  • 如果CPU和系统支持多线程与多进程,多个进程并行执行的同时,每个进程中的线程也可以并行执行,这样才能最大限度的榨取硬件的性能;

线程和进程的上下文切换

进程切换过程切换牵涉到非常多的东西,寄存器内容保存到任务状态段TSS,切换页表,堆栈等。简单来说可以分为下面两步:

页全局目录切换,使CPU到新进程的线性地址空间寻址;
切换内核态堆栈和硬件上下文,硬件上下文包含CPU寄存器的内容,存放在TSS中;
线程运行于进程地址空间,切换过程不涉及到空间的变换,只牵涉到第二步;

使用多线程还是多进程?

CPU密集型:程序需要占用CPU进行大量的运算和数据处理;

I/O密集型:程序中需要频繁的进行I/O操作;例如网络中socket数据传输和读取等;

由于python多线程并不是并行执行,因此较适合与I/O密集型程序,多进程并行执行适用于CPU密集型程序;

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python编写分类决策树的代码
Dec 21 Python
100行Python代码实现自动抢火车票(附源码)
Jan 11 Python
python 接口返回的json字符串实例
Mar 27 Python
Python import与from import使用及区别介绍
Sep 06 Python
对python:print打印时加u的含义详解
Dec 15 Python
python 内置函数汇总详解
Sep 16 Python
Python爬虫爬取Bilibili弹幕过程解析
Oct 10 Python
django 连接数据库出现1045错误的解决方式
May 14 Python
tensorflow使用CNN分析mnist手写体数字数据集
Jun 17 Python
python递归函数用法详解
Oct 26 Python
详解Python自动化之文件自动化处理
Jun 21 Python
python井字棋游戏实现人机对战
Apr 28 Python
python PIL和CV对 图片的读取,显示,裁剪,保存实现方法
Aug 07 #Python
python实现对图片进行旋转,放缩,裁剪的功能
Aug 07 #Python
Django 实现前端图片压缩功能的方法
Aug 07 #Python
Django 后台获取文件列表 InMemoryUploadedFile的例子
Aug 07 #Python
python 直接赋值和copy的区别详解
Aug 07 #Python
Python中模块(Module)和包(Package)的区别详解
Aug 07 #Python
python的常见矩阵运算(小结)
Aug 07 #Python
You might like
深入理解PHP之require/include顺序 推荐
2011/01/02 PHP
PHP常用处理静态操作类
2015/04/03 PHP
PHP 返回数组后处理方法(开户成功后弹窗提示)
2017/07/03 PHP
深入理解Yii2.0乐观锁与悲观锁的原理与使用
2017/07/26 PHP
php实现将数据做成json的格式给前端使用
2018/08/21 PHP
理解 JavaScript 预解析
2009/10/25 Javascript
浅谈Javascript面向对象编程
2011/11/15 Javascript
JS函数实现动态添加CSS样式表文件
2012/12/15 Javascript
Jquery使用Firefox FireBug插件调试Ajax步骤讲解
2013/12/02 Javascript
javascript获取四位数字或者字母的随机数
2015/01/09 Javascript
jQuery实现下拉框左右移动(全部移动,已选移动)
2016/04/15 Javascript
JS调用某段SQL语句的方法
2016/10/20 Javascript
Easyui笔记2:实现datagrid多行删除的示例代码
2017/01/14 Javascript
详解react如何在组件中获取路由参数
2017/06/15 Javascript
谈谈对vue响应式数据更新的误解
2017/08/01 Javascript
通过一个简单的例子学会vuex与模块化
2017/11/22 Javascript
cordova+vue+webapp使用html5获取地理位置的方法
2019/07/06 Javascript
Nuxt 项目性能优化调研分析
2020/11/07 Javascript
numpy数组拼接简单示例
2017/12/15 Python
使用Python的SymPy库解决数学运算问题的方法
2019/03/27 Python
使用python接入微信聊天机器人
2020/03/31 Python
利用python-docx模块写批量生日邀请函
2019/08/26 Python
python 用户交互输入input的4种用法详解
2019/09/24 Python
详解Python list和numpy array的存储和读取方法
2019/11/06 Python
PyCharm 无法 import pandas 程序卡住的解决方式
2020/03/09 Python
浅谈matplotlib默认字体设置探索
2021/02/03 Python
IE浏览器单独写CSS样式的几种方法
2014/10/14 HTML / CSS
浅谈CSS3特性查询(Feature Query: @supports)功能简介
2017/07/31 HTML / CSS
彪马土耳其官网:PUMA土耳其
2019/07/14 全球购物
澳大利亚Rockwear官网:女子瑜伽、健身和运动服
2021/01/26 全球购物
电子专业自荐信
2014/07/01 职场文书
学习焦裕禄同志为人民服务思想汇报
2014/09/10 职场文书
六查六看自查报告
2014/10/14 职场文书
高中运动会前导词
2015/07/20 职场文书
pytorch--之halfTensor的使用详解
2021/05/24 Python
pnpm对npm及yarn降维打击详解
2022/08/05 Javascript