视频加载失败

线程相关知识点小结

4562 字
23 分钟
线程相关知识点小结

【3.0】知识点小结(线程相关)#

【一】什么是线程#

  • 进程

    • 资源单位
  • 线程

    • 执行单位
  • 将操作系统比喻成大的工厂

    • 进程相当于工厂里面的车间
    • 线程相当于车间里面的流水线

每一个进程必定自带一个线程

进程:资源单位

​ 起一个进程仅仅只是 在内存空间中开辟出一块独立的空间

线程:执行单位

​ 真正被CPU执行的其实是进程里面的线程,线程指的就是代码的执行过程,执行代码中所需要使用到的资源都找所在的进程索要

进程和线程都是虚拟单位,只是为了我们更加方便的描述问题

【二】为何要有线程#

  • 开设进程

    • 申请内存空间 — 耗资源
    • 拷贝代码 - 耗资源
  • 开设线程

    • 一个进程内可以开设多个线程
    • 在一个进程内开设多个线程无需再次申请内存空间及拷贝代码操作
  • 总结线程的优点

    • 减少了资源的消耗
    • 同一个进程下的多个线程资源共享

案例需求:开发一款文本编辑器

​ 获取用户输入的功能

​ 实时展示到屏幕的功能

​ 自动保存数据到硬盘的功能

针对上述功能进程合适还是线程合适?

​ 开三个线程更加合理

【三】开启线程的两种方式#

开启线程不需要在main下面执行代码,直接书写即可

但是我们还是习惯性的将启动命令写在main下面

【1】方式一:直接调用#

进程与线程的对比

# -*-coding: Utf-8 -*-
# @File : 01 开启线程的两种方式 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/24
from multiprocessing import Process
from threading import Thread
import time
def task(name):
print(f'当前任务:>>>{name} 正在运行')
time.sleep(3)
print(f'当前任务:>>>{name} 结束运行')
def Thread_main():
t = Thread(target=task, args=("dream",))
# 创建线程的开销非常小,几乎代码运行的一瞬间线程就已经创建了
t.start()
'''
当前任务:>>>dream 正在运行this is main process!
this is main process!
当前任务:>>>dream 结束运行
'''
def Process_main():
p = Process(target=task, args=("dream",))
p.start()
'''
this is main process!
当前任务:>>>dream 正在运行
当前任务:>>>dream 结束运行
'''
if __name__ == '__main__':
Thread_main()
# Process_main()
print('this is main process!')

【2】方式二:继承父类#

# -*-coding: Utf-8 -*-
# @File : 01 开启线程的两种方式 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/24
# 方式二
from multiprocessing import Process
from threading import Thread
import time
class MyThread(Thread):
def __init__(self, name):
# 重写了别人的方法,又不知道别人的方法里面有什么, 就调用父类的方法
super().__init__()
self.name = name
# 定义 run 函数
def run(self):
print(f'{self.name} is running')
time.sleep(3)
print(f'{self.name} is ending')
def main():
t = MyThread('dream')
t.start()
print(f'this is a main process')
"""
dream is running
this is a main process
dream is ending
"""
if __name__ == '__main__':
main()

【四】如何实现TCP服务端并发效果#

【1】基础版1.0#

服务端只能接待一个客户端

  • 客户端
# -*-coding: Utf-8 -*-
# @File : 02 如何实现客户端并发效果 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/24
from socket import *
# 不写参数:默认是TCP协议
# (1)创建客户端对象
client = socket()
# (2)绑定 IP PORT
# (2)建立链接桥梁 --(呼应客户端的 ip 和 port)
IP = '127.0.0.1'
PORT = 8082
client.connect((IP, PORT))
# (4)链接循环
while True:
# (4.1)向服务端发数据
msg_to_server = b'this is a message'
client.send(msg_to_server)
# 接受服务器返回的数据
data_from_server = client.recv(1024)
# (4.3)接收到客户端的信息
msg_from_client = data_from_server.decode('utf-8')
print(msg_from_client)
  • 服务端
