99999久久久久久亚洲,欧美人与禽猛交狂配,高清日韩av在线影院,一个人在线高清免费观看,啦啦啦在线视频免费观看www

熱線電話:13121318867

登錄
首頁(yè)精彩閱讀淺析Python中的多進(jìn)程與多線程的使用
淺析Python中的多進(jìn)程與多線程的使用
2017-08-10
收藏

淺析Python中的多進(jìn)程與多線程的使用

在批評(píng)Python的討論中,常常說(shuō)起Python多線程是多么的難用。還有人對(duì) global interpreter lock(也被親切的稱(chēng)為“GIL”)指指點(diǎn)點(diǎn),說(shuō)它阻礙了Python的多線程程序同時(shí)運(yùn)行。因此,如果你是從其他語(yǔ)言(比如C++或Java)轉(zhuǎn)過(guò)來(lái)的話,Python線程模塊并不會(huì)像你想象的那樣去運(yùn)行。必須要說(shuō)明的是,我們還是可以用Python寫(xiě)出能并發(fā)或并行的代碼,并且能帶來(lái)性能的顯著提升,只要你能顧及到一些事情。

在本文中,我們將會(huì)寫(xiě)一個(gè)小的Python腳本,用于下載Imgur上最熱門(mén)的圖片。我們將會(huì)從一個(gè)按順序下載圖片的版本開(kāi)始做起,即一個(gè)一個(gè)地下載。在那之前,你得注冊(cè)一個(gè)Imgur上的應(yīng)用。如果你還沒(méi)有Imgur賬戶(hù),請(qǐng)先注冊(cè)一個(gè)。

本文中的腳本在Python3.4.2中測(cè)試通過(guò)。稍微改一下,應(yīng)該也能在Python2中運(yùn)行——urllib是兩個(gè)版本中區(qū)別最大的部分。
開(kāi)始動(dòng)手

讓我們從創(chuàng)建一個(gè)叫“download.py”的Python模塊開(kāi)始。這個(gè)文件包含了獲取圖片列表以及下載這些圖片所需的所有函數(shù)。我們將這些功能分成三個(gè)單獨(dú)的函數(shù):

第三個(gè)函數(shù),“setup_download_dir”,用于創(chuàng)建下載的目標(biāo)目錄(如果不存在的話)。

Imgur的API要求HTTP請(qǐng)求能支持帶有client ID的“Authorization”頭部。你可以從你注冊(cè)的Imgur應(yīng)用的面板上找到這個(gè)client ID,而響應(yīng)會(huì)以JSON進(jìn)行編碼。我們可以使用Python的標(biāo)準(zhǔn)JSON庫(kù)去解碼。下載圖片更簡(jiǎn)單,你只需要根據(jù)它們的URL獲取圖片,然后寫(xiě)入到一個(gè)文件即可。

代碼如下:

import json
import logging
import os
from pathlib import Path
from urllib.request import urlopen, Request
 
logger = logging.getLogger(__name__)
 
def get_links(client_id):
  headers = {'Authorization': 'Client-ID {}'.format(client_id)}
  req = Request('https://api.imgur.com/3/gallery/', headers=headers, method='GET')
  with urlopen(req) as resp:
    data = json.loads(resp.readall().decode('utf-8'))
  return map(lambda item: item['link'], data['data'])
 
def download_link(directory, link):
  logger.info('Downloading %s', link)
  download_path = directory / os.path.basename(link)
  with urlopen(link) as image, download_path.open('wb') as f:
    f.write(image.readall())
 
def setup_download_dir():
  download_dir = Path('images')
  if not download_dir.exists():
    download_dir.mkdir()
  return download_dir

接下來(lái),你需要寫(xiě)一個(gè)模塊,利用這些函數(shù)去逐個(gè)下載圖片。我們給它命名為“single.py”。它包含了我們最原始版本的Imgur圖片下載器的主要函數(shù)。這個(gè)模塊將會(huì)通過(guò)環(huán)境變量“IMGUR_CLIENT_ID”去獲取Imgur的client ID。它將會(huì)調(diào)用“setup_download_dir”去創(chuàng)建下載目錄。最后,使用get_links函數(shù)去獲取圖片的列表,過(guò)濾掉所有的GIF和專(zhuān)輯URL,然后用“download_link”去將圖片下載并保存在磁盤(pán)中。下面是“single.py”的代碼:

