文章目錄
- 第十三章:并發程式設計
-
- 一、作業系統的基本介紹
-
- (一) 多道技術
-
- 一、空間上的複用
- 二、時間上的複用
- (二) 作業系統的作用
- (三) 多道技術總結
- 二、并發程式設計之程序
-
- (一) 基本概念
-
- 一、什麼是程序
- 二、并發
- 三、并行
- 四、程序的三種狀态
- 五、同步和異步的基本概念
- 六、阻塞與非阻塞
- (二) 建立程序的兩種方式
-
- 方式一
- 方式二
- (三) join方法
- (四) 程序之間資料彼此隔離
- (五) 程序對象及其他方法
- (六)僵屍程序與孤兒程序
- (七) 守護程序
- (八) 互斥鎖
- (九) 隊列
- (十) IPC機制
- (十一) 生産者消費者模型
- 三、并發程式設計之線程
-
- (一) 基本概念
-
- 一、什麼是線程
- 二、為何要有線程
- (二) 開啟線程的兩種方式
- (三) TCP服務端實作并發的效果
- (四) 線程對象的join方法
- (五) 同一個程序下的多個線程資料是共享的
- (六) 線程對象屬性及其他方法
- (七)守護線程
- (八) 線程互斥鎖
- (九) GIL全局解釋器鎖
- (十) GIL與普通互斥鎖的差別
- (十一) 同一個程序下的多線程無法利用多核優勢,是不是就沒有用了
- 總結
第十三章:并發程式設計
一、作業系統的基本介紹
第一代計算機(1940~1955):真空管和穿孔卡片
第二代計算機(1955~1965):半導體和批處理系統
第三代計算機(1965~1980):內建電路晶片和多道程式設計
第四代計算機(1980~至今):個人計算機
(一) 多道技術
一、空間上的複用
将記憶體分為幾部分,每個部分放入一個程式,這樣,同一時間記憶體中就有了多道程式。
二、時間上的複用
當一個程式在等待I/O時,另一個程式可以使用cpu,如果記憶體中可以同時存放足夠多的作業,則cpu的使用率可以接近100%,類似于我們國小數學所學的統籌方法。
(作業系統采用了多道技術後,可以控制程序的切換,或者說程序之間去争搶cpu的執行權限。這種切換不僅會在一個程序遇到io時進行,一個程序占用cpu時間過長也會切換,或者說被作業系統奪走cpu的執行權限)
(二) 作業系統的作用
1:隐藏醜陋複雜的硬體接口,提供良好的抽象接口
2:管理、排程程序,并且将多個程序對硬體的競争變得有序
(三) 多道技術總結
1.産生背景:針對單核,實作并發
注意:現在的主機一般是多核,那麼每個核都會利用多道技術有4個cpu,運作于cpu1的某個程式遇到io阻塞,會等到io結束再重新排程,會被排程到4個cpu中的任意一個,具體由作業系統排程算法決定。
2.空間上的複用:如記憶體中同時有多道程式
3.時間上的複用:複用一個cpu的時間片
強調:遇到io切,占用cpu時間過長也切,核心在于切之前将程序的狀态儲存下來,這樣才能保證下次切換回來時,能基于上次切走的位置繼續運作
二、并發程式設計之程序
(一) 基本概念
一、什麼是程序
1.程式:一堆代碼檔案
2.程序:一個正在運作的程式or程式的運作過程
3.程序與程式的差別:程式僅僅隻是一堆代碼而已,而程序指的是程式的運作過程。
二、并發
多個程序/任務看起來是同時運作的
三、并行
多個程序/任務是真正意義上的同時運作
四、程序的三種狀态
運作态
就緒态
阻塞态
五、同步和異步的基本概念
**同步:**就是在發出一個功能調用時,在沒有得到結果之前,該調用就不會傳回。
**異步:**它的概念和同步相對。當一個異步功能調用發出後,調用者不能立刻得到結果。當該異步功能完成後,通過狀态、通知或回調來通知調用者。如果異步功能用狀态來通知,那麼調用者就需要每隔一定時間檢查一次,效率就很低(有些初學多線程程式設計的人,總喜歡用一個循環去檢查某個變量的值,這其實是一 種很嚴重的錯誤)。如果是使用通知的方式,效率則很高,因為異步功能幾乎不需要做額外的操作。至于回調函數,其實和通知沒太多差別。
六、阻塞與非阻塞
阻塞調用: 指調用結果傳回之前,目前線程會被挂起(如遇到io操作)。函數隻有在得到結果之後才會将阻塞的線程激活。有人也許會把阻塞調用和同步調用等同起來,實際上他是不同的。對于同步調用來說,很多時候目前線程還是激活的,隻是從邏輯上目前函數沒有傳回而已。
非阻塞: 它和阻塞的概念相對應,指在不能立刻得到結果之前也會立刻傳回,同時該函數不會阻塞目前線程。
理想狀态: 我們應該讓代碼永遠處于就緒态和運作态之間切換
綜合五和六所述,最搞笑的一種組合式 異步非阻塞
(二) 建立程序的兩種方式
方式一
# 建議使用第一種方式
from multiprocessing import Process
import time
def task(name):
print('%s is running' % name)
time.sleep(3)
print('%s is end' % name)
# 注意:
# windows作業系統下,建立程序一定要在main内建立
# 因為Windows下建立程序類似于子產品導入的方式,會從往下依次執行程式
# Linux中是直接将代碼完整的拷貝一份
if __name__ == '__main__':
# 建立程序
# 1.建立一個對象
p = Process(target=task, args=('wuchangwen',))
# 2.啟動程序
p.start() # 告訴作業系統建立一個程序 異步送出
print('主')
方式二
# 第二種方式 類的繼承
from multiprocessing import Process
import time
class MyProcess(Process):
def run(self):
print('run process')
time.sleep(1)
print('end process')
if __name__ == '__main__':
p = MyProcess()
p.start()
print('主')
總結:
'''
建立程序就是在記憶體中申請一塊記憶體空間将需要運作的代碼丢進去
一個程序對應在記憶體中就是一塊獨立的記憶體空間
一個程序對應在記憶體中就是一塊獨立的記憶體空間
程序與程序之間資料預設情況下是無法直接互動的,如果需要互動則借助于第三方工具或者子產品
'''
(三) join方法
join是讓主程序等待子程序程式運作結束後,再繼續運作,不影響其他子程序的執行。
from multiprocessing import Process
import time
def task(name, n):
print('%s is running' % name)
time.sleep(n)
print('%s is end' % name)
if __name__ == '__main__':
# # 建立程序
# # 1.建立一個對象
# p1 = Process(target=task, args=('wuchangwen', 1))
# p2 = Process(target=task, args=('tom', 2))
# p3 = Process(target=task, args=('tony', 3))
# start_time = time.time()
# # 2.啟動程序
# p1.start() # 告訴作業系統建立一個程序 異步送出
# p2.start() # 告訴作業系統建立一個程序 異步送出
# p3.start() # 告訴作業系統建立一個程序 異步送出
# # time.sleep(5)
# # p.join() # 主程序等待子程序p運作結束之後再繼續往後執行程式
# p1.join()
# p2.join()
# p3.join()
start_time = time.time()
p_list = []
for i in range(1, 4):
p = Process(target=task, args=('子程序 %s' % i, i))
p.start()
p_list.append(p)
# p.join()
for p in p_list:
p.join()
print('主', time.time() - start_time)
(四) 程序之間資料彼此隔離
from multiprocessing import Process
money = 1000000
def task():
global money # 局部修改全局
money = 11111111
print('子', money)
if __name__ == '__main__':
p = Process(target=task)
p.start()
p.join()
print(money)
(五) 程序對象及其他方法
# 方式一
from multiprocessing import Process, current_process
import time
import os
# 一台計算機上面運作這很多多程序,那麼計算機是如何區分管理這些程序服務端的呢?
# 計算機會給一個運作的程序配置設定一個PID号
# 如何檢視?
# Windows電腦輸入cmd輸入tasklist即可檢視,tasklist | findstr PID檢視具體的程序
# mac電腦 進入終端之後輸入ps aux,ps aux|grep PID檢視具體的程序
def task():
print('%s is running' % current_process().pid) # 檢視目前程序的程序号
time.sleep(1)
if __name__ == '__main__':
p = Process(target=task)
p.start()
print('主', current_process().pid)
#方式二
from multiprocessing import Process, current_process
import time
import os
# 一台計算機上面運作這很多多程序,那麼計算機是如何區分管理這些程序服務端的呢?
# 計算機會給一個運作的程序配置設定一個PID号
# 如何檢視?
# Windows電腦輸入cmd輸入tasklist即可檢視,tasklist | findstr PID檢視具體的程序
# mac電腦 進入終端之後輸入ps aux,ps aux|grep PID檢視具體的程序
def task():
# print('%s is running' % current_process().pid) # 方式一、檢視目前程序的程序号
print('%s is running' % os.getpid()) # 方式二、檢視目前程序的程序号
print('%s 子程序的父程序号' % os.getppid())
time.sleep(1)
if __name__ == '__main__':
p = Process(target=task)
p.start()
# print('主', current_process().pid)
print('主', os.getpid())
print('主主主', os.getppid()) # 擷取父程序的pid号
# terminate和is_alive
from multiprocessing import Process
import time
import os
def task():
print('%s is running' % os.getpid()) # 方式二、檢視目前程序的程序号
time.sleep(1)
if __name__ == '__main__':
p = Process(target=task)
p.start()
p.terminate() # 殺死目前程序,告訴作業系統幫你去殺死目前程序,但需要時間,而代碼運作速度極快
time.sleep(0.3) # 是以此處等待小一段時間
print(p.is_alive()) # 判斷目前程序是否存活
'''
一般情況下我們會預設将存儲布爾值的變量名和傳回的結果是布爾值的方法名都起成is_開頭
'''
print('主')
(六)僵屍程序與孤兒程序
from multiprocessing import Process
import time
def run():
print('start,,,')
time.sleep(1)
print('end...')
if __name__ == '__main__':
p = Process(target=run)
p.start()
print('主')
# 僵屍程序
"""
死了但是沒有死透
當你開設了子程序之後 該程序死後不會立刻釋放占用的程序号
因為我要讓父程序能夠檢視到它開設的子程序的一些基本資訊 占用的pid号 運作時間。。。
所有的程序都會步入僵屍程序
父程序不死并且在無限制的建立子程序并且子程序也不結束
回收子程序占用的pid号
父程序等待子程序運作結束
父程序調用join方法
"""
# 孤兒程序
"""
子程序存活,父程序意外死亡
作業系統會開設一個“兒童福利院”專門管理孤兒程序回收相關資源
"""
(七) 守護程序
from multiprocessing import Process
import time
def task(name):
print('%s 張三正在活着' % name)
time.sleep(3)
print('%s 張三正在死亡')
if __name__ == '__main__':
p = Process(target=task, args=('罪人', ))
# p = Process(target=task, kwargs={'name':'罪人'})
p.daemon = True # 将程序p設定成守護程序 這一句一定要放在start方法上面才有效否則會直接報錯
p.start()
print('同夥李四畏罪自殺')
(八) 互斥鎖
多個程序操作同一份資料的時候,會出現資料錯亂的問題
針對上述問題,解決方式就是加鎖處理:将并發變成串行,犧牲效率但是保證了資料的安全
from multiprocessing import Process, Lock
import json
import time
import random
# 查票
def search(i):
# 檔案操作讀取票數
with open('data','r',encoding='utf8') as f:
dic = json.load(f)
print('使用者%s查詢餘票:%s'%(i, dic.get('ticket_num')))
# 字典取值不要用[]的形式 推薦使用get 你寫的代碼打死都不能報錯!!!
# 買票 1.先查 2.再買
def buy(i):
# 先查票
with open('data','r',encoding='utf8') as f:
dic = json.load(f)
# 模拟網絡延遲
time.sleep(random.randint(1,3))
# 判斷目前是否有票
if dic.get('ticket_num') > 0:
# 修改資料庫 買票
dic['ticket_num'] -= 1
# 寫入資料庫
with open('data','w',encoding='utf8') as f:
json.dump(dic,f)
print('使用者%s買票成功'%i)
else:
print('使用者%s買票失敗'%i)
# 整合上面兩個函數
def run(i, mutex):
search(i)
# 給買票環節加鎖處理
# 搶鎖
mutex.acquire()
buy(i)
# 釋放鎖
mutex.release()
if __name__ == '__main__':
# 在主程序中生成一把鎖 讓所有的子程序搶 誰先搶到誰先買票
mutex = Lock()
for i in range(1,11):
p = Process(target=run, args=(i, mutex))
p.start()
"""
擴充 行鎖 表鎖
注意:
1.鎖不要輕易的使用,容易造成死鎖現象(我們寫代碼一般不會用到,都是内部封裝好的)
2.鎖隻在處理資料的部分加來保證資料安全(隻在争搶資料的環節加鎖處理即可)
"""
(九) 隊列
# 隊列Queue子產品
"""
管道:subprocess
stdin stdout stderr
隊列:管道+鎖
隊列:先進先出
堆棧:先進後出
"""
from multiprocessing import Queue
# 建立一個隊列
q = Queue(5) # 括号内可以傳數字 标示生成的隊列最大可以同時存放的資料量
# 往隊列中存資料
q.put(111)
q.put(222)
q.put(333)
# print(q.full()) # 判斷目前隊列是否滿了
# print(q.empty()) # 判斷目前隊列是否空了
q.put(444)
q.put(555)
# print(q.full()) # 判斷目前隊列是否滿了
# q.put(666) # 當隊列資料放滿了之後 如果還有資料要放程式會阻塞 直到有位置讓出來 不會報錯
"""
存取資料 存是為了更好的取
千方百計的存、簡單快捷的取
同在一個屋檐下
差距為何那麼大
"""
# 去隊列中取資料
v1 = q.get()
v2 = q.get()
v3 = q.get()
v4 = q.get()
v5 = q.get()
# print(q.empty())
# V6 = q.get_nowait() # 沒有資料直接報錯queue.Empty
# v6 = q.get(timeout=3) # 沒有資料之後原地等待三秒之後再報錯 queue.Empty
try:
v6 = q.get(timeout=3)
print(v6)
except Exception as e:
print('一滴都沒有了!')
# # v6 = q.get() # 隊列中如果已經沒有資料的話 get方法會原地阻塞
# print(v1, v2, v3, v4, v5, v6)
"""
q.full()
q.empty()
q.get_nowait()
在多程序的情況下是不精确
"""
(十) IPC機制
from multiprocessing import Queue, Process
"""
研究思路
1.主程序跟子程序借助于隊列通信
2.子程序跟子程序借助于隊列通信
"""
def producer(q):
q.put('我是23号技師 很高興為您服務')
def consumer(q):
print(q.get())
if __name__ == '__main__':
q = Queue()
p = Process(target=producer,args=(q,))
p1 = Process(target=consumer,args=(q,))
p.start()
p1.start()
(十一) 生産者消費者模型
"""
生産者:生産/制造東西的
消費者:消費/處理東西的
該模型除了上述兩個之外還需要一個媒介
生活中的例子做包子的将包子做好後放在蒸籠(媒介)裡面,買包子的取蒸籠裡面拿
廚師做菜做完之後用盤子裝着給你消費者端過去
生産者和消費者之間不是直接做互動的,而是借助于媒介做互動
生産者(做包子的) + 消息隊列(蒸籠) + 消費者(吃包子的)
"""
三、并發程式設計之線程
(一) 基本概念
一、什麼是線程
"""
程序:資源機關
線程:執行機關
将作業系統比喻成一個大的工廠
那麼程序就相當于工廠裡面的工廠中的房間
而線程就是工廠中的房間裡面的流水線
每一個程序肯定自帶一個線程
再次總結:
程序:資源機關(起一個程序僅僅隻是在記憶體空間中開辟一塊獨立的空間)
線程:執行機關(真正被cpu執行的其實是程序裡面的線程,線程指的就是代碼的執行過程,執行代碼中所需要使用到的資源都找所在的程序索要)
程序和線程都是虛拟機關,隻是為了我們更加友善的描述問題
"""
二、為何要有線程
"""
開設程序
1.申請記憶體空間 耗資源
2.“拷貝代碼” 耗資源
開線程
一個程序内可以開設多個線程,在用一個程序内開設多個線程無需再次申請記憶體空間操作
總結:
開設線程的開銷要遠遠的小于程序的開銷
同一個程序下的多個線程資料是共享的!!!
"""
我們要開發一款文本編輯器
擷取使用者輸入的功能
實時展示到螢幕的功能
自動儲存到硬碟的功能
針對上面這三個功能,開設程序還是線程合适???
開三個線程處理上面的三個功能更加的合理
(二) 開啟線程的兩種方式
# from multiprocessing import Process
# from threading import Thread
# import time
#
#
# def task(name):
# print('%s is running'%name)
# time.sleep(1)
# print('%s is over'%name)
#
#
# # 開啟線程不需要在main下面執行代碼 直接書寫就可以
# # 但是我們還是習慣性的将啟動指令寫在main下面
# t = Thread(target=task,args=('egon',))
# # p = Process(target=task,args=('jason',))
# # p.start()
# t.start() # 建立線程的開銷非常小 幾乎是代碼一執行線程就已經建立了
# print('主')
from threading import Thread
import time
class MyThead(Thread):
def __init__(self, name):
"""針對刷個下劃線開頭雙下滑線結尾(__init__)的方法 統一讀成 雙下init"""
# 重寫了别人的方法 又不知道别人的方法裡有啥 你就調用父類的方法
super().__init__()
self.name = name
def run(self):
print('%s is running'%self.name)
time.sleep(1)
print('egon DSB')
if __name__ == '__main__':
t = MyThead('egon')
t.start()
print('主')
(三) TCP服務端實作并發的效果
import socket
from threading import Thread
from multiprocessing import Process
"""
服務端
1.要有固定的IP和PORT
2.24小時不間斷提供服務
3.能夠支援并發
從現在開始要養成一個看源碼的習慣
我們前期要立志稱為拷貝忍者 卡卡西 不需要有任何的創新
等你拷貝到一定程度了 就可以開發自己的思想了
"""
server =socket.socket() # 括号内不加參數預設就是TCP協定
server.bind(('127.0.0.1',8080))
server.listen(5)
# 将服務的代碼單獨封裝成一個函數
def talk(conn):
# 通信循環
while True:
try:
data = conn.recv(1024)
# 針對mac linux 用戶端斷開連結後
if len(data) == 0: break
print(data.decode('utf-8'))
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
# 連結循環
while True:
conn, addr = server.accept() # 接客
# 叫其他人來服務客戶
# t = Thread(target=talk,args=(conn,))
t = Process(target=talk,args=(conn,))
t.start()
"""用戶端"""
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
client.send(b'hello world')
data = client.recv(1024)
print(data.decode('utf-8'))
(四) 線程對象的join方法
from threading import Thread
import time
def task(name):
print('%s is running'%name)
time.sleep(3)
print('%s is over'%name)
if __name__ == '__main__':
t = Thread(target=task,args=('egon',))
t.start()
t.join() # 主線程等待子線程運作結束再執行
print('主')
(五) 同一個程序下的多個線程資料是共享的
from threading import Thread
import time
money = 100
def task():
global money
money = 666
print(money)
if __name__ == '__main__':
t = Thread(target=task)
t.start()
t.join()
print(money)
(六) 線程對象屬性及其他方法
from threading import Thread, active_count, current_thread
import os,time
def task(n):
# print('hello world',os.getpid())
print('hello world',current_thread().name)
time.sleep(n)
if __name__ == '__main__':
t = Thread(target=task,args=(1,))
t1 = Thread(target=task,args=(2,))
t.start()
t1.start()
t.join()
print('主',active_count()) # 統計目前正在活躍的線程數
# print('主',os.getpid())
# print('主',current_thread().name) # 擷取線程名字
(七)守護線程
# from threading import Thread
# import time
#
#
# def task(name):
# print('%s is running'%name)
# time.sleep(1)
# print('%s is over'%name)
#
#
# if __name__ == '__main__':
# t = Thread(target=task,args=('egon',))
# t.daemon = True
# t.start()
# print('主')
"""
主線程運作結束之後不會立刻結束 會等待所有其他非守護線程結束才會結束
因為主線程的結束意味着所在的程序的結束
"""
# 稍微有一點迷惑性的例子
from threading import Thread
import time
def foo():
print(123)
time.sleep(1)
print('end123')
def func():
print(456)
time.sleep(3)
print('end456')
if __name__ == '__main__':
t1 = Thread(target=foo)
t2 = Thread(target=func)
t1.daemon = True
t1.start()
t2.start()
print('主.......')
(八) 線程互斥鎖
from threading import Thread,Lock
import time
money = 100
mutex = Lock()
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1)
money = tmp - 1
mutex.release()
if __name__ == '__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)
(九) GIL全局解釋器鎖
Ps:部落格園密碼:[email protected]
"""
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.)
"""
"""
python解釋器其實有多個版本
Cpython
Jpython
Pypypython
但是普遍使用的都是CPython解釋器
在CPython解釋器中GIL是一把互斥鎖,用來阻止同一個程序下的多個線程的同時執行
同一個程序下的多個線程無法利用多核優勢!!!
疑問:python的多線程是不是一點用都沒有???無法利用多核優勢
因為cpython中的記憶體管理不是線程安全的
記憶體管理(垃圾回收機制)
1.應用計數
2.标記清楚
3.分代回收
"""
"""
重點:
1.GIL不是python的特點而是CPython解釋器的特點
2.GIL是保證解釋器級别的資料的安全
3.GIL會導緻同一個程序下的多個線程的無法同時執行即無法利用多核優勢(******)
4.針對不同的資料還是需要加不同的鎖處理
5.解釋型語言的通病:同一個程序下多個線程無法利用多核優勢
"""
(十) GIL與普通互斥鎖的差別
from threading import Thread,Lock
import time
mutex = Lock()
money = 100
def task():
global money
# with mutex:
# tmp = money
# time.sleep(0.1)
# money = tmp -1
mutex.acquire()
tmp = money
time.sleep(0.1) # 隻要你進入IO了 GIL會自動釋放
money = tmp - 1
mutex.release()
if __name__ == '__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)
"""
100個線程起起來之後 要先去搶GIL
我進入io GIL自動釋放 但是我手上還有一個自己的互斥鎖
其他線程雖然搶到了GIL但是搶不到互斥鎖
最終GIL還是回到你的手上 你去操作資料
"""
(十一) 同一個程序下的多線程無法利用多核優勢,是不是就沒有用了
"""
多線程是否有用要看具體情況
單核:四個任務(IO密集型\計算密集型)
多核:四個任務(IO密集型\計算密集型)
"""
# 計算密集型 每個任務都需要10s
單核(不用考慮了)
多程序:額外的消耗資源
多線程:介紹開銷
多核
多程序:總耗時 10+
多線程:總耗時 40+
# IO密集型
多核
多程序:相對浪費資源
多線程:更加節省資源
代碼驗證
# 計算密集型
# from multiprocessing import Process
# from threading import Thread
# import os,time
#
#
# def work():
# res = 0
# for i in range(10000000):
# res *= i
#
# if __name__ == '__main__':
# l = []
# print(os.cpu_count()) # 擷取目前計算機CPU個數
# start_time = time.time()
# for i in range(12):
# p = Process(target=work) # 1.4679949283599854
# t = Thread(target=work) # 5.698534250259399
# t.start()
# # p.start()
# # l.append(p)
# l.append(t)
# for p in l:
# p.join()
# print(time.time()-start_time)
# IO密集型
from multiprocessing import Process
from threading import Thread
import os,time
def work():
time.sleep(2)
if __name__ == '__main__':
l = []
print(os.cpu_count()) # 擷取目前計算機CPU個數
start_time = time.time()
for i in range(4000):
# p = Process(target=work) # 21.149890184402466
t = Thread(target=work) # 3.007986068725586
t.start()
# p.start()
# l.append(p)
l.append(t)
for p in l:
p.join()
print(time.time()-start_time)
總結
"""
多程序和多線程都有各自的優勢
并且我們後面在寫項目的時候通常可以
多程序下面再開設多線程
這樣的話既可以利用多核也可以介紹資源消耗
"""
作者:吳常文
出處:https://blog.csdn.net/qq_41405475
本文版權歸作者和CSDN共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接。