# -*-coding: Utf-8 -*-
# @File : 02 如何实现客户端并发效果 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/24
from threading import Thread
from multiprocessing import Process
from socket import *
'''
服务端的三大特点:
(1)要有固定的IP和PORT
(2)24h不间断提供服务
(3)能够支持并发
'''
# 不写参数:默认是TCP协议
# (1)创建服务器对象
server = socket()
# (2)建立链接桥梁 --(呼应客户端的 ip 和 port)
IP = '127.0.0.1'
PORT = 8082
server.bind((IP, PORT))
# (3)半连接池创建
server.listen(5)
# 正常版本
def normal_version():
# (4)链接循环
while True:
# (4.1) 接受连接对象和 ip port
conn, addr = server.accept()
while True:
# 捕获异常并抛出
try:
msg_from_client = conn.recv(1024)
# (4.2)接受的信息为空时,会无限循环
if len(msg_from_client) == 0:
break
# (4.3)接收到客户端的信息
msg_from_client = msg_from_client.decode('utf-8')
print(msg_from_client)
# (4.4)返回给客户端信息
msg_to_client = msg_from_client.upper()
msg_to_client = msg_to_client.encode('utf-8')
conn.send(msg_to_client)
except Exception as e:
print(e)
break
# (4.5)关闭链接
conn.close()
# 将接受处理数据部分封装成函数调用
def talk(conn):
while True:
# 捕获异常并抛出
try:
msg_from_client = conn.recv(1024)
# (4.2)接受的信息为空时,会无限循环
if len(msg_from_client) == 0:
break
# (4.3)接收到客户端的信息
msg_from_client = msg_from_client.decode('utf-8')
print(msg_from_client)
# (4.4)返回给客户端信息
msg_to_client = msg_from_client.upper()
conn.send(msg_to_client)
except Exception as e:
print(e)
break
# (4.5)关闭链接
conn.close()
if __name__ == '__main__':
normal_version()

【2】升级版2.0#

封装函数:启用多线程

  • 服务端
# -*-coding: Utf-8 -*-
# @File : 02 如何实现客户端并发效果 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/24
from threading import Thread
from multiprocessing import Process
from socket import *
'''
服务端的三大特点:
(1)要有固定的IP和PORT
(2)24h不间断提供服务
(3)能够支持并发
'''
# 不写参数:默认是TCP协议
# (1)创建服务器对象
server = socket()
# (2)建立链接桥梁 --(呼应客户端的 ip 和 port)
IP = '127.0.0.1'
PORT = 8083
server.bind((IP, PORT))
# (3)半连接池创建
server.listen(5)
# 正常版本
def normal_version():
# (4)链接循环
while True:
# (4.1) 接受连接对象和 ip port
conn, addr = server.accept()
while True:
# 捕获异常并抛出
try:
msg_from_client = conn.recv(1024)
# (4.2)接受的信息为空时,会无限循环
if len(msg_from_client) == 0:
break
# (4.3)接收到客户端的信息
msg_from_client = msg_from_client.decode('utf-8')
print(msg_from_client)
# (4.4)返回给客户端信息
msg_to_client = msg_from_client.upper()
msg_to_client = msg_to_client.encode('utf-8')
conn.send(msg_to_client)
except Exception as e:
print(e)
break
# (4.5)关闭链接
conn.close()
# 将接受处理数据部分封装成函数调用
def talk(conn):
while True:
# 捕获异常并抛出
try:
msg_from_client = conn.recv(1024)
# (4.2)接受的信息为空时,会无限循环
if len(msg_from_client) == 0:
break
# (4.3)接收到客户端的信息
msg_from_client = msg_from_client.decode('utf-8')
print(msg_from_client)
# (4.4)返回给客户端信息
msg_to_client = msg_from_client.upper()
msg_to_client = msg_to_client.encode('utf-8')
conn.send(msg_to_client)
except Exception as e:
print(e)
break
# (4.5)关闭链接
conn.close()
# 多线程版本
def threading_version(conn):
t = Thread(target=talk, args=(conn,))
t.start()
# 多进程版本
def process_version(conn):
p = Process(target=talk, args=(conn,))
p.start()
def main_t():
# (4)链接循环
while True:
# (4.1) 接受连接对象和 ip port
conn, addr = server.accept()
threading_version(conn)
def main_p():
# (4)链接循环
while True:
# (4.1) 接受连接对象和 ip port
conn, addr = server.accept()
process_version(conn)
if __name__ == '__main__':
main_t()
  • 客户端
