文章目录
1. 基本概念
(1)进程:一个正在运行的应用程序就是一个进程。每个进程均运行在其专用且受保护的内存空间中。(进程结束,内存会自动释放)。进程相当于工厂。
(2)线程:是进程执行任务的基本单元。如果一个进程需要执行任务,必须要线程。所有的任务都必须在线程中执行。线程相当于工人。
(3)线程的串行:如果在一个线程中执行多个任务,多个任务是串行执行的(一个一个按顺序执行的)。
(4)多线程:一个进程中默认只有一个线程。多线程就是一个进程中同时拥有多个线程。
特点:多个线程执行多个任务,是并行(同时)执行的。
多线程可以提高程序的执行效率,但是并不是线程数越多越好。一般情况下:手机应用程序一般3-5个、电脑程序几十个到两三百个
(5)多线程原理:多线程的存在可以让一个程序同一时间执行不同的任务。一个cpu同一时间只能执行一个线程。多线程实质上是CPU在多个线程之间来回调度(切换),当调度速度足够快的时候,就会造成多线程同时执行的假象。类似这样:
2. 多线程基本用法
python程序默认只有一个线程(这个线程叫主线程),除了主线程以外需要别的线程(子线程),必须用代码来创建线程对象(Thread的对象)。默认情况下,程序中的代码都是在主线程中执行。
(1)导入线程类
RIGHT Example:
from threading import Thread
from datetime import datetime
from time import sleep
def download(m_url):
print(f'---------{m_url}开始下载:{datetime.now()}--------------')
sleep(2)
print(f'---------{m_url}下载结束:{datetime.now()}--------------')
(2)线程的串行
RIGHT Example:
download('肖申克的救赎')
download('许三观卖血记')
download('沉默的羔羊')
(3)多线程的并行
RIGHT Example:
# (1)创建线程对象,并且给子线程添加任务
"""
Thread(target=函数, args=元组)
函数:需要在子线程中执行的任务
元组:元组中的元素就是target对应的函数在调用的时候需要的参数
"""
t1 = Thread(target=download, args=('肖申克的救赎',))
t2 = Thread(target=download, args=('许三观卖血记',))
t3 = Thread(target=download, args=('沉默的羔羊',))
# (2)启动线程:线程对象.start()
t1.start()
t2.start()
t3.start()
(4)批量创建子线程
RIGHT Example:
names = [f'电影{x}' for x in range(1, 11)]
for x in names:
t = Thread(target=download, args=(x,))
t.start()
3. 实际案例
*APPLICATION 使用多线程获取数据:
import requests
import csv
from datetime import datetime
from tqdm import tqdm
from lxml import etree
from threading import Thread
headers = {
'User-Agent': r'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36'
}
def get_resp(url):
resp = requests.get(url=url, headers=headers)
return resp
def get_all(text, xpath):
root = etree.HTML(text)
return root.xpath(xpath)
def get_one(single, xpath):
target = single.xpath(xpath)
return target
def main(i):
print(f'开始下载{datetime.now()}')
url = f'https://movie.douban.com/top250?start={i}&filter='
resp0 = get_resp(url)
html_list = get_all(resp0.text, '//div[@class="article"]/ol[@class="grid_view"]/li')
for single in tqdm(html_list):
name = get_one(single, './div/div[@class="info"]/div/a/span[1]/text()')
writer.writerow(name)
print(f'结束下载{datetime.now()}')
if __name__ == '__main__':
writer = csv.writer(open(f'files/top250.csv', 'w', encoding='utf-8', newline=''))
for i in range(0, 226, 25):
t = Thread(target=main, args=(i,))
t.start()
4. 阻塞
多线程编程的时候,如果一个线程中需要等待另外的子线程中的任务结束才执行某个操作,就可以在需要等待的位置用子线程对象调用join方法
注意:如果只需要等待1个子线程任务完成,就用一个子线程对象调用join,如果需要等多个子线程任务完成,就用多个线程对象调用join
RIGHT Example:
# 案例1:等待所有的子线程任务都结束,打印全部下载完成
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
print('全部下载完成')
# 案例2:第1个电影下完才同时下第2个和第3个电影
t1.start()
t1.join()
t2.start()
t3.start()
t2.join()
t3.join()
print('全部下载完成')
# 案例3:同时下载10个电影,要求10个电影都下载结束的时候打印'全部下载完成'
all = []
for i in range(1, 11):
t = Thread(target=download, args=(f'电影{i}',))
all.append(t)
t.start()
for i in all:
i.join()
print('下载完成')
5. 多线程通信
同一个进程中的多个线程之间可以直接通信
通信方法:使用全局变量
注意:如果子线程调用的函数有返回值,那么这个函数的返回值是没有办法获取的
WRONG Example:
def f(n: int):
s = 1
for x in range(1, n + 1):
s *= x
return s
if __name__ == '__main__':
t1 = Thread(target=f, args=(10,))
t2 = Thread(target=f, args=(34,))
print(t1.start()) # None
print(t2.start()) # None
RIGHT Example:
def f(n: int):
s = 1
for x in range(1, n + 1):
s *= x
result.append(s)
if __name__ == '__main__':
result = []
t1 = Thread(target=f, args=(10,))
t2 = Thread(target=f, args=(34,))
t1.start()
t2.start()
t1.join()
t2.join()
print(result) # [3628800, 295232799039604140847618609643520000000]
6. 队列的使用
队列是容器,可以同时保存多个数据,数据保存的特点:先进先出
创建队列:Queue()
添加数据:队列对象.put(数据)
获取数据:队列对象.get()
get获取的是当前队列中最先添加的数据,获取一个数据就会少一个
当队列为空时,get操作不会报错,并且会阻塞线程,直到队列中有数据为止
6.1 基本用法
RIGHT Example:
def download(name):
print(f'----------{name}开始下载:{datetime.now()}')
time.sleep(random.randint(2, 7))
print(f'----------{name}下载结束:{datetime.now()}')
q1.put(f'{name}数据')
if __name__ == '__main__':
q1 = Queue()
for x in range(1, 6):
t = Thread(target=download, args=(f'电影{x}',))
t.start()
for _ in range(5):
print(f'保存:{q1.get()}')
6.2 添加结束标志
RIGHT Example:
from queue import Queue
from threading import Thread
import time, random
from datetime import datetime
def download(name):
print(f'----------{name}开始下载:{datetime.now()}')
time.sleep(random.randint(5, 10))
print(f'----------{name}下载结束:{datetime.now()}')
q1.put(f'{name}数据')
def save_data():
while True:
data = q1.get()
if data == 'end':
print('结束了')
break
else:
print(f'开始保存{data}')
time.sleep(random.randint(1, 5))
print(f'保存{data}成功')
if __name__ == '__main__':
q1 = Queue()
ts = []
for x in range(1, random.randint(3, 5)):
t = Thread(target=download, args=(f'电影{x}',))
t.start()
ts.append(t)
# 从队列中获取数据,处理数据的操作必须在子线程中执行
t1 = Thread(target=save_data)
t1.start()
# 在所有下载数据的线程都结束以后,往队列中添加结束标志
for t in ts:
t.join()
q1.put('end')
7. 线程池的使用
7.1 线程池相关概念
线程池:线程池用来保存线程并且给线程分配任务
工作原理:提前创建指定个数个线程,并且添加所有需要在子线程中执行的任务,然后线程池会自动给线程池中的线程分配提前准备好的任务
7.2 创建线程池
(1)创建线程池对象:ThreadPoolExecutor(线程数)
RIGHT Example:
pool = ThreadPoolExecutor(3)
(2)添加任务
a. 一次添加一个任务:线程池对象.submit(函数, 参数1, 参数2, …)
RIGHT Example:
pool.submit(download, '电影1')
pool.submit(download, '电影2')
b. 一次添加多个任务:线程池对象.map(函数, 列表)
注意:这儿的函数只能是只有一个参数的函数
RIGHT Example:
pool.map(download, ['电影3', '电影4', '电影5', '电影6', '电影7'])
pool.map(download, [f'电影{x}' for x in range(8, 21)])
c. 关闭和等待
关闭:如果关闭线程池,那么线程池就不能再添加任务
等待:等线程池中所有的任务都完成
RIGHT Example:
pool.shutdown()
7.3 案例:如果失败了就再添加回线程池
RIGHT Example:
import time, random
from queue import Queue
from concurrent.futures import ThreadPoolExecutor
def print_num():
try:
if random.randint(1, 3) == 1:
raise KeyError
print(1)
except:
print('失败了!')
global future
future = pool.submit(print_num)
if __name__ == '__main__':
count_ = 0
q = Queue()
pool = ThreadPoolExecutor(4)
for x in range(20):
future = pool.submit(print_num)
while not future.done():
time.sleep(1)