什么是多线程/多进程

引用虫师的解释:

计算机程序只不过是磁盘中可执行的,二进制(或其它类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命期。

进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间。

线程(有时被称为轻量级进程)跟进程有些相似,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。我们可以想像成是在主进程或“主线程”中并行运行的“迷你进程”。

为什么需要多线程/多进程

我们直接编写的爬虫程序是单线程的,在数据需求量不大时它能够满足我们的需求。

但如果数据量很大,比如要通过访问数百数千个url去爬取数据,单线程必须等待当前url访问完毕并且数据提取保存完成后才可以对下一个url进行操作,一次只能对一个url进行操作;

我们使用多线程/多进程的话,就可以实现对多个url同时进行操作。这样就能大大缩减了爬虫运行时间。

实现多线程/多进程

多线程

python提供了两组多线程接口,一是thread模块_thread,提供低等级接口;二是threading模块,在thread模块基础上进行封装,提供更容易使用的基于对象的接口,可以继承Thread对象来实现多线程。

同时,还有其他线程相关的对象,如Timer、Lock等。

在这里,我们使用threading模块实现多线程。

1. 添加线程

threading.Thread(target, args)

使用threading.Thread()新建一个线程,target是需要执行的函数,args是需要传入该函数的参数,args接受一个tuple,即使只有一个参数也需要写成(x,)形式

import threading

print(threading.active_count()) # 显示当前激活的线程数
print(threading.enumerate()) # 显示当前激活的线程
print(threading.current_thread()) # 当前运行的线程 def thread_job():
print('This is a thread of %s' % threading.current_thread()) def main():
thread = threading.Thread(target=thread_job,) # 添加一个线程
thread.start() # 开始该线程 if __name__ == '__main__':
main()

2. 线程阻塞:join

join()的作用是调用该线程时,等待该线程完成后再继续往下运行。

join通常用于主线程与子线程之间,主线程等待子线程运行完毕后再继续执行,避免子程序和主程序同时运行,子程序还没有运行完的时候主程序就已经运行结束。

import threading
import time # 定义一个fun,传入线程
def T1_job():
print('T1 start\n')
for i in range(10):
time.sleep(0.1)
print('T1 finish\n') def T2_job():
print('T2 start\n')
print('T2 finish\n') def main():
thread1 = threading.Thread(target=T1_job, name='T1') # 添加线程,准备执行thread_job,命名T1
thread2 = threading.Thread(target=T2_job, name='T2') thread1.start() # 执行该线程,没有添加join的时候,同步执行main和thread_job
thread2.start() thread1.join() # 等待thread1完成后才进行下一步-主程序
thread2.join() # 等待thread2完成后才进行下一步-主程序
print('all done') if __name__ == '__main__':
main()

3. 信息传递:Queue队列

Queue是python标准库中的线程安全的队列(FIFO)实现,提供了一个适用于多线程编程的先进先出的数据结构,即队列。

Queue是一种先进先出的数据结构,一般来说读数据都从Queue头读,写数据都从Queue尾写入。

import threading
from queue import Queue def job(l, q):
for i in range(len(l)):
l[i] = l[i]**2
q.put(l) # 线程中,return获取的值无法提取,需要放入q中 def multithreading():
q = Queue() # 队列
threads = [] # 全部线程
data = [[1, 2, 3], [3, 4, 5], [4,4,4], [5,5,5]]
for i in range(4):
# 4个线程来执行job函数
t = threading.Thread(target=job, args=(data[i], q))
t.start()
threads.append(t) # 当前线程加入全部线程中 # 对主线程中的每一个线程都执行join()
for thread in threads:
thread.join() results = [] # 保存结果
for _ in range(4):
results.append(q.get()) # 从q中拿出值,每次只能按顺序拿出一个值
print(results) if __name__ == '__main__':
multithreading() # [[1, 4, 9], [9, 16, 25], [16, 16, 16], [25, 25, 25]]

4. 线程锁:Lock

lock在不同线程使用同一共享内存时,能够确保线程之间互不影响。

使用lock的方法是:在每个线程执行运算修改共享内存之前执行lock.acquire()将共享内存上锁, 确保当前线程执行时,内存不会被其他线程访问;

执行运算完毕后使用lock.release()将锁打开, 保证其他的线程可以使用该共享内存。

lock.acquire()和lock.release()必须成对出现

# lock锁,当前线程运行完成后才进行下一进程
import threading def job1():
global A, lock
lock.acquire() # 打开锁
for i in range(10):
A += 1
time.sleep(0.2)
print('job1', A)
lock.release() # 关闭锁 def job2():
global A, lock
lock.acquire() # 打开锁
for i in range(10):
A += 10
time.sleep(0.2)
print('job2', A)
lock.release() # 关闭锁 if __name__ == '__main__':
lock = threading.Lock() # lock锁
A = 0
t1 = threading.Thread(target=job1)
t2 = threading.Thread(target=job2)
t1.start()
t2.start()

将上述代码中的lock.acquire()和lock.release()四行代码注释后运行,就是不加锁的情况,这时候输出结果都是混乱的。而加锁后,输出结果正常。

5. 线程池

线程池有几种方法可以实现,这里我们使用multiprocessing.dummy库。

from multiprocessing.dummy import Pool as ThreadPool # 线程池
import threading def job(i):
print(i, '\n', threading.current_thread()) if __name__ == '__main__':
pool = ThreadPool(4) # 创建一个包含4个线程的线程池
pool.map(job, range(12))
pool.close() # 关闭线程池的写入
pool.join() # 阻塞,保证子线程运行完毕后再继续主进程 

多进程

多进程multiprocessing和多线程threading类似,都是用在python中进行并行计算的,而多进程则是为了弥补python在多线程中的劣势而出现的。

multiprocessing是使用计算机的多核进行运算,它可以避免多线程中GIL的影响。

python使用multiprocessing模块实现多进程,用法和threading基本一致。

1. 添加进程

multiprocessing.Process(target, args)

使用multiprocessing.Process新建一个进程,target是需要执行的函数,args是需要传入该函数的参数,args接受一个tuple,即使只有一个参数也需要写成(x,)形式

import multiprocessing as mp

def job(a,d):
print('aaaaa') if __name__=='__main__':
p1 = mp.Process(target=job,args=(1,2)) # 添加一个进程
p1.start()
p1.join()

2. 信息传递:Queue队列

多进程中的Queue使用同多线程一致,同样为先进先出

多进程可以直接从multiprocessing.Queue()导入Queue队列。

import multiprocessing as mp

def job(q):
res=0
for i in range(1000):
res+=i+i**2+i**3
q.put(res) # 将值放入队列 if __name__=='__main__':
q = mp.Queue() # Queue队列
p1 = mp.Process(target=job,args=(q,))
p2 = mp.Process(target=job,args=(q,))
p1.start()
p2.start()
p1.join()
p2.join()
res1 = q.get() # 从队列中取出值
res2 = q.get() # 从队列中取出值
print(res1, res2)

3. 进程池

import multiprocessing as mp

def job(x):
return x*x def multicore():
pool = mp.Pool() # 定义一个进程池
res = pool.map(job, range(100))
print(res) if __name__=='__main__':
multicore()

关于进程池的更多信息请跳转至:

4. 共享内存

一般的变量在进程之间是没法进行通讯的,multiprocessing 给我们提供了 Value 和 Array 模块,他们可以在不通的进程中共同使用。

Value()和Array()都接受两个参数,第一个为数据类型,第二个是传入的数。

Value()可以接受传入单个数值,Array()可以接受传入一个一维数组

import multiprocessing as mp

value1 = mp.Value('i', 0) # value接受单个数值,i表示一个带符号的整型
array = mp.Array('i', [1, 2, 3, 4]) # Array接受一个一维数组 array2 = mp.Array('i', [[1,2], [2,3]]) # 传入一个二维数组错误,传入参数非一维数组

数据类型如下:

Type code
C Type Python Type Minimum size in bytes
'b' signed char int 1
'B' unsigned char int 1
'u' py_UNICODE Unicode character 2
'h' signed short int 2
'H' unsigned short int 2
'i' signed int int 2
'I' unsigned int int 2
'l' signed long int 4
'L' unsigned long int 4
'q' signed long long int 8
'Q' unsigned long long int 8
'f' float float 4
'd'
double
float 
8

5. 进程锁

进程锁同线程锁使用方法一致,lock在不同进程使用同一共享内存时,能够确保进程之间互不影响。

使用lock的方法是:在每个进程执行运算修改共享内存之前执行lock.acquire()将共享内存上锁, 确保当前进程执行时,内存不会被其他进程访问;

执行运算完毕后使用lock.release()将锁打开, 保证其他的进程可以使用该共享内存。

lock.acquire()和lock.release()必须成对出现

import multiprocessing as mp

def job(v, num, l):
l.acquire() # 锁住
for _ in range(5):
time.sleep(0.1)
v.value += num # 获取共享内存
print(v.value)
l.release() # 释放 def multicore():
l = mp.Lock() # 定义一个进程锁
v = mp.Value('i', 0) # 定义共享内存
p1 = mp.Process(target=job, args=(v,1,l)) # 需要将lock传入
p2 = mp.Process(target=job, args=(v,3,l))
p1.start()
p2.start()
p1.join()
p2.join() if __name__ == '__main__':
multicore()

如何选择多线程/多进程

1. 结论

CPU密集型代码(各种循环处理、计算等等):使用多进程

IO密集型代码(文件处理、网络爬虫等):使用多线程

2. 解释

多线程和多进程的理解可以类比于公路。

假设当前公路均为单行道,并且出于安全考虑,一个车道只能同时行驶一辆汽车,一条公路只有一名驾驶员。只有一名指挥者进行集中调度,驾驶员获取到了指挥者的调度信息才会驾驶。

单线程是只有一条公路而且是单车道,只能同时行驶一辆汽车;

多线程是只有一条公路,但是是多车道,可以同时行驶多辆汽车;

多进程是有很多条公路,每条公路可能是单车道也可能是多车道,同样可以同时行驶多辆汽车。

因为GIL的存在,python中的多线程其实在同一时间只能运行一个线程,就像一名驾驶员只能同时驾驶一辆汽车。四线程类比于一条四车道的公路,但是驾驶员可以从驾驶车道A上的汽车切换至驾驶车道B上的汽车,驾驶员切换的速度够快的话,看起来就像是这条公路上的四辆汽车都在同时行驶。指挥者发布的命令只需要跨越车道就能传递给驾驶员,命令传输的时间损耗相对较小。所以对于多线程,我们希望指挥者可以比较频繁发布命令,驾驶员获取到命令后能够很快就完成然后切换到下一个车道继续执行命令,这样看起来就像是驾驶员同时驾驶四辆汽车了。所以对于IO密集型代码,推荐使用多线程。

而对于多进程来说,每条公路都有一名驾驶员,四线程类比于四条公路,则四名驾驶员可以同时驾驶四辆汽车。但指挥者发布的命令需要跨越公路才能传递给驾驶员,命令传输的时间损耗相对较大。所以对于多进程,我们希望指挥者发布一次命令后驾驶员可以执行较长时间,这样就不必把时间过多花费在信息传输上。所以对于CPU密集型代码,推荐使用多进程。

参考资料

1. python的多线程中的join的作用

2. python队列Queue

3. Python多线程(2)——线程同步机制

4. 莫烦PYTHON-Threading多线程

5. Python 多进程锁 多进程共享内存

6. python学习笔记——多进程中共享内存Value & Array

7. 莫烦PYTHON-multiprocessing多进程

8. python 之 多进程

9. Python多进程

10. Python 使用multiprocessing 特别耗内存

11. 廖雪峰-进程和线程

12.python 多线程,详细教程,线程同步,线程加锁,ThreadPoolExecutor

13. 多进程 multiprocessing 多线程Threading 线程池和进程池concurrent.futures

python爬虫入门八:多进程/多线程的更多相关文章

  1. Python爬虫(十八)_多线程糗事百科案例

    多线程糗事百科案例 案例要求参考上一个糗事百科单进程案例:http://www.cnblogs.com/miqi1992/p/8081929.html Queue(队列对象) Queue是python ...

  2. Python 爬虫入门(二)——爬取妹子图

    Python 爬虫入门 听说你写代码没动力?本文就给你动力,爬取妹子图.如果这也没动力那就没救了. GitHub 地址: https://github.com/injetlee/Python/blob ...

  3. Python 爬虫入门之爬取妹子图

    Python 爬虫入门之爬取妹子图 来源:李英杰  链接: https://segmentfault.com/a/1190000015798452 听说你写代码没动力?本文就给你动力,爬取妹子图.如果 ...

  4. Python爬虫进阶五之多线程的用法

    前言 我们之前写的爬虫都是单个线程的?这怎么够?一旦一个地方卡到不动了,那不就永远等待下去了?为此我们可以使用多线程或者多进程来处理. 首先声明一点! 多线程和多进程是不一样的!一个是 thread ...

  5. Python爬虫入门一之综述

    大家好哈,最近博主在学习Python,学习期间也遇到一些问题,获得了一些经验,在此将自己的学习系统地整理下来,如果大家有兴趣学习爬虫的话,可以将这些文章作为参考,也欢迎大家一共分享学习经验. Pyth ...

  6. python爬虫入门-开发环境与小例子

    python爬虫入门 开发环境 ubuntu 16.04 sublime pycharm requests库 requests库安装: sudo pip install requests 第一个例子 ...

  7. Python爬虫入门教程 48-100 使用mitmdump抓取手机惠农APP-手机APP爬虫部分

    1. 爬取前的分析 mitmdump是mitmproxy的命令行接口,比Fiddler.Charles等工具方便的地方是它可以对接Python脚本. 有了它我们可以不用手动截获和分析HTTP请求和响应 ...

  8. Python爬虫入门教程 43-100 百思不得姐APP数据-手机APP爬虫部分

    1. Python爬虫入门教程 爬取背景 2019年1月10日深夜,打开了百思不得姐APP,想了一下是否可以爬呢?不自觉的安装到了夜神模拟器里面.这个APP还是比较有名和有意思的. 下面是百思不得姐的 ...

  9. Python爬虫入门之正则表达式

    在前面我们已经搞定了怎样获取页面的内容,不过还差一步,这么多杂乱的代码夹杂文字我们怎样把它提取出来整理呢?下面就开始介绍一个十分强大的工具,正则表达式! 1.了解正则表达式 正则表达式是对字符串操作的 ...

随机推荐

  1. Java Web项目在Mac系统上启动时提示nodename nor servname provided的解决办法

    今天在Mac系统上启动Java Web项目的时候,提示了Java.net.UnknownHostException: yangxiaomindeMacBook-Pro.local nodename n ...

  2. cucumber的疑问解答

    在cucumber的自动化测试框架下面,在一个steps文件中定义的@page对象,可以在其他的不同的steps文件中调用,在整个的场景生命周期中都是有效的 原因:cucumber开始执行时,一次性把 ...

  3. webpack.config.js====引入Jquery库文件

    1. 安装 cnpm install --save jquery expose-loader 2. 在webpack.config.js中配置 Jquery库是使用的webpack的一个插件Provi ...

  4. springboot+Jsp部署linux

    这个springboot部署到linux,我之前一直都是在linux上使用tomcat部署,但是这样部署容易出现EL表达式无法使用导致项目报错:后来发现了一种更简单的方法,就是将项目打成war包,注册 ...

  5. subline 安装 package control 连接服务器失败,解决办法

    解决办法: 打开C:\Windows\system32\drivers\etc\hosts文件 增加 50.116.34.243 sublime.wbond.net50.116.34.243 pack ...

  6. hibernate课程 初探单表映射1-4 hibernate开发前准备

    开发前准备: 1 eclipse 2 hibernate tools的安装(需要相关的jar包)(可以简化orm框架) hibernate tools的安装步骤: 1 到官网下载 https://so ...

  7. 符号替换问题:请实现一个函数,将一个字符串中的空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

    public class Solution { public String replaceSpace(StringBuffer str) { String str1=str.toString(); c ...

  8. agc016B - Colorful Hats(智商题)

    题意 题目链接 有$n$个人,每个人有一种颜色,第$i$个人说除了我之外有$a_i$种不同的颜色,问是否存在一组合法解 Sol 700分的题就这么神仙了么..好难啊... 先说结论吧 设$mx, mn ...

  9. SpringBoot的快速构建

    1.http://start.spring.io2.Spring Tool Suite3.IntelliJ IDEA4.Spring Boot CLI5.Maven手工构建

  10. python 之Requests库学习笔记

    1.    Requests库安装 Windows平台安装说明: 直接以管理员身份打开cmd运行界面,使用pip管理工具进行requests库的安装. 具体安装命令如下: >pip instal ...