创建线程的三种方式
1. 创建Thread对象 ---> target / args ---> start()
2. 继承Thread类 ---> 重写run()方法 ---> start()
3. 使用线程池(最佳选择)---> 减少频繁创建和释放线程造成的系统开销
线程间的通信非常简单,可以通过共享内存来实现
进程间的通信比较麻烦,因为进程间的内存是相互隔离的,需要使用管道、套接字等方式来通信
-
多进程 —> 计算密集型任务 —> 科学计算 / 音视频编解码 / 加密算法
-
多线程—>异步I/O —> I/O密集型任务 —> Web应用 / 爬虫 / 数据读写
-
什么时候会使用多线程???
- 任务耗费的时间比较长(会造成其他任务的阻塞)
- 任务之间没有偏序关系(一个任务不用等另一个任务执行完毕) 多线程程序的好处?
- 提升执行效率
- 改善用户体验
-
全局解释器锁(GIL) Global Interpreter Lock
-
CPython —> 通过C实现的Python 解释器,申请和释放内存的操作并非线程安全
所以需要GIL来保证只有一个线程能够执行关键操作,导致无法利用CPU的多核特性即便如此,多线程还是可以提升程序的执行效率(对于I/O密集型任务),也可以改善用户体验
因此,对于计算密集型任务,可以使用多进程的方式突破GIL的限制
-
守护线程
守护线程–不值得保留的线程
守护线程 — 主线程如果结束了,守护线程即便任务没有执行完也要跟着结束
import time
from threading import Thread
from multiprocessing import Process,freeze_support
def output(content):
while True:
print(content,end='')
time.sleep(0.01)
if __name__ == '__main__':
Thread(target=output,args=('Ping',),daemon=True).start()
Thread(target=output,args=('Pong',),daemon=True).start()
time.sleep(2)
# daemon :主线程结束,它也结束了。不然就死循环。
银行账户类
定义一个类:一个银行账户(余额,存钱,取钱)
创建一个银行对象,启动100个线程,每个线程转入1块钱
转账完成后,查看银行账户余额。存钱和取钱的受理是需要耗费时间的
当多个线程竞争一个资源的时候,如果资源本身并不是线程安全的,那么就会出问题
如果代码中需要对数据进行保护(保护临界资源),可以使用锁机制
如果对象实现了上下文管理器协议,就可以用到wuth语法中
上下文管理器就是两个魔术方法:
- __enter__ 进入上下文的时候自动调用
- __exit__ 离开上下文的时候自动调用
"""
from threading import Thread, RLock
import time
class Account:
"""银行账户"""
def __init__(self):
self.balance = 0
# 重入锁(一般情况都使用)
self.lock = RLock()
def deposit(self,money):
"""存钱"""
# 如果多个线程同时执行到这个代码,那么后面的线程对余额的更新会覆盖掉之前的更新
# 这个现象在数据库层面也有可能发生,称之为丢失更新
# (第一类丢失更新,第二类丢失更新)
# 通过上下文语法自动获得锁和释放锁
with self.lock:
new_balance = self.balance + money
time.sleep(0.01)
self.balance = new_balance
# 获得锁
# self.lock.acquire()
# try:
# new_balance = self.balance +money
# time.sleep(0.01)
# self.balance = new_balance
# finally: # 总是执行
# # 释放锁
# self.lock.release()
def withdraw(self,money):
"""取钱"""
with self.lock:
if money <= self.balance:
new_balance = self.balance - money
time.sleep(0.01)
self.balance = new_balance
return True
return False
if __name__ == '__main__':
account = Account()
account.balance = 100
add_money_threads = []
for _ in range(100):
t = Thread(target=account.withdraw,args=(1,))
add_money_threads.append(t)
t.start()
for t in add_money_threads:
t.join()
print(account.balance)
池化的思想和重入锁
"""
创建和释放一个线程都会产生比较大的系统开销,所以如果程序要长时间使用多个线程
最好的方式是通过线程池来使用线程。
线程池会提前创建好若干个线程,在使用过程中不需要创建和释放,程序和线程池之间是借还关系
池化的思想:空间换时间的技术
"""
import time
from concurrent.futures.thread import ThreadPoolExecutor
from threading import RLock
class Account:
"""银行账户"""
def __init__(self):
self.balance = 0
# 重入锁(一般情况都使用)
self.lock = RLock()
def deposit(self,money):
"""存钱"""
# 如果多个线程同时执行到这个代码,那么后面的线程对余额的更新会覆盖掉之前的更新
# 这个现象在数据库层面也有可能发生,称之为丢失更新
# (第一类丢失更新,第二类丢失更新)
# 通过上下文语法自动获得锁和释放锁
with self.lock:
new_balance = self.balance + money
time.sleep(0.01)
self.balance = new_balance
def withdraw(self,money):
"""取钱"""
with self.lock:
if money <= self.balance:
new_balance = self.balance - money
time.sleep(0.01)
self.balance = new_balance
return True
return False
if __name__ == '__main__':
account = Account()
account.balance = 100
# 创建线程池 (多线程)
with ThreadPoolExecutor(max_workers=16) as pool:
for _ in range(100):
# 如果传入的函数有多个参数,可以直接依次传入,不需要用元组包装
# print('准备存入')
pool.submit(account.deposit,1)
# print('准备取钱')
pool.submit(account.withdraw,1)
# pool.shutdown() # 线程池中的任务完成后才结束
print(account.balance)
I/O操作模式
-
同步:调用一个函数,必须等函数返回以后代码才能往回执行
同步阻塞:
同步非阻塞: -
异步:调用一个函数,不必等函数返回以后代码才能往回执行
异步阻塞: — 多路I/O复用
异步非阻塞: — 回调式I/O —并发 -
python做并发(并行:必须在同一时间开始)编程的三种方式:
多线程:
多进程:
异步I/O: --> 协作式并发(通过多个子程序相互协作,提高CPU利用率、 -
迭代器:实现了迭代器协议的对象
- 迭代器协议对应两个魔术方法:
iter :获取迭代器对象
next :获取到下一个值
- 迭代器协议对应两个魔术方法:
斐波那契数列迭代器
# 迭代器
class FibIter:
def __init__(self, num):
self.a, self.b = 0, 1
self.num = num
self.counter = 0
def __iter__(self):
return self
def __next__(self):
if self.counter < self.num:
self.a, self.b = self.b, self.a+self.b
self.counter += 1
return self.a
raise StopIteration() # 抛异常来停止迭代
if __name__ == '__main__':
fiter = FibIter(20)
for value in fiter:
print(value)
生成器
"""
生成器:迭代器语法升级版本
"""
# 生成器
def fib(num):
a, b = 0, 1
for _ in range(num):
a, b = b, a + b
yield a
if __name__ == '__main__':
fiter = fib(20)
for value in fiter:
print(value)
协程
"""
协程(coroutine):相互协作的子程序
生成器经过预激活操作就可以升级为一个协程
yield ---> 产出、屈服,让步
"""
# yield和 send 协作
def callc_avg():
total, counter = 0, 0
avg_value = 0
while True:
curr_num = yield avg_value # 生成器遇到yield又交回控制权
total += curr_num
counter += 1
avg_value = total / counter
def main():
gen = callc_avg()
# 将生成器预激活为 协程 --- 必须激活
# next(gen)
gen.send(None) # 激活必须是None # send 数据发给 yield
for _ in range(3):
num = int(input())
print(gen.send(num)) # send 数据发给 yield
if __name__ == '__main__':
main()
使用异步函数来创建协程对象
import asyncio
"""
使用异步函数来创建协程对象
async --- asynchronous
sync --- synchronous
"""
# 定义异步函数
async def say_hello():
print('hello')
# 调用异步函数不是执行函数体,而是得到一个协程对象
co = say_hello()
print(co) # <coroutine object say_hello at 0x00000246FFADFEC8>
# 协程对象需要跟其他对象协作
# 协程对象需要挂载到一个事件循环上才能运转起来
loop = asyncio.get_event_loop()
loop.run_until_complete(co) # hello
loop.close()
同步阻塞式调用
"""
同步阻塞式调用
"""
import time
def display(num):
print(num)
time.sleep(1)
def main():
for num in range(1, 10):
display(num)
if __name__ == '__main__':
main()
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
异步非阻塞式调用
import asyncio
"""
同步 --- 有序 异步 --- 无序
异步非阻塞式调用 --- > 协作式i并发 ---> 多个子程序相互协作提高CPU利用率
"""
import time
async def display(num):
print(num)
await asyncio.sleep(1) # await == yield 主动放弃对CPU的占用,让给其他的
def main():
cos = [display(num) for num in range(1, 10)]
# print(cos) # 列表里是9个协程对象
loop = asyncio.get_event_loop()
# asyncio.wait(cos) # 列表转协程
loop.run_until_complete(asyncio.wait(cos)) # hello
loop.close()
if __name__ == '__main__':
main()
# 3
# 5
# 6
# 1
# 4
# 7
# 8
# 2
# 9
质数迭代器
"""
作业:写一个迭代出从2开始的若干个质数的迭代器
"""
class Prime:
def __init__(self, num):
self.a = 1
self.num = num
self.counter = 0
def __iter__(self):
return self
def __next__(self):
if self.counter < self.num:
while True:
self.a += 1
flag = 1
for i in range(2, int(self.a ** 0.5) + 1):
if (self.a % i == 0):
flag = 0
break
if flag:
# print(self.a, '是素数')
self.counter += 1
return self.a
raise StopIteration() # 抛异常来停止迭代
if __name__ == '__main__':
p1 = Prime(10)
for x in p1:
print(x)
异步I/O爬取数据
"""
Time:2021/6/3 16:48
Author:Spectre
"""
"""
如何通过异步I/0爬取数据
aiohttp / httpx
"""
import aiohttp
import asyncio
from bs4 import BeautifulSoup
async def main(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
# print("Status:", response.status)
# print("Content-type:", response.headers['content-type'])
html = await response.text()
bs = BeautifulSoup(html, 'lxml')
title = bs.select_one('head>title')
title = title.get_text().replace('\n','') if title else ''
print(title)
url_list = [
'https://www.jianshu.com/',
'https://blog.youkuaiyun.com/',
'https://www.douyu.com/',
'https://lol.qq.com/main.shtml',
'https://www.pinduoduo.com/',
'https://www.amap.com/',
'https://www.jd.com/',
'https://www.taobao.com/',
'https://www.colorful.cn/',
'https://www.nvidia.cn/'
]
cos = [main(x) for x in url_list]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(cos))