import logging
import os
from time import time
 
from download import setup_download_dir, get_links, download_link
 
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logging.getLogger('requests').setLevel(logging.CRITICAL)
logger = logging.getLogger(__name__)
 
def main():
  ts = time()
  client_id = os.getenv('IMGUR_CLIENT_ID')
  if not client_id:
    raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!")
  download_dir = setup_download_dir()
  links = [l for l in get_links(client_id) if l.endswith('.jpg')]
  for link in links:
    download_link(download_dir, link)
  print('Took {}s'.format(time() - ts))
 
if __name__ == '__main__':
  main()

在我的筆記本上,這個(gè)腳本花了19.4秒去下載91張圖片。請(qǐng)注意這些數(shù)字在不同的網(wǎng)絡(luò)上也會(huì)有所不同。19.4秒并不是非常的長(zhǎng),但是如果我們要下載更多的圖片怎么辦呢?或許是900張而不是90張。平均下載一張圖片要0.2秒,900張的話大概需要3分鐘。那么9000張圖片將會(huì)花掉30分鐘。好消息是使用了并發(fā)或者并行后,我們可以將這個(gè)速度顯著地提高。

接下來(lái)的代碼示例將只會(huì)顯示導(dǎo)入特有模塊和新模塊的import語(yǔ)句。所有相關(guān)的Python腳本都可以在這方便地找到this GitHub repository。
使用線程

線程是最出名的實(shí)現(xiàn)并發(fā)和并行的方式之一。操作系統(tǒng)一般提供了線程的特性。線程比進(jìn)程要小,而且共享同一塊內(nèi)存空間。

在這里,我們將寫(xiě)一個(gè)替代“single.py”的新模塊。它將創(chuàng)建一個(gè)有八個(gè)線程的池,加上主線程的話總共就是九個(gè)線程。之所以是八個(gè)線程,是因?yàn)槲业碾娔X有8個(gè)CPU內(nèi)核,而一個(gè)工作線程對(duì)應(yīng)一個(gè)內(nèi)核看起來(lái)還不錯(cuò)。在實(shí)踐中,線程的數(shù)量是仔細(xì)考究的,需要考慮到其他的因素,比如在同一臺(tái)機(jī)器上跑的的其他應(yīng)用和服務(wù)。

下面的腳本幾乎跟之前的一樣,除了我們現(xiàn)在有個(gè)新的類(lèi),DownloadWorker,一個(gè)Thread類(lèi)的子類(lèi)。運(yùn)行無(wú)限循環(huán)的run方法已經(jīng)被重寫(xiě)。在每次迭代時(shí),它調(diào)用“self.queue.get()”試圖從一個(gè)線程安全的隊(duì)列里獲取一個(gè)URL。它將會(huì)一直堵塞,直到隊(duì)列中出現(xiàn)一個(gè)要處理元素。一旦工作線程從隊(duì)列中得到一個(gè)元素,它將會(huì)調(diào)用之前腳本中用來(lái)下載圖片到目錄中所用到的“download_link”方法。下載完成之后,工作線程向隊(duì)列發(fā)送任務(wù)完成的信號(hào)。這非常重要,因?yàn)殛?duì)列一直在跟蹤隊(duì)列中的任務(wù)數(shù)。如果工作線程沒(méi)有發(fā)出任務(wù)完成的信號(hào),“queue.join()”的調(diào)用將會(huì)令整個(gè)主線程都在阻塞狀態(tài)。

from queue import Queue
from threading import Thread
 
class DownloadWorker(Thread):
  def __init__(self, queue):
    Thread.__init__(self)
    self.queue = queue
 
  def run(self):
    while True:
      # Get the work from the queue and expand the tuple
      # 從隊(duì)列中獲取任務(wù)并擴(kuò)展tuple
      directory, link = self.queue.get()
      download_link(directory, link)
      self.queue.task_done()
 