# -*-coding: Utf-8 -*-
# @File : 02 如何实现客户端并发效果 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/24
from socket import *
# 不写参数:默认是TCP协议
# (1)创建客户端对象
client = socket()
# (2)绑定 IP PORT
# (2)建立链接桥梁 --(呼应客户端的 ip 和 port)
IP = '127.0.0.1'
PORT = 8083
client.connect((IP, PORT))
# (4)链接循环
while True:
# (4.1)向服务端发数据
msg_to_server = b'this is a message'
client.send(msg_to_server)
# 接受服务器返回的数据
data_from_server = client.recv(1024)
# (4.3)接收到客户端的信息
msg_from_client = data_from_server.decode('utf-8')
print(msg_from_client)

【五】线程对象的 join 方法#

# -*-coding: Utf-8 -*-
# @File : 03 线程对象的 join 方法 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread
import time
def task(name):
print(f'the task {name} is beginning')
time.sleep(3)
print(f'the task {name} is ending')
def main():
t = Thread(target=task, args=('dream',))
t.start()
# 主线程等待子进程结束之后再运行
t.join()
print(f'the task is main task')
if __name__ == '__main__':
main()
# the task dream is beginning
# the task dream is ending
# the task is main task

【六】同一个进程下的多个线程之间数据是共享的#

# -*-coding: Utf-8 -*-
# @File : 04 同一个进程下的多个线程之间数据是共享的 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread
import time
money = 999
def task():
global money
money = 99
print(f'task中的money:>>>>{money}')
def main():
print(f'子进程之前的money:>>>>{money}')
t = Thread(target=task)
t.start()
print(f'子进程之后的money:>>>>{money}')
if __name__ == '__main__':
main()
# 子进程之前的money:>>>>999
# task中的money:>>>>99
# 子进程之后的money:>>>>99

【七】线程对象属性及其他方法#

同一个进程下的进程号相同

# -*-coding: Utf-8 -*-
# @File : 05 线程对象属性及其他方法 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread
import time, os
def task():
print(f'this is a task PID {os.getpid()}')
def main():
t = Thread(target=task)
t.start()
print(f'this is a main PID {os.getpid()}')
if __name__ == '__main__':
main()
# this is a task PID 6496
# this is a main PID 6496

current_thread#

获取当前进程的名字

# -*-coding: Utf-8 -*-
# @File : 05 线程对象属性及其他方法 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread, active_count, current_thread
import time, os
def task():
# 获取当前线程的名字
print(f'this is a task name {current_thread().name}')
def main():
t = Thread(target=task)
t.start()
print(f'this is a main name {current_thread().name}')
if __name__ == '__main__':
main()
# this is a task name Thread-1
# this is a main name MainThread

active_count#

统计当前活跃的线程数

  • 这里统计的线程数 2 个
    • 可能已经有一个子线程已经死了
# -*-coding: Utf-8 -*-
# @File : 05 线程对象属性及其他方法 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread, active_count, current_thread
import time, os
def task():
# 获取当前线程的名字
print(f'this is a task name {current_thread().name}')
def main():
t = Thread(target=task)
t1 = Thread(target=task)
t.start()
t1.start()
# 统计当前活跃的线程数
print(f'this is a main process active_process {active_count()}')
print(f'this is a main name :>>>> {current_thread().name}')
if __name__ == '__main__':
main()
# this is a task name Thread-1
# this is a task name Thread-2
# this is a main process active_process 1
# this is a main name :>>>> MainThread
  • 这里统计的活跃进程数是 3 个
    • 让我们第一个子进程晚一点死
# -*-coding: Utf-8 -*-
# @File : 05 线程对象属性及其他方法 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread, active_count, current_thread
import time, os
def task():
# 获取当前线程的名字
print(f'this is a task name {current_thread().name}')
time.sleep(1)
def main():
t = Thread(target=task)
t1 = Thread(target=task)
t.start()
t1.start()
# 统计当前活跃的线程数
print(f'this is a main process active_process {active_count()}')
print(f'this is a main name :>>>> {current_thread().name}')
if __name__ == '__main__':
main()
# this is a task name Thread-1
# this is a task name Thread-2
# this is a main process active_process 3
# this is a main name :>>>> MainThread

【八】守护线程#

【1】主线程死亡,子线程未死亡#

