什么是线程
python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
1.线程是cpu执行的基本单元
2.线程之间的执行是无序的
3.同一进程下的线程的资源是共享的 (线程锁,互斥锁)
4.线程可以实现多任务,多用来处理I/O密集型任务
使用threading模块
单线程执行
import time
def saySorry():
for i in range(5):
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
def do(): for i in range(5):
print("亲爱的,我错了,我给你按摩")
time.sleep(1)
if __name__ == "__main__":
saySorry()
saydo()
多线程执行
import threading
import time
def saySorry():
for i in range(5):
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
def do():
for i in range(5):
print("亲爱的,我错了,我给你按摩")
time.sleep(1)
if __name__ == "__main__":
td1 = threading.Thread(target=saySorry)
td1.start() #启动线程,即让线程开始执行
td2 = threading.Thread(target=saySorry)
td2.start() #启动线程,即让线程开始执行
threading.Thread参数介绍
-
target:线程执行的函数
-
name:线程名称
-
args:执行函数中需要传递的参数,元组类型
-
kwargs:传参数(字典)
另外:注意daemon参数 -
如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,如果该子线程还在运行,则主线程会等待它完成后再退出;
-
如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论是否运行完成。
-
属性daemon的值默认为False,如果需要修改,必须在调用start()方法启动线程之前进行设置
说明
- 可以明显看出使用了多线程并发的操作,花费时间要短很多
- 当调用start()时,才会真正的创建线程,并且开始执行
方法名 | 作用 |
---|---|
start()方法 | 开启线程 |
join()方法 | 线程阻塞 |
daemon = False | 后台线程,主线程结束不影响子线程运行 |
daemon = True | 前台线程,主线程结束子线程随之结束 |
互斥锁
- GIL:由于python的CPython解释器的原因,存在一个GIL全局解释器锁用来保证同一时刻只有一个线程在执行,类似于单核处理,所有说多线程并不能充分的利用cpu资源
- 当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
- 互斥锁为资源引入一个状态:锁定/非锁定
- 某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
- hreading模块中定义了Lock类,可以方便的处理锁定:
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release(
注意:
- 如果这个锁之前是没有上锁的,那么acquire不会堵塞
- 如果在调用acquire对这个锁上锁之前 它已经被其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止
使用互斥锁完成2个线程对同一个全局变量各加100万次的操作
import threading
import time
g_num = 0
def test1(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test1---g_num=%d"%g_num)
def test2(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print("---test2---g_num=%d"%g_num)
# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()
创建2个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
p1.join() p2.join()
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
运行结果:
2个线程对同一个全局变量操作之后的最终结果是:2000000
可以看到最后的结果,加入互斥锁后,其结果与预期相符。
上锁解锁过程
- 当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
- 每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
- 线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
总结
锁的好处
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
线程池
导入模块包
from concurrent.futures import ThreadPoolExecutor
创建线程池,并往线程池中添加任务
#创建一个线程池
pool = ThreadPoolExecutor(10)
#如何提交任务给线程池呢?
# 往线程池中添加任务
for pagenum in range(50):
#submit: 表示将我们需要执行的任务给这个线程池,
handler = pool.submit(get_page_data,pagenum)
#给线程池设置任务之后,可以设置一个回调函数, #作用是:当我们某个任务执行完毕之后,就会回调你设置的回调函数
handler.add_done_callback(done)
pool.shutdown(wait=True)
案例
from concurrent.futures import ThreadPoolExecutor
import requests
from lxml.html import etree
import requests
class CollegateRank(object):
def get_page_data(self,url):
response = self.send_request(url=url)
if response:
# print(response)
with open('page.html','w',encoding='gbk') as file:
file.write(response)
self.parse_page_data(response)
def parse_page_data(self,response):
#使用xpath解析数据
etree_xpath = etree.HTML(response)
ranks = etree_xpath.xpath('//div[@class="scores_List"]/dl')
# print(ranks)
pool = ThreadPoolExecutor(10)
for dl in ranks:
school_info = {}
school_info['url'] = self.extract_first(dl.xpath('./dt/a[1]/@href'))
school_info['icon'] = self.extract_first(dl.xpath('./dt/a[1]/img/@src'))
school_info['name'] = self.extract_first(dl.xpath('./dt/strong/a/text()'))
school_info['adress'] = self.extract_first(dl.xpath('./dd/ul/li[1]/text()'))
school_info['tese'] = '、'.join(dl.xpath('./dd/ul/li[2]/span/text()'))
school_info['type'] = self.extract_first(dl.xpath('./dd/ul/li[3]/text()'))
school_info['belong'] = self.extract_first(dl.xpath('./dd/ul/li[4]/text()'))
school_info['level'] = self.extract_first(dl.xpath('./dd/ul/li[5]/text()'))
school_info['weburl'] = self.extract_first(dl.xpath('./dd/ul/li[6]/text()'))
print(school_info['url'],school_info)
result = pool.submit(self.send_request,school_info['url'])
result.add_done_callback(self.parse_school_detail)
# pool.shutdown()
# 线程执行完毕的回调方法
def parse_school_detail(self,future):
text = future.result()
print('解析数据',len(text))
def extract_first(self,data=None,defalut=None):
if len(data) > 0:
return data[0]
return defalut
def send_request(self, url, headers=None):
headers = headers if headers else {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'}
response = requests.get(url=url,headers=headers)
if response.status_code == 200:
return response.text
if __name__ == '__main__':
url = 'http://college.gaokao.com/schlist/'
obj = CollegateRank()
obj.get_page_data(url)