def main():
  ts = time()
  client_id = os.getenv('IMGUR_CLIENT_ID')
  if not client_id:
    raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!")
  download_dir = setup_download_dir()
  links = [l for l in get_links(client_id) if l.endswith('.jpg')]
  # Create a queue to communicate with the worker threads
  queue = Queue()
  # Create 8 worker threads
  # 創(chuàng)建八個(gè)工作線程
  for x in range(8):
    worker = DownloadWorker(queue)
    # Setting daemon to True will let the main thread exit even though the workers are blocking
    # 將daemon設(shè)置為T(mén)rue將會(huì)使主線程退出,即使worker都阻塞了
    worker.daemon = True
    worker.start()
  # Put the tasks into the queue as a tuple
  # 將任務(wù)以tuple的形式放入隊(duì)列中
  for link in links:
    logger.info('Queueing {}'.format(link))
    queue.put((download_dir, link))
  # Causes the main thread to wait for the queue to finish processing all the tasks
  # 讓主線程等待隊(duì)列完成所有的任務(wù)
  queue.join()
  print('Took {}'.format(time() - ts))

在同一個(gè)機(jī)器上運(yùn)行這個(gè)腳本,下載時(shí)間變成了4.1秒!即比之前的例子快4.7倍。雖然這快了很多,但還是要提一下,由于GIL的緣故,在這個(gè)進(jìn)程中同一時(shí)間只有一個(gè)線程在運(yùn)行。因此,這段代碼是并發(fā)的但不是并行的。而它仍然變快的原因是這是一個(gè)IO密集型的任務(wù)。進(jìn)程下載圖片時(shí)根本毫不費(fèi)力,而主要的時(shí)間都花在了等待網(wǎng)絡(luò)上。這就是為什么線程可以提供很大的速度提升。每當(dāng)線程中的一個(gè)準(zhǔn)備工作時(shí),進(jìn)程可以不斷轉(zhuǎn)換線程。使用Python或其他有GIL的解釋型語(yǔ)言中的線程模塊實(shí)際上會(huì)降低性能。如果你的代碼執(zhí)行的是CPU密集型的任務(wù),例如解壓gzip文件,使用線程模塊將會(huì)導(dǎo)致執(zhí)行時(shí)間變長(zhǎng)。對(duì)于CPU密集型任務(wù)和真正的并行執(zhí)行,我們可以使用多進(jìn)程(multiprocessing)模塊。

官方的Python實(shí)現(xiàn)——CPython——帶有GIL,但不是所有的Python實(shí)現(xiàn)都是這樣的。比如,IronPython,使用.NET框架實(shí)現(xiàn)的Python就沒(méi)有GIL,基于Java實(shí)現(xiàn)的Jython也同樣沒(méi)有。你可以點(diǎn)這查看現(xiàn)有的Python實(shí)現(xiàn)。
生成多進(jìn)程

多進(jìn)程模塊比線程模塊更易使用,因?yàn)槲覀儾恍枰窬€程示例那樣新增一個(gè)類(lèi)。我們唯一需要做的改變?cè)谥骱瘮?shù)中。

為了使用多進(jìn)程,我們得建立一個(gè)多進(jìn)程池。通過(guò)它提供的map方法,我們把URL列表傳給池,然后8個(gè)新進(jìn)程就會(huì)生成,它們將并行地去下載圖片。這就是真正的并行,不過(guò)這是有代價(jià)的。整個(gè)腳本的內(nèi)存將會(huì)被拷貝到各個(gè)子進(jìn)程中。在我們的例子中這不算什么,但是在大型程序中它很容易導(dǎo)致嚴(yán)重的問(wèn)題。

from functools import partial
from multiprocessing.pool import Pool
 
def main():
  ts = time()
  client_id = os.getenv('IMGUR_CLIENT_ID')
  if not client_id:
    raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!")
  download_dir = setup_download_dir()
  links = [l for l in get_links(client_id) if l.endswith('.jpg')]
  download = partial(download_link, download_dir)
  with Pool(8) as p:
    p.map(download, links)
  print('Took {}s'.format(time() - ts))