主线程结束运行后不会马上结束,而是等待其他非守护子线程结束之后才会结束

如果主线程死亡就代表者主进程也死亡,随之而来的是所有子线程的死亡

# -*-coding: Utf-8 -*-
# @File : 06 守护线程 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread
import time
def task(name):
print(f'当前 {name} is beginning')
time.sleep(2)
print(f'当前 {name} is ending')
def main():
t = Thread(target=task, args=('dream',))
t.start()
print(f' this is main process')
if __name__ == '__main__':
main()
# 当前 dream is beginning
# this is main process
# 当前 dream is ending

【2】主线程死亡,子线程也死亡#

# -*-coding: Utf-8 -*-
# @File : 06 守护线程 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread
import time
def task(name):
print(f'当前 {name} is beginning')
time.sleep(2)
print(f'当前 {name} is ending')
def main():
t = Thread(target=task, args=('dream',))
# 开启守护线程
t.daemon = True
t.start()
print(f' this is main process')
if __name__ == '__main__':
main()
# 当前 dream is beginning
# this is main process

【3】迷惑性例子#

# -*-coding: Utf-8 -*-
# @File : 06 守护线程 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread
from multiprocessing import Process
import time
def foo():
print(f' this is foo begin')
time.sleep(1)
print(f' this is foo end')
def func():
print(f' this is func begin')
time.sleep(3)
print(f' this is func end')
def main():
t1 = Thread(target=foo)
t2 = Thread(target=func)
t1.daemon = True
t1.start()
t2.start()
print(f' this is main')
if __name__ == '__main__':
main()
# this is foo begin
# this is func begin
# this is main
# this is foo end
# this is func end

分析

​ t1 是守护线程,会随着主线程的死亡而死亡

​ 当多线程开启时,主线程运行,开启子线程

​ 再开启主线程

​ 主线程结束后会等待非守护子线程结束,所以需要等待3s,等待func结束运行

​ 所以执行顺序是 子线程1---子线程2---主线程---子线程1结束---子线程2结束

【九】线程的互斥锁#

【1】问题#

所有子线程都会进行阻塞操作,导致最后的改变只是改了一次

# -*-coding: Utf-8 -*-
# @File : 07 线程的互斥锁 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread
import time
money = 100
def task():
global money
# 模拟获取到车票信息
temp = money
# 模拟网络延迟
time.sleep(2)
# 模拟购票
money = temp - 1
def main():
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
# 所有子线程结束后打印 money
print(money)
if __name__ == '__main__':
main()
# 99

【2】解决办法#

在数据发生变化的地方进行加锁处理

# -*-coding: Utf-8 -*-
# @File : 07 线程的互斥锁 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread, Lock
import time
money = 100
# 生成锁
mutex = Lock()
def task():
global money
# 数据发生改变之前加锁
mutex.acquire()
# 模拟获取到车票信息
temp = money
# 模拟网络延迟
time.sleep(0.6)
# 模拟购票
money = temp - 1
# 数据发生改变后解锁
mutex.release()
def main():
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
# 所有子线程结束后打印 money
print(money)
if __name__ == '__main__':
main()
# 0

【十】GIL全局解释器锁#

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
  • Python解释器其实有多个版本

    • Cpython
    • Jpython
    • Pypypython
  • 但是普遍使用的都是Cpython解释器

  • 在 Cpython 解释器中 GIL 是一把互斥锁,用来阻止同一个进程下的多个线程的同时进行

    • 同一个进程下的多个线程无法利用这一优势?
      • Python的多线程是不是一点用都没有?
  • 因为在 Cpython 中的内存管理不是线程安全的

    • ps<内存管理>(垃圾回收机制)
      • 应用计数

      • 标记清除

      • 分代回收

1.GIL 不是python的特点而是Cpython解释器的特点

2.GIL 保证解释器级别的数据的安全

3.GIL会导致同一个进程下的多个线程的无法同时进行即无法利用多核优势

4.针对不同的数据还是需要加不同的锁处理

5.解释型语言的通病:同一个进程下的多个线程无法利用多核优势

【十一】GIL锁与普通互斥锁的区别#

【1】普通版 1.0#

当睡了 0.1s 后

所有线程都去抢那把 GIL 锁住的数据,当所有子线程都抢到后再去修改数据就变成了 99

