前言
在入门python后,第一件事当然是上手爬虫项目,比如爬爬图片什么的,练练手。的确,简单的小爬虫可以用很简短的代码逻辑便可实现业务需求。但是后来发现,爬虫速度实在太慢了,特别是下载图片的时候,苦苦等待,心想:能否为爬虫提速呢?
于是乎,经过一番折腾摸索,才发现了线程和进程的概念。小弟不才,以下仅仅只是个人的一些学习总结,如有纰漏或错误,烦请各位看官指正讨论,一起营造学习氛围。
1. 多进程
进程(process),其实就是我们电脑中的每一个任务,如打开并运行中的QQ便是一个进程。事实上,我们可以打开任务管理器(Task Manager),就可了解到电脑目前正在运行的的所有进程。
值得注意的是,单核CPU一次只能运行一个进程(任务)!
但我们的电脑CPU不过4核,或8核,那为什么我们平时却可以打开那么多的任务同时处理呢?也就是俗称的“多任务工作”,那这不就核上述定义有所出入了吗?
非也!之所以可以在单核CPU上运行多个进程(表面现象),是因为错觉的误导!实际上,并表面看似同时运行的进程,在暗地里是逐个单一运行的。这里就要牵涉到“并发”和“并行”的概念了
1.1 并发
“并发”(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
1.2 并行
“并行”(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行
参考知乎某一回答(本人改进版):
1. 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 ==>单线程滴同步干活!(做完A,再做B)
2. 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 ==>(A,B任务都由同一个人(处理机,or cpu)启动了,A,B互相切换的进行)
3. 你吃饭吃到一半,电话来了,你一边吃饭,你妈妈一边接电话说话,这说明你的家支持并行。 ==>(A,B两个任务 同时 进行!你:是一个核心,你妈:是另外一个核心)
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
所以我认为它们最关键的点就是:是否是『同时』
参考:并发和并行有什么区别?cloud.tencent.com
所以,回到刚才提出的那个问题,为什么单核cpu的计算机表面上看好像也能多进程(多任务)执行呢? 实际上,这压根就不是多任务“同时(并行)”执行,不过是一种并发执行罢了。也就是先执行了A任务比如0.1s,然后切换执行B任务0.2s之类,速度之快,让我们觉得同时执行而已!
也就是说,单核cpu每次只能执行一个进程依旧是成立的!
而多进程的同时执行,必定需要运用多核,也就是得并行了
2. 多线程
说完进程,再来谈谈线程。如果说单核cpu是一个工厂,而工厂内虽有许多车间(进程/任务),但每次工厂的电力(系统资源)只足够供一个车间(进程)运行,而每个车间可以至少对应1个或以上工人在工作(线程)。
这里的车间便是一个进程,而工人便是线程。可以看出,一个进程必定最少包含一个线程或以上。车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
i. 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
ii. 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。
不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
参考:进程与线程的一个简单解释 - 阮一峰的网络日志www.ruanyifeng.com
常见问题:
问1:市场上所谓的八核,四核处理器的核心数与真实的cpu个数有什么区别?
- CPU核心数量与CPU数量:一个核心,在此时此刻,只能处理一个任务。一个核心,处理多任务的方法,要么排队,一个一个的。要么一个执行一小会儿,在多个任务间切换。当然了,没有被服务的任务只能等待了,表现就是你的电脑有点卡。如果你的CPU有两个核心,那么它的每个核心,在此时此刻,可以分别服务一个任务。这样可以减少等待,所以你看看CPU的发展史,从一开始的提高时钟频率,到后来的增加核心数。你有多个核心,就相当于你有很多个工人给你干活。你有一个核心,就相当于你只有一个工人给你干活。很明白了吧。那多核CPU不就是工人组团吗。然后CPU核心的时钟频率就是工人的工作效率。你当然喜欢工作效率高的工人。当然你还可以给工人施加压力,让他们超负荷工作,当然是有把工人累坏的风险。这就是CPU超频。
问2:是四个cpu核心就能理论上支持四个并行运算吗?
- 多核心CPU就可以并行了。没错。
问3:通常说的多线程开发,多进程开发是真正意义上的并行还是并发?
- 多线程,多进程是并行还是并发取决于你的CPU核心数量。如果是单核CPU,多线程也没用。如果是多核心CPU,那么就可以并行了。CPU多一个核心,这个多出来的核心就可以多处理一个线程。
问4:异步和并发的区别在哪里?他们的实现原理分别是什么?
- 异步对应的概念是同步。多线程是实现异步的方法。多线程并行(同时进行)(这属于异步)依赖于多核心。多线程并发(把核心比作勺子,把每个线程比作人,把线程的任务比作喝汤。只有一个勺子(单核),一人一口轮着喝(并发))(也属于异步)不依赖与多核心。把一个马桶比作一个CPU核心,线程比做人,那么一个马桶,蹲这个马桶就属于同步,不能拉一半换另一个人拉,然后再换回来接着拉。必须上一个人拉完了,下一个人才能进来拉。
参考:大江狗:一文看懂Python多进程与多线程编程(工作学习面试必读)zhuanlan.zhihu.com
补充:
GIL锁
刚才上面多线程提到,可以设定“锁”来控制线程切换,如只有1把锁的时候(上厕所),只有单一线程跑动,只有等他跑完(上完厕所),下一个线程才可以获得这把锁去上。如果是10把锁的时候(如去厨房干活),如果10把锁都被线程获取完毕了,其他人(线程)只能等待,等有锁空出来,才能获得锁从而干活,而获得这10把锁的人,理论上来说是在同时干活的(并行),假设,电脑cpu有10个核心,如果查看任务管理器,理论上看到cpu占有率应该是1000%,但实际上可能才140%左右,只占用了1个核心多一些而已!(并发运行)
然而这是为什么呢??
这其实是由于Python和其他编程语言不同,存在一个全局解释器锁(GIL锁),在执行多线程的时候,哪怕我们没给这些线程上锁,如上锁10个,正常情况下只有这10个获得锁的才能并行运行,其他线程被阻塞等待。而没上锁的话,理论上所有被创建出来的线程都应该是并行处理,同时运行的!,但实际上却是并发的!!这背后便是因为python本身就上了一把GIL锁导致的,从而阻塞了其他线程。这也就是为什么许多人会说:python的多线程就是鸡肋。
然而这句话,对,也不全对!
是不是意味着python的多线程毫无意义呢?---当然不是!
python的多线程要真正派上用场,那就要分辨所要执行的任务是“CPU计算密集型”亦或“IO密集型”
CPU计算密集型 vs IO密集型
CPU计算密集型任务:指该被执行任务,需要消耗大量的cpu计算资源,如运算大量数据等。
而IO密集型任务:则指需要大量写入写出的任务,如图片,视频,音频的写入等,在python里很好的一个例子就是爬虫,需要大量的写入文件操作。
而python的多线程能真正的发挥作用的,便是IO密集型任务,不需要占用太多的CPU计算资源,在等待文件写入时,可以不必等完才执行。从而提高效率!!!
所以:在python中,如果是IO密集型,推荐多线程,计算密集型推荐多进程
项目:Python爬虫利用多线程提升效率爬取图片!
代码详情:ChanForWang/python-crawl-pic-by-using-Multi-thread (github.com)github.com
import requests
import parsel
import threading
import os
import time
#图片地址
# https://www.jpxgmn.cc/YouMi/ + upload。。。。
'''
need:
1:相册自建立
2:input相册网址智能下载
3:所有站都可:如优美etc
'''
# 设置最大线程锁 => 等同于10个线程一起做,然后只有这10个线程的锁release了,
# 才会给下面另外10个线程获得锁。 如:下载30个任务,有10个线程, 前10个获得锁,只有等这10个下完了,第11到20才能获得锁,
# 其实就是规定只有10个线程在跑,其他的没有获得锁的线程,就被阻塞了
#然而如果不加这把锁,便代表有几个图片,就有几个线程在跑。
#if value=1, 其实就是单线程在跑,就和普通版一模一样。需要完全等待下载完成,锁释放,才轮到下一个。而value=10,有10把锁,这10个线程可以同时下载,不用非等下载完成。但第11个就要等待其中的某个线程完成下载,把锁释放,才能获取,从而工作
thread_lock = threading.BoundedSemaphore(value=81)
#请求相册的第一页
def getHtml(url, headers):
response = requests.get(url, headers)
response.encoding = response.apparent_encoding
response = response.text
return response
def getPagesDataList(url,headers,base_url,img_source):
response = getHtml(url,headers)
html_data = parsel.Selector(response) #return a selector object
total_page_num_list = html_data.xpath('//article[@class="article-content"]/div[1]/ul/a/text()').extract()
total_page_num = total_page_num_list[-2] #获取倒数第二个值作为总页数,因为这个列表的倒数第一个是“下一页”
print("-----共有{}页".format(total_page_num))
pages_data = []
for num in range(int(total_page_num)):
if num == 0:
#根据图片地址规则发现,为1时是第二页,为0时,就是第一页,所以保持原地址
next_img_page_url = url
else:
next_img_page_url = base_url + '/{}/'.format(img_source) + url.split("/")[-1].split(".")[0] + '_{}'.format(num) + '.html'
print(next_img_page_url)
res = getHtml(next_img_page_url,headers)
requests.packages.urllib3.disable_warnings()
pages_data.append(res)
return pages_data
#获取所有页面的img_urls, 放入一个list
def getImgUrlsList(pages_data):
img_urls = []
for page in pages_data:
html_data = parsel.Selector(page)
img_url_list = html_data.xpath('//article[@class="article-content"]/p/img/@src').extract()
img_urls.extend(img_url_list)
print("------------------{} pics in total".format(len(img_urls)))
return img_urls
def downloadImg(img_url,headers,num,base_url,file_name):
if not os.path.exists("jpxgmv.com/" + file_name):
os.mkdir("jpxgmv.com/" + file_name)
path = "jpxgmv.com/" + file_name + "/" + str(num)+ ".jpg"
new_img_url = base_url + img_url
# verify = False 是为了关闭SSL认证(关于网址重定向的,as requests是基于http的,访问https会重定向)
img_data = requests.get(new_img_url, headers=headers, verify=False).content
# 这是去除警告
requests.packages.urllib3.disable_warnings()
with open(path,"wb") as f:
f.write(img_data)
# 下载完了,解锁
thread_lock.release()
def main():
url = input("请输入jpxgmv.comの相册-网址:")
start_t = time.time()
headers = {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36 Edg/91.0.864.37","referer":url}
img_source = url.split('/')[-2]
base_url = "https://www.jpxgmn.cc"
file_name = url.split('/')[-2] + "-" + url.split('/')[-1].split('.')[0]
pages_data = getPagesDataList(url,headers,base_url,img_source)
img_urls = getImgUrlsList(pages_data)
num = 0
for img_url in img_urls:
num += 1
print('Downloading No.{} Pic'.format(num))
#上锁
thread_lock.acquire()
t = threading.Thread(target=downloadImg, args=(img_url,headers,num,base_url,file_name))
t.start()
print("-------------------------------------DONE!!")
end_t = time.time()
cost = end_t - start_t
print("爬取用时:{} seconds".format(cost))
main()
发表评论
您尚未登录,登录后方可评论~~登陆 or 注册