分布式任務(wù)

你已經(jīng)知道了線程和多進(jìn)程模塊可以給你自己的電腦跑腳本時(shí)提供很大的幫助,那么在你想要在不同的機(jī)器上執(zhí)行任務(wù),或者在你需要擴(kuò)大規(guī)模而超過(guò)一臺(tái)機(jī)器的的能力范圍時(shí),你該怎么辦呢?一個(gè)很好的使用案例是網(wǎng)絡(luò)應(yīng)用的長(zhǎng)時(shí)間后臺(tái)任務(wù)。如果你有一些很耗時(shí)的任務(wù),你不會(huì)希望在同一臺(tái)機(jī)器上占用一些其他的應(yīng)用代碼所需要的子進(jìn)程或線程。這將會(huì)使你的應(yīng)用的性能下降,影響到你的用戶(hù)們。如果能在另外一臺(tái)甚至很多臺(tái)其他的機(jī)器上跑這些任務(wù)就好了。

Python庫(kù)RQ非常適用于這類(lèi)任務(wù)。它是一個(gè)簡(jiǎn)單卻很強(qiáng)大的庫(kù)。首先將一個(gè)函數(shù)和它的參數(shù)放入隊(duì)列中。它將函數(shù)調(diào)用的表示序列化(pickle) ,然后將這些表示添加到一個(gè)Redis列表中。任務(wù)進(jìn)入隊(duì)列只是第一步,什么都還沒(méi)有做。我們至少還需要一個(gè)能去監(jiān)聽(tīng)任務(wù)隊(duì)列的worker(工作線程)。

第一步是在你的電腦上安裝和使用Redis服務(wù)器,或是擁有一臺(tái)能正常的使用的Redis服務(wù)器的使用權(quán)。接著,對(duì)于現(xiàn)有的代碼只需要一些小小的改動(dòng)。先創(chuàng)建一個(gè)RQ隊(duì)列的實(shí)例并通過(guò)redis-py 庫(kù)傳給一臺(tái)Redis服務(wù)器。然后,我們執(zhí)行“q.enqueue(download_link, download_dir, link)”,而不只是調(diào)用“download_link” 。enqueue方法的第一個(gè)參數(shù)是一個(gè)函數(shù),當(dāng)任務(wù)真正執(zhí)行時(shí),其他的參數(shù)或關(guān)鍵字參數(shù)將會(huì)傳給該函數(shù)。

最后一步是啟動(dòng)一些worker。RQ提供了方便的腳本,可以在默認(rèn)隊(duì)列上運(yùn)行起worker。只要在終端窗口中執(zhí)行“rqworker”,就可以開(kāi)始監(jiān)聽(tīng)默認(rèn)隊(duì)列了。請(qǐng)確認(rèn)你當(dāng)前的工作目錄與腳本所在的是同一個(gè)。如果你想監(jiān)聽(tīng)別的隊(duì)列,你可以執(zhí)行“rqworker queue_name”,然后將會(huì)開(kāi)始執(zhí)行名為queue_name的隊(duì)列。RQ的一個(gè)很好的點(diǎn)就是,只要你可以連接到Redis,你就可以在任意數(shù)量上的機(jī)器上跑起任意數(shù)量的worker;因此,它可以讓你的應(yīng)用擴(kuò)展性得到提升。下面是RQ版本的代碼:

from redis import Redis
from rq import Queue
 
def main():
  client_id = os.getenv('IMGUR_CLIENT_ID')
  if not client_id:
    raise Exception("Couldn't find IMGUR_CLIENT_ID environment variable!")
  download_dir = setup_download_dir()
  links = [l for l in get_links(client_id) if l.endswith('.jpg')]
  q = Queue(connection=Redis(host='localhost', port=6379))
  for link in links:
    q.enqueue(download_link, download_dir, link)

然而RQ并不是Python任務(wù)隊(duì)列的唯一解決方案。RQ確實(shí)易用并且能在簡(jiǎn)單的案例中起到很大的作用,但是如果有更高級(jí)的需求,我們可以使用其他的解決方案(例如 Celery)。

總結(jié)