# -*-coding: Utf-8 -*-
# @File : 08 GIL锁 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread, Lock
import time
mutex = Lock()
money = 100
def task():
global money
temp = money
time.sleep(0.1)
money -= temp
def main():
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
if __name__ == '__main__':
main()
# 99

【2】升级版 2.0#

谁先抢到谁就先处理数据

# -*-coding: Utf-8 -*-
# @File : 08 GIL锁 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread, Lock
import time
mutex = Lock()
money = 100
def task():
global money
temp = money
money -= temp
def main():
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
if __name__ == '__main__':
main()
# 0

【3】终极版 3.0#

自动加锁并解锁

子线程启动 , 后先去抢 GIL 锁 , 进入 IO 自动释放 GIL 锁 , 但是自己加的锁还没解开 ,其他线程资源能抢到 GIL 锁,但是抢不到互斥锁

最终 GIL 回到 互斥锁的那个进程上,处理数据

# -*-coding: Utf-8 -*-
# @File : 08 GIL锁 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from threading import Thread, Lock
import time
mutex = Lock()
money = 100
def task():
global money
# 自动执行 加锁 再解锁操作
with mutex:
temp = money
time.sleep(0.1) # 只要进入 IO 会自动释放 GIL 锁
money -= temp
def main():
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
if __name__ == '__main__':
main()

【十二】同一个进程下的多线程无法利用多核优势,是不是就没用了#

【1】多线程是否有用要看情况#

  • 单核
    • 四个任务(IO密集型/计算密集型)
  • 多核
    • 四个任务(IO密集型/计算密集型)

【2】计算密集型#

一直处在计算运行中

  • 每个任务都需要 10s

    • 单核

      • 多进程:额外消耗资源

      • 多线程:减少开销

    • 多核

      • 多进程:总耗时 10s
      • 多线程:总耗时 40s+
# -*-coding: Utf-8 -*-
# @File : 09 验证多进程与多线程应用场景 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from multiprocessing import Process
from threading import Thread
import time, os
def work():
res = 0
for i in range(1, 100000000):
res *= i
def main_t():
p_list = []
# 获取当前CPU运行的个数
print(os.cpu_count())
start_time = time.time()
for i in range(12):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print(f'总耗时:>>>{time.time() - start_time}')
# 8
# 总耗时:>>>28.140103101730347
def main_p():
t_list = []
# 获取当前CPU运行的个数
print(os.cpu_count())
start_time = time.time()
for i in range(12):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(f'总耗时:>>>{time.time() - start_time}')
# 8
# 总耗时:>>>63.330037117004395
if __name__ == '__main__':
# main_t()
main_p()

【3】IO密集型#

存在多个 IO 阻塞切换操作

  • 每个任务都需要 10s
    • 多核
      • 多进程:相对浪费资源
      • 多线程:更加节省资源
# -*-coding: Utf-8 -*-
# @File : 09 验证多进程与多线程应用场景 .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/6/25
from multiprocessing import Process
from threading import Thread
import time, os
def work():
time.sleep(2)
def main_t():
p_list = []
# 获取当前CPU运行的个数
print(os.cpu_count())
start_time = time.time()
for i in range(400):
p = Process(target=work)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print(f'总耗时:>>>{time.time() - start_time}')
# 8
# 总耗时:>>>36.23059678077698
def main_p():
t_list = []
# 获取当前CPU运行的个数
print(os.cpu_count())
start_time = time.time()
for i in range(400):
t = Thread(target=work)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(f'总耗时:>>>{time.time() - start_time}')
# 8
# 总耗时:>>>2.1423909664154053
if __name__ == '__main__':
main_t()
# main_p()

【4】小结#

  • 多线程和多进程都有各自的优势

  • 通常项目都是多进程下面开多进程

    • 这样既可以利用多核又可以节省资源消耗

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
线程相关知识点小结
https://firefly.cuteleaf.cn/posts/framework/concurrent/30知识点小结线程相关/
作者
Dream
发布于
2023-06-06
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Dream
Hello, I'm Dream.
公告
欢迎来到我的博客!爱学习爱进步。
分类
标签
最新动态
站点统计
文章
300
分类
5
标签
26
总字数
640,882
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Vercel
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录