最近公司內部網絡經常出問題,奇慢無比,導致人臉檢測程序在下載圖片時經常卡住,為了不影響數據的核對, 決定在網絡不佳圖片下載超時后放棄下載,繼續執行后續程序。
於是整理出解決思路如下:
1、在線程中完成圖片下載任務
2、設置圖片下載超時的時間
3、當下載超時后線束下載線程, 執行后續任務
為了便於演示下載效果, 決定采集requests請求方法, 而不用urltrieve下載
一、先看看單線程如何下載圖片的問題
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# __author__:kzg import threading import time from urllib.request import urlretrieve def callbackinfo(down, block, size): ''' 回調函數: down:已經下載的數據塊 block:數據塊的大小 size:遠程文件的大小 ''' per = 100.0 * (down * block) / size if per > 100: per = 100 time.sleep(1) # sleep 1秒 print('%.2f%%' % per)
# 圖片下載函數 def downpic(url): urlretrieve(url, 'test.jpg', callbackinfo) url = 'https://s1.tuchong.com/content-image/201909/98cac03c4a131754ce46d51faf597230.jpg' # 執行線程 t = threading.Thread(target=downpic, args=(url,)) t.start() t.join(3) print("down OK") 結果: 0.00% 1.51% down OK 3.02% 4.52% 6.03%
……
可以看到,執行過程
1、將圖片下載程序塞到線程中執行
2、啟動線程
3、三秒后線程仍未執行完,放棄阻塞
4、執行print
5、線程繼續執行, 直到完成
二、守護線程(deamon)
守護線程結束, 其中的子線程也被迫結束
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # __author__:kzg import threading import time from urllib.request import urlretrieve def callbackinfo(down, block, size): ''' 回調函數: down:已經下載的數據塊 block:數據塊的大小 size:遠程文件的大小 ''' per = 100.0 * (down * block) / size if per > 100: per = 100 time.sleep(1) print('%.2f%%' % per) def downpic(url): urlretrieve(url, 'test.jpg', callbackinfo) def mainFunc(funcname, args): ''' :param funcname: 函數名(圖片下載函數) :param args: 參數(url地址) :return: ''' t = threading.Thread(target=funcname, args=(args,)) t.start() # 開始執行線程 t.join(timeout=5) # 5秒后線程仍未執行完則放棄阻塞, 繼續執行后續代碼 url = 'https://s1.tuchong.com/content-image/201909/98cac03c4a131754ce46d51faf597230.jpg' m = threading.Thread(target=mainFunc, args=(downpic, url)) m.setDaemon(True) m.start() m.join() 結果: 0.00% 1.51% 3.02% 4.52%
可以看到執行結果:
1、mainfunc函數被塞到m線程中
2、m線程設置為守護線程
3、啟動守護線程
4、mainfunc下的子線程 t在5秒后仍未執行完,
放棄阻塞,執行后續程序
m.join被執行, 守護線程結束,子線程t 被迫結束(結果中只有圖片只下載了4秒)
圖片中止下載
按說到此為止應該圓滿結束了, 然而在程序執行過程中發現子線程超時后, 確實開始執行后續代碼,但子線程並未退出,仍然在運行。 經過不斷排查發現問題出現在for循環上, 原來for循環也類似一個demon的線程,如果for循環一直不結束, 其內的子線程就不會結束。
三、遇到問題, 子線程未被關閉
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # __author__:kzg import threading import time from urllib.request import urlretrieve def callbackinfo(down, block, size): ''' 回調函數: down:已經下載的數據塊 block:數據塊的大小 size:遠程文件的大小 ''' per = 100.0 * (down * block) / size if per > 100: per = 100 time.sleep(1) print('%.2f%%' % per) # 圖片下載函數 def downpic(url): urlretrieve(url, 'test.jpg', callbackinfo) def mainFunc(funcname, args): ''' :param funcname: 函數名(圖片下載函數) :param args: 參數(url地址) :return: ''' t = threading.Thread(target=funcname, args=(args,)) t.start() # 開始執行線程 t.join(timeout=5) # 3秒后線程仍未執行完則放棄阻塞, 繼續執行后續代碼 for i in range(2): if i == 0: url = 'https://s1.tuchong.com/content-image/201909/98cac03c4a131754ce46d51faf597230.jpg' else: break # 守護線程 m = threading.Thread(target=mainFunc, args=(downpic, url)) m.setDaemon(True) m.start() m.join() print(m.is_alive()) time.sleep(100) # sleep 100秒, 模擬for一直不結束 結果: 0.00% 1.51% 3.02% 4.52% False 6.03% 7.54% 9.05% 10.55%
從結果可以看出, 5秒后deamon線程結束, 意味着 t 線程會被關閉,然而子線程 t 卻一直在執行。
怎么辦呢?
四、問題解決, 強制關閉子線程
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # __author__:kzg import threading import time import inspect import ctypes from urllib.request import urlretrieve def callbackinfo(down, block, size): ''' 回調函數: down:已經下載的數據塊 block:數據塊的大小 size:遠程文件的大小 ''' per = 100.0 * (down * block) / size if per > 100: per = 100 time.sleep(1) print('%.2f%%' % per) # 圖片下載函數 def downpic(url): urlretrieve(url, 'test.jpg', callbackinfo) def _async_raise(tid, exctype): """raises the exception, performs cleanup if needed""" tid = ctypes.c_long(tid) if not inspect.isclass(exctype): exctype = type(exctype) res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype)) if res == 0: raise ValueError("invalid thread id") elif res != 1: # """if it returns a number greater than one, you're in trouble, # and you should call it again with exc=NULL to revert the effect""" ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) raise SystemError("PyThreadState_SetAsyncExc failed") def stop_thread(thread): _async_raise(thread.ident, SystemExit) for i in range(2): if i == 0: url = 'https://s1.tuchong.com/content-image/201909/98cac03c4a131754ce46d51faf597230.jpg' else: break t = threading.Thread(target=downpic, args=(url,)) t.start() t.join(5) print(t.is_alive()) if t.is_alive(): stop_thread(t) print("t is kill") time.sleep(100) 結果: 0.00% 1.51% 3.02% 4.52% True t is kill
可以看到:
1、 主函數mainfunc去掉了
2、在for循環中直接加入子線程
3、在timeout的時間后線程仍然活着則強制關閉
附: 測試圖片下載的另一種方法
#!/usr/bin/python3 # -*- coding: utf-8 -*- import requests import os import time def downpic(url): ''' 根據url下載圖片 :param url: url地址 :return: 下載后的圖片名稱 ''' try: print("Start Down %s" % url) ret = requests.get(url, timeout=3) # 請求超時 if ret.status_code == 200: with open("test.jpg", 'wb') as fp: for d in ret.iter_content(chunk_size=10240): time.sleep(1) # 每次下載10k,sleep 1秒 fp.write(d) print("downLoad ok %s" % url) except Exception as ex: print("downLoad pic fail %s" % url)
其它:
urlretrieve第三個參數為reporthook:
是一個回調函數,當連接上服務器以及相應數據塊傳輸完畢時會觸發該回調,我們就可以利用該回調函數來顯示當前的下載進度。
下載狀態的報告,他有多個參數,
1)參數1:當前傳輸的塊數
2)參數2:塊的大小
3)參數3,總數據大小
def urlretrieve(url, filename=None, reporthook=None, data=None): """ Retrieve a URL into a temporary location on disk. Requires a URL argument. If a filename is passed, it is used as the temporary file location. The reporthook argument should be a callable that accepts a block number, a read size, and the total file size of the URL target. The data argument should be valid URL encoded data. If a filename is passed and the URL points to a local resource, the result is a copy from local file to new file. Returns a tuple containing the path to the newly created data file as well as the resulting HTTPMessage object. """