如果你的代碼是IO密集型的,線程和多進(jìn)程可以幫到你。多進(jìn)程比線程更易用,但是消耗更多的內(nèi)存。如果你的代碼是CPU密集型的,多進(jìn)程就明顯是更好的選擇——特別是所使用的機(jī)器是多核或多CPU的。對(duì)于網(wǎng)絡(luò)應(yīng)用,在你需要擴(kuò)展到多臺(tái)機(jī)器上執(zhí)行任務(wù),RQ是更好的選擇。



數(shù)據(jù)分析咨詢(xún)請(qǐng)掃描二維碼

若不方便掃碼,搜微信號(hào):CDAshujufenxi

數(shù)據(jù)分析師資訊
更多

OK
客服在線
立即咨詢(xún)
客服在線
立即咨詢(xún)
') } function initGt() { var handler = function (captchaObj) { captchaObj.appendTo('#captcha'); captchaObj.onReady(function () { $("#wait").hide(); }).onSuccess(function(){ $('.getcheckcode').removeClass('dis'); $('.getcheckcode').trigger('click'); }); window.captchaObj = captchaObj; }; $('#captcha').show(); $.ajax({ url: "/login/gtstart?t=" + (new Date()).getTime(), // 加隨機(jī)數(shù)防止緩存 type: "get", dataType: "json", success: function (data) { $('#text').hide(); $('#wait').show(); // 調(diào)用 initGeetest 進(jìn)行初始化 // 參數(shù)1:配置參數(shù) // 參數(shù)2:回調(diào),回調(diào)的第一個(gè)參數(shù)驗(yàn)證碼對(duì)象,之后可以使用它調(diào)用相應(yīng)的接口 initGeetest({ // 以下 4 個(gè)配置參數(shù)為必須,不能缺少 gt: data.gt, challenge: data.challenge, offline: !data.success, // 表示用戶(hù)后臺(tái)檢測(cè)極驗(yàn)服務(wù)器是否宕機(jī) new_captcha: data.new_captcha, // 用于宕機(jī)時(shí)表示是新驗(yàn)證碼的宕機(jī) product: "float", // 產(chǎn)品形式,包括:float,popup width: "280px", https: true // 更多配置參數(shù)說(shuō)明請(qǐng)參見(jiàn):http://docs.geetest.com/install/client/web-front/ }, handler); } }); } function codeCutdown() { if(_wait == 0){ //倒計(jì)時(shí)完成 $(".getcheckcode").removeClass('dis').html("重新獲取"); }else{ $(".getcheckcode").addClass('dis').html("重新獲取("+_wait+"s)"); _wait--; setTimeout(function () { codeCutdown(); },1000); } } function inputValidate(ele,telInput) { var oInput = ele; var inputVal = oInput.val(); var oType = ele.attr('data-type'); var oEtag = $('#etag').val(); var oErr = oInput.closest('.form_box').next('.err_txt'); var empTxt = '請(qǐng)輸入'+oInput.attr('placeholder')+'!'; var errTxt = '請(qǐng)輸入正確的'+oInput.attr('placeholder')+'!'; var pattern; if(inputVal==""){ if(!telInput){ errFun(oErr,empTxt); } return false; }else { switch (oType){ case 'login_mobile': pattern = /^1[3456789]\d{9}$/; if(inputVal.length==11) { $.ajax({ url: '/login/checkmobile', type: "post", dataType: "json", data: { mobile: inputVal, etag: oEtag, page_ur: window.location.href, page_referer: document.referrer }, success: function (data) { } }); } break; case 'login_yzm': pattern = /^\d{6}$/; break; } if(oType=='login_mobile'){ } if(!!validateFun(pattern,inputVal)){ errFun(oErr,'') if(telInput){ $('.getcheckcode').removeClass('dis'); } }else { if(!telInput) { errFun(oErr, errTxt); }else { $('.getcheckcode').addClass('dis'); } return false; } } return true; } function errFun(obj,msg) { obj.html(msg); if(msg==''){ $('.login_submit').removeClass('dis'); }else { $('.login_submit').addClass('dis'); } } function validateFun(pat,val) { return pat.test(val); }