cpu:内核
进程:
优点:提高效率,利用cpu多核优势
开启个数:理论上是cpu内核的1-2倍
描述:一段程序或者脚本的执行,cpu资源分配的最小单位
缺点:资源消耗非常大,进程过多,cpu切换进程执行也消耗资源,资源共享困难
使用场景:cpu密集型应用程序(计算密集型)
线程:
优点:提高效率,资源共享
开启个数:跟计算硬件有关系,跟应用场景有关系,一般高于可开启进程守数
描述:进程下可以开启多个线程,cpu调度的最小单位
缺点:开启个数也不是无限的,如果开启过多,造成进程瘫痪
使用场景:IO(input(response),ouput(request),)密集型应用程序,爬虫成是网络IO,长延时操作
跨平台的进程创建模块(multiprocessing)
multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束
步骤:
1、从multiprocessing模块导入Process类
2、进程的操作(1)创建和启动创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。创建格式:p=Process(target=函数名)启动格式:进程对象名.start()
参数列表:Process([group [, target [, name [, args [, kwargs]]]]])
target:表示这个进程实例所调用对象;
args:表示调用对象的位置参数元组;
kwargs:表示调用对象的关键字参数字典;
name:为当前进程实例的别名;
group:大多数情况下用不到;
# 在windows下 一定要加 __name__ == '__main__
#单进程代码:
#导入进程模块 import multiprocessing import os # 自定义进程 def foo(): print('开启进程:%s'%os.getpid()) if __name__ == '__main__': print('Process:%s' % os.getpid()) #创建一个进程,则先需要定义一个函数 p = multiprocessing.Process(target=foo) p.start() #结果 Process:6484 开启进程:6872
#参数的使用
#导入进程模块 import multiprocessing import os # 自定义进程 def foo(filename,type,**kwargs): print('开启进程:%s'%os.getpid()) if kwargs['device'] == 'iphonex': path = 'www.jd.com/iphonex' print('下载:%s.%s.%s'%(path,filename,type)) print(os.name) if __name__ == '__main__': print('Process:%s' % os.getpid()) #创建一个进程,则先需要定义一个函数 p = multiprocessing.Process(target=foo,name='下载', args=('log','png'),kwargs={'device':'iphonex'}) #别名 print(p.name) p.start() #结果 Process:7768 下载 开启进程:9272 下载:www.jd.com/iphonex.log.png nt
#多进程
import multiprocessing # 导入多进程模块 # 定义进程函数 def foo(num): print('hello %d号进程' % num) # 在windows下如果创建多进程则需要在以下条件成立 if __name__ == '__main__': # 创建一个进程,则需要先定义一个函数 # target : 进程函数对象 # args : 进程函数的参数 for i in range(10): p = multiprocessing.Process(target=foo,args=(i,)) p.start() #结果 hello 0号进程 hello 4号进程 hello 3号进程 hello 8号进程 hello 7号进程 hello 1号进程 hello 5号进程 hello 9号进程 hello 6号进程 hello 2号进程
多进程资源共享
import multiprocessing # 导入多进程模块 from multiprocessing import Manager # 定义进程函数 def foo(num,ls): ls.append(num) print('hello %d号进程' % num) # 在windows下如果创建多进程则需要在以下条件成立 if __name__ == '__main__': # 创建一个进程,则需要先定义一个函数 # target : 进程函数对象 # args : 进程函数的参数 m = Manager() # 初始化一个manager对象 ls = m.list() # 创建共享数据类型 process_list = [] for i in range(10): p = multiprocessing.Process(target=foo,args=(i,ls)) p.start() # 启动一个进程 process_list.append(p) # 进程执行中会阻塞主进程 for p in process_list: p.join() # 需要等待10个进程全部执行完毕再继续执行主进程 print('进程执行完毕,执行我') print(ls) #结果 hello 7号进程 hello 3号进程 hello 2号进程 hello 8号进程 hello 6号进程 hello 0号进程 hello 4号进程 hello 9号进程 hello 5号进程 hello 1号进程 进程执行完毕,执行我 [7, 3, 2, 8, 6, 0, 4, 9, 5, 1]
进程池的使用
当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来执行,请看下面的实例:# 1-50-1号进程 51-100-2号进程 import time # 进程池 from multiprocessing import Pool def foo(num): time.sleep(1) print(num) if __name__ == '__main__': # 创建一个进程池 pool = Pool(16) for i in range(100): # 创建一个进程 pool.apply_async(func=foo,args=(i,)) # 进程池中所有进程执行完毕,继续执行 pool.close() pool.join()
腾讯招聘多进程爬虫
import multiprocessing from multiprocessing import Manager from queue import Queue import time import requests # 任务函数,把要执行的任务全部封装到这个函数中 def foo(task_q): # 如果不为空 while not task_q.empty(): #取出对列里的内容 fullurl = task_q.get() #请求头 headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36' } #请求页面 response = requests.get(fullurl,headers=headers) print(response.url) if __name__ == '__main__': print(time.ctime()) # 生成任务队列 manager = Manager() task_q = manager.Queue() base_url = 'https://hr.tencent.com/position.php?start=%d' for i in range(0,3900 + 1,10): # 创建进程,计划了一个任务 fullurl = base_url % i #加入对列 task_q.put(fullurl) p_list = [] # 创建的进程数,并发数16 for i in range(16): # 创建进程,计划了一个任务 p = multiprocessing.Process(target=foo,args=(task_q,)) # 把计划任务执行起来 p.start() # p.join() p_list.append(p) for p in p_list: #等待进程实例执行结束 p.join() print(time.ctime())
多线程基本使用
import threading # 导入线程模块 import time # 线程函数 def foo(num): time.sleep(2) print(num) # 创建一个线程 t1 = threading.Thread(target=foo,args=(1,)) t2 = threading.Thread(target=foo,args=(2,)) # 启动线程 t1.start() t2.start() - import threading import time def foo(i): print('%d号线程启动' % i) time.sleep(2) print('%d号线程结束' % i) thread_list = [] for i in range(1,10): t = threading.Thread(target=foo,args=(i,)) t.start() print(t.is_alive()) thread_list.append(t) # 等待所有线程执行完毕 for t in thread_list: t.join() # 查看所有线程状态 for t in thread_list: print(t.is_alive())
线程类的使用
import threading # 导入线程模块 import time # 线程函数 def foo(num): time.sleep(2) print(num) # 创建一个线程 t1 = threading.Thread(target=foo,args=(1,)) t2 = threading.Thread(target=foo,args=(2,)) # 启动线程 t1.start() t2.start() - import threading import time def foo(i): print('%d号线程启动' % i) time.sleep(2) print('%d号线程结束' % i) thread_list = [] for i in range(1,10): t = threading.Thread(target=foo,args=(i,)) t.start() print(t.is_alive()) thread_list.append(t) # 等待所有线程执行完毕 for t in thread_list: t.join() # 查看所有线程状态 for t in thread_list: print(t.is_alive())
互斥锁
from threading import Thread,Lock g_num =0 def w1(): global g_num for i in range(1000000): #开锁 mut = mutex.acquire(True) # 判断是否有线程在用 if mut: g_num += 1 #解锁 mutex.release() print(g_num) def w2(): global g_num for i in range(1000000): #开锁 mut = mutex.acquire(True) #判断是否有线程在用 if mut: g_num += 1 #解锁 mutex.release() print(g_num) if __name__ == '__main__': #创建锁 mutex = Lock() t1 = Thread(target=w1) t1.start() t2 = Thread(target=w2) t2.start()
上锁解锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态
总结
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整;
地执行锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子:有两个人分别做“西兰花”和“红烧肉”,每个人都需要“铲子”和“锅”之后才能炒菜。
代码
import threading class XiLanHua(threading.Thread): def run(self): if mutex_A.acquire(): print(self.name,'拿到了铲子') if mutex_B.acquire(): print(self.name,'拿到了锅') mutex_B.release() mutex_A.release() print(self.name,'使用完了') class HongShaoRui(threading.Thread): def run(self): if mutex_B.acquire(): print(self.name,'拿到了锅') #判断锁的状态 if mutex_A.acquire(): print(self.name, '拿到了铲子') mutex_A.release() mutex_B.release() print(self.name, '使用完了') if __name__ == '__main__': #创建两把锁 mutex_A = threading.Lock() mutex_B = threading.Lock() cook1 = XiLanHua() cook2 = HongShaoRui() cook1.start() cook2.start()
避免死锁
程序设计时要尽量避免(银行家算法)
添加超时时间
线程池
from queue import Queue import threading from multiprocessing import Pool import time class ThreadPool(object): def __init__(self,max_num): # 设置线程队列 self.thread_q = Queue(max_num) # 往队列里添加线程类对象 for i in range(max_num): self.thread_q.put(threading.Thread) # 获取一个线程 def get_thread(self): return self.thread_q.get() # 添加一个线程 def add_thread(self): self.thread_q.put(threading.Thread) # 使用线程池 def foo(i,pool): print(i) time.sleep(1) pool.add_thread() if __name__ == '__main__': # 创建线程池 pool = ThreadPool(8) # 循环创建任务 for i in range(1,11): # 从线程池拿取一个线程 Thread = pool.get_thread() # 创建线程任务 t = Thread(target=foo,args=(i,pool)) # 启动任务 t.start()
线程类的爬虫
import threading from queue import Queue import requests from urllib import request from lxml import etree import time import random import json concurrent = 3 conparse = 3 # 解析类 class Parse(threading.Thread): def __init__(self,number,data_q,thread_list,f): super(Parse, self).__init__() self.number = number self.data_q = data_q self.thread_list = thread_list self.f = f self.is_parse = True def run(self): while True: for t in self.thread_list: #如果为死线程就退出 if t.is_alive(): break else: if self.data_q.qsize() == 0: self.is_parse = False if self.is_parse: try: html = self.data_q.get(timeout=3) self.parse(html) except Exception as e: pass else: break print('%d号解析线程结束' % self.number) def parse(self,html): html = etree.HTML(html) # 获取段子div duanzi_div = html.xpath('//div[@id="content-left"]/div') # 循环所有段子,获取详细内容 for duanzi in duanzi_div: nick = duanzi.xpath('.//h2/text()')[0].strip() age = duanzi.xpath('.//div[@class="articleGender manIcon"]/text() | .//div[@class="articleGender womenIcon"]/text()') if age: age = age[0] else: age = 0 gender_cls = duanzi.xpath('.//div[@class="articleGender manIcon"]/@class | .//div[@class="articleGender womenIcon"]/@class') if gender_cls: if 'man' in gender_cls[0]: gender = '男' elif 'women' in gender_cls[0]: gender = '女' else: gender = '中' content = duanzi.xpath('.//div[@class="content"]/span/text()') content = ''.join(content).strip() # 星星和评论 starts = duanzi.xpath('.//span[@class="stats-vote"]/i/text()')[0] comments = duanzi.xpath('.//span[@class="stats-comments"]//i/text()')[0] data = { 'nick' : nick, 'age' : age, 'gender' : gender, 'content' : content, 'starts' : starts, 'comments' : comments } # 获取图片并下载 duanzi_img = duanzi.xpath('//div[@class="thumb"]//img/@src') if duanzi_img: # 段子有图片 duanzi_img = duanzi_img[0] duanzi_img = 'https:' + duanzi_img fname = duanzi_img.split('/')[-1] request.urlretrieve(duanzi_img,'./image/' + fname) else: duanzi_img = '' # 存储 self.f.write(json.dumps(data,ensure_ascii=False) + '\n') # 采集类 class Crawl(threading.Thread): proxies = [ {"host": "61.155.164.111", "port": "3128"}, {"host": "122.72.18.34", "port": "80"} ] def __init__(self,number,task_q,data_q): super(Crawl, self).__init__() self.number = number self.task_q = task_q self.data_q = data_q print(self.number) # 如果线程调用start方法就调用run方法 def run(self): while not self.task_q.empty(): fullurl = task_q.get() print('%d号线程采集%s' % (self.number,fullurl)) # 随机获取一个代理 ran_proxy = random.choice(self.proxies) proxy = { 'http' : 'http://' + ran_proxy['host'] + ':' + ran_proxy['port'], 'https' : 'http://' + ran_proxy['host'] + ':' + ran_proxy['port'] } response = requests.get(url=fullurl) # print(response.status_code) # 获取响应状态 if 200 <= response.status_code <= 300: html = response.text self.data_q.put(html) else: print('响应内容错误') # time.sleep(random.random() * 3) if __name__ == '__main__': # 生成请求队列 task_q = Queue() # 数据队列 data_q = Queue() # 生成文件对象 f = open('duanzi.json','w',encoding='utf-8') base_url = 'https://www.qiushibaike.com/8hr/page/%d/' for page in range(1, 13 + 1): fullurl = base_url % page task_q.put(fullurl) thread_list = [] for i in range(concurrent): t = Crawl(i + 1,task_q,data_q) t.start() thread_list.append(t) # 启动解析线程 parse_thread = [] for i in range(conparse): t = Parse(i + 1 ,data_q,thread_list,f) t.start() parse_thread.append(t) # 等待所有采集线程运行完毕 for t in thread_list: t.join() # 等待所有解析线程运行完毕 for t in parse_thread: t.join() f.close()
ThreadLocal
在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。import threading local_school = threading.local() class Student(): def __init__(self,name): self.name = name def process_student(name): std = Student(name) local_school.student = std do_task_1() do_task_2() def do_task_1(): std = local_school.student print('do_task_1',std.name) def do_task_2(): std = local_school.student print('do_task_2',std.name) if __name__ == '__main__': t1 = threading.Thread(target=process_student,args=('小明',)) t2 = threading.Thread(target=process_student, args=('小hong',)) t2.start() t1.start() #结果
do_task_1 小hong
do_task_2 小hong
do_task_1 小明
do_task_2 小明
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
协程
首先我们得知道协程是啥?协程其实可以认为是比线程更小的执行单元。 为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机, 我们可以把一个协程 切换到另一个协程。 只要这个过程中保存或恢复 CPU上下文,那么程序还是可以运行的。通俗的理解:在一个线程中的某个函数,可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外个函数中执,注意不是通过调用函数的方式做到的,并且切换的次数以及什么时候再切换到原来的函数都由开发者确定。
在IO密集型的程序中由于IO操作远远慢于CPU的操作,所以往往需要CPU去 等IO操作。 同步IO下系统需要切换线程,让操作系统可以在IO过程中执行其他的东西。这样虽然代码是符合人类的思维习惯但是由于大量的线程切换带 来了大量的性能的浪费,尤其是IO密集型的程序。所以人们发明了异步IO。就是当数据到达的时候触发我的回调。来减少线程 切换带来性能损失。 但是这样的坏处也是很大的,主要的坏处就是操作被 “分片” 了,代码写的不是 “一气呵成” 这种。而是每次来段数据就要判断数据够不够处理哇,够处理就处理吧,不够处理就在等等吧。这样代码的可读性很低,其实也不符合人类的习惯。但是协程可以很好解决这个问题。例如把一个IO操作 写成一个协程。当触发 IO操作的时候就 动让出CPU给其他协程。要知道协程的切换很轻的。 协程通过这种对异步IO的封装 既保留了性能也保证了代码的容易编写和可读性。
from gevent import monkey monkey.patch_all() import gevent import requests from queue import Queue import greenlet import time headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36' } def getPage(task_q): # print('启动') # print(gevent.getcurrent()) # response = requests.get(task_q,headers=headers) # print(response.status_code) while not task_q.empty(): fullurl = task_q.get() response = requests.get(fullurl,headers=headers) # gevent.sleep(1) print(response.url) print(time.ctime()) base_url = 'https://hr.tencent.com/position.php?start=%d' # 创建任务 task_q = Queue() for i in range(0, 3901, 10): fullurl = base_url % i task_q.put(fullurl) g_list = [] for i in range(35): g = gevent.spawn(getPage,task_q) g_list.append(g) gevent.joinall(g_list) print('结束运行') print(time.ctime())