自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一篇文章讀懂 Python 多線程

開發(fā) 后端
Threading模塊從 Python 1.5.2 版開始出現(xiàn),用于增強(qiáng)底層的多線程模塊thread。Threading 模塊讓操作多線程變得更簡單,并且支持程序同時(shí)運(yùn)行多個(gè)操作。

 Threading模塊從 Python 1.5.2 版開始出現(xiàn),用于增強(qiáng)底層的多線程模塊thread。Threading 模塊讓操作多線程變得更簡單,并且支持程序同時(shí)運(yùn)行多個(gè)操作。

[[277715]]

注意,Python 中的多線程最好用于處理有關(guān) I/O 的操作,如從網(wǎng)上下載資源或者從本地讀取文件或者目錄。如果你要做的是 CPU 密集型操作,那么你需要使用 Python 的multiprocessing模塊。這樣做的原因是,Python 有一個(gè)全局解釋器鎖 (GIL),使得所有子線程都必須運(yùn)行在同一個(gè)主線程中。正因?yàn)槿绱?,?dāng)你通過多線程來處理多個(gè) CPU 密集型任務(wù)時(shí),你會(huì)發(fā)現(xiàn)它實(shí)際上運(yùn)行的更慢。因此,我們將重點(diǎn)放在那些多線程最擅長的領(lǐng)域:I/O 操作!

線程簡介

多線程能讓你像運(yùn)行一個(gè)獨(dú)立的程序一樣運(yùn)行一段長代碼。這有點(diǎn)像調(diào)用子進(jìn)程(subprocess),不過區(qū)別是你調(diào)用的是一個(gè)函數(shù)或者一個(gè)類,而不是獨(dú)立的程序。在我看來,舉例說明更有助于解釋。下面來看一個(gè)簡單的例子:

  1. import threading 
  2.  
  3. def doubler(number): 
  4. ""
  5. 可以被線程使用的一個(gè)函數(shù) 
  6. ""
  7. print(threading.currentThread.getName + '\n'
  8. print(number * 2) 
  9. print 
  10.  
  11. if __name__ == '__main__'
  12. for i in range(5): 
  13. my_thread = threading.Thread(target=doubler, args=(i,)) 
  14. my_thread.start 

這里,我們導(dǎo)入 threading 模塊并且創(chuàng)建一個(gè)叫 doubler的常規(guī)函數(shù)。這個(gè)函數(shù)接受一個(gè)值,然后把這個(gè)值翻一番。它還會(huì)打印出調(diào)用這個(gè)函數(shù)的線程的名稱,并在最后打印一行空行。然后在代碼的最后一塊,我們創(chuàng)建五個(gè)線程并且依次啟動(dòng)它們。在我們實(shí)例化一個(gè)線程時(shí),你會(huì)注意到,我們把 doubler 函數(shù)傳給target參數(shù),同時(shí)也給 doubler 函數(shù)傳遞了參數(shù)。Args參數(shù)看起來有些奇怪,那是因?yàn)槲覀冃枰獋鬟f一個(gè)序列給 doubler 函數(shù),但它只接受一個(gè)變量,所以我們把逗號(hào)放在尾部來創(chuàng)建只有一個(gè)參數(shù)的序列。

需要注意的是,如果你想等待一個(gè)線程結(jié)束,那么需要調(diào)用 join方法。

當(dāng)你運(yùn)行以上這段代碼,會(huì)得到以下輸出內(nèi)容:

  1. Thread-1 
  2.  
  3.  
  4. Thread-2 
  5.  
  6.  
  7. Thread-3 
  8.  
  9.  
  10. Thread-4 
  11.  
  12.  
  13. Thread-5 
  14.  

當(dāng)然,通常情況下你不會(huì)希望輸出打印到標(biāo)準(zhǔn)輸出。如果不幸真的這么做了,那么最終的顯示效果將會(huì)非常混亂。你應(yīng)該使用 Python 的 logging 模塊。它是線程安全的,并且表現(xiàn)出色。讓我們用 logging模塊修改上面的例子并且給我們的線程命名。代碼如下:

  1. import logging 
  2. import threading 
  3.  
  4. def get_logger: 
  5. logger = logging.getLogger("threading_example"
  6. logger.setLevel(logging.DEBUG) 
  7.  
  8. fh = logging.FileHandler("threading.log"
  9. fmt = '%(asctime)s - %(threadName)s - %(levelname)s - %(message)s' 
  10. formatter = logging.Formatter(fmt) 
  11. fh.setFormatter(formatter) 
  12.  
  13. logger.addHandler(fh) 
  14. return logger 
  15.  
  16. def doubler(number, logger): 
  17. ""
  18. 可以被線程使用的一個(gè)函數(shù) 
  19. ""
  20. logger.debug('doubler function executing'
  21. result = number * 2 
  22. logger.debug('doubler function ended with: {}'.format( 
  23. result)) 
  24.  
  25. if __name__ == '__main__'
  26. logger = get_logger 
  27. thread_names = ['Mike''George''Wanda''Dingbat''Nina'
  28. for i in range(5): 
  29. my_thread = threading.Thread( 
  30. target=doubler, name=thread_names[i], args=(i,logger)) 
  31. my_thread.start 

代碼中最大的改變就是加入了 get_logger函數(shù)。這段代碼將創(chuàng)建一個(gè)被設(shè)置為調(diào)試級(jí)別的日志記錄器。它將日志保存在當(dāng)前目錄(即腳本運(yùn)行所在的目錄)下,然后設(shè)置每行日志的格式。格式包括時(shí)間戳、線程名、日志記錄級(jí)別以及日志信息。

在 doubler 函數(shù)中,我們把 print語句換成 logging 語句。你會(huì)注發(fā)現(xiàn),在創(chuàng)建線程時(shí),我們給 doubler 函數(shù)傳入了 logger 對(duì)象。這樣做的原因是,如果在每個(gè)線程中實(shí)例化 logging 對(duì)象,那么將會(huì)產(chǎn)生多個(gè) logging 單例(singleton),并且日志中將會(huì)有很多重復(fù)的內(nèi)容。

最后,創(chuàng)建一個(gè)名稱列表,然后使用 name關(guān)鍵字參數(shù)為每一個(gè)線程設(shè)置具體名稱,這樣就可以為線程命名。運(yùn)行以上代碼,將會(huì)得到包含以下內(nèi)容的日志文件:

  1. 2016-07-24 20:39:50,055 - Mike - DEBUG - doubler function executing 
  2. 2016-07-24 20:39:50,055 - Mike - DEBUG - doubler function ended with: 0 
  3. 2016-07-24 20:39:50,055 - George - DEBUG - doubler function executing 
  4. 2016-07-24 20:39:50,056 - George - DEBUG - doubler function ended with: 2 
  5. 2016-07-24 20:39:50,056 - Wanda - DEBUG - doubler function executing 
  6. 2016-07-24 20:39:50,056 - Wanda - DEBUG - doubler function ended with: 4 
  7. 2016-07-24 20:39:50,056 - Dingbat - DEBUG - doubler function executing 
  8. 2016-07-24 20:39:50,057 - Dingbat - DEBUG - doubler function ended with: 6 
  9. 2016-07-24 20:39:50,057 - Nina - DEBUG - doubler function executing 
  10. 2016-07-24 20:39:50,057 - Nina - DEBUG - doubler function ended with: 8 

輸出結(jié)果不言自明,所以繼續(xù)介紹其他內(nèi)容。在本節(jié)中再多說一點(diǎn),即通過繼承 threading.Thread實(shí)現(xiàn)多線程。舉最后一個(gè)例子,通過繼承 threading.Thread 創(chuàng)建子類,而不是直接調(diào)用 Thread 函數(shù)。

更新后的代碼如下:

  1. import logging 
  2. import threading 
  3.  
  4. class MyThread(threading.Thread): 
  5. def __init__(self, number, logger): 
  6. threading.Thread.__init__(self) 
  7. self.number = number 
  8. self.logger = logger 
  9.  
  10. def run(self): 
  11. ""
  12. 運(yùn)行線程 
  13. ""
  14. logger.debug('Calling doubler'
  15. doubler(self.number, self.logger) 
  16.  
  17. def get_logger: 
  18. logger = logging.getLogger("threading_example"
  19. logger.setLevel(logging.DEBUG) 
  20.  
  21. fh = logging.FileHandler("threading_class.log"
  22. fmt = '%(asctime)s - %(threadName)s - %(levelname)s - %(message)s' 
  23. formatter = logging.Formatter(fmt) 
  24. fh.setFormatter(formatter) 
  25.  
  26. logger.addHandler(fh) 
  27. return logger 
  28.  
  29. def doubler(number, logger): 
  30. ""
  31. 可以被線程使用的一個(gè)函數(shù) 
  32. ""
  33. logger.debug('doubler function executing'
  34. result = number * 2 
  35. logger.debug('doubler function ended with: {}'.format( 
  36. result)) 
  37.  
  38. if __name__ == '__main__'
  39. logger = get_logger 
  40. thread_names = ['Mike''George''Wanda''Dingbat''Nina'
  41. for i in range(5): 
  42. thread = MyThread(i, logger) 
  43. thread.setName(thread_names[i]) 
  44. thread.start 

這個(gè)例子中,我們只是創(chuàng)建一個(gè)繼承于 threading.Thread的子類。像之前一樣,傳入一個(gè)需要翻一番的數(shù)字,以及 logging 對(duì)象。但是這次,設(shè)置線程名稱的方式有點(diǎn)不太一樣,變成了通過調(diào)用 thread 對(duì)象的setName方法來設(shè)置。不過仍然需要調(diào)用start來啟動(dòng)線程,不過你可能注意到我們并不需要在子類中定義該方法。當(dāng)調(diào)用start時(shí),它會(huì)通過調(diào)用run方法來啟動(dòng)線程。在我們的類中,我們調(diào)用 doubler 函數(shù)來做處理。輸出結(jié)果中除了一些添加的額外信息內(nèi)容幾乎差不多。運(yùn)行下這個(gè)腳本,看看你會(huì)得到什么。

線程鎖與線程同步

當(dāng)你有多個(gè)線程,就需要考慮怎樣避免線程沖突。我的意思是說,你可能遇到多個(gè)線程同時(shí)訪問同一資源的情況。如果不考慮這些問題并且制定相應(yīng)的解決方案,那么在開發(fā)產(chǎn)品過程中,你總會(huì)在最糟糕的時(shí)候遇到這些棘手的問題。

解決辦法就是使用線程鎖。鎖由 Python 的 threading 模塊提供,并且它最多被一個(gè)線程所持有。當(dāng)一個(gè)線程試圖獲取一個(gè)已經(jīng)鎖在資源上的鎖時(shí),該線程通常會(huì)暫停運(yùn)行,直到這個(gè)鎖被釋放。來讓我們看一個(gè)非常典型沒有卻應(yīng)具備鎖功能的例子:

  1. import threading 
  2.  
  3. total = 0 
  4.  
  5. def update_total(amount): 
  6. ""
  7. Updates the total by the given amount 
  8. ""
  9. global total 
  10. total += amount 
  11. print (total) 
  12. if __name__ == '__main__'
  13. for i in range(10): 
  14. my_thread = threading.Thread( 
  15. target=update_total, args=(5,)) 
  16. my_thread.start 

如果往以上代碼添加 time.sleep函數(shù)并給出不同長度的時(shí)間,可能會(huì)讓這個(gè)例子更有意思。無論如何,這里的問題是,一個(gè)線程可能已經(jīng)調(diào)用update_total函數(shù)并且還沒有更新完成,此時(shí)另一個(gè)線程也有可能調(diào)用它并且嘗試更新內(nèi)容。根據(jù)操作執(zhí)行順序的不同,該值可能只被增加一次。

讓我們給這個(gè)函數(shù)添加鎖。有兩種方法可以實(shí)現(xiàn)。第一種方式是使用 try/finally,從而確保鎖肯定會(huì)被釋放。下面是示例:

  1. import threading 
  2.  
  3. total = 0 
  4.  
  5. lock = threading.Lock 
  6. def update_total(amount): 
  7. ""
  8. Updates the total by the given amount 
  9. ""
  10. global total 
  11. lock.acquire 
  12. try: 
  13. total += amount 
  14. finally: 
  15. lock.release 
  16. print (total) 
  17.  
  18. if __name__ == '__main__'
  19. for i in range(10): 
  20. my_thread = threading.Thread( 
  21. target=update_total, args=(5,)) 
  22. my_thread.start 

如上,在我們做任何處理之前就獲取鎖。然后嘗試更新 total 的值,最后釋放鎖并打印出 total 的當(dāng)前值。事實(shí)上,我們可以使用 Python 的 with語句避免使用 try/finally 這種較為繁瑣的語句:

  1. import threading 
  2.  
  3. total = 0 
  4.  
  5. lock = threading.Lock 
  6.  
  7. def update_total(amount): 
  8. ""
  9. Updates the total by the given amount 
  10. ""
  11. global total 
  12. with lock: 
  13. total += amount 
  14. print (total) 
  15.  
  16. if __name__ == '__main__'
  17. for i in range(10): 
  18. my_thread = threading.Thread( 
  19. target=update_total, args=(5,)) 
  20. my_thread.start 

正如你看到的那樣,我們不再需要 try/finally作為上下文管理器,而是由with語句作為替代。

當(dāng)然你也會(huì)遇到要在代碼中通過多個(gè)線程訪問多個(gè)函數(shù)的情況。當(dāng)你第一次編寫并發(fā)代碼時(shí),代碼可能是這樣的:

  1. import threading 
  2.  
  3. total = 0 
  4.  
  5. lock = threading.Lock 
  6. def do_something: 
  7. lock.acquire 
  8. try: 
  9. print('Lock acquired in the do_something function'
  10. finally: 
  11. lock.release 
  12. print('Lock released in the do_something function'
  13. return "Done doing something" 
  14.  
  15. def do_something_else: 
  16. lock.acquire 
  17. try: 
  18. print('Lock acquired in the do_something_else function'
  19. finally: 
  20. lock.release 
  21. print('Lock released in the do_something_else function'
  22. return "Finished something else" 
  23.  
  24. if __name__ == '__main__'
  25. result_one = do_something 
  26. result_two = do_something_else 

這樣的代碼在上面的情況下能夠正常工作,但假設(shè)你有多個(gè)線程都調(diào)用這兩個(gè)函數(shù)呢。當(dāng)一個(gè)線程正在運(yùn)行這兩個(gè)函數(shù),然后另外一個(gè)線程也可能會(huì)修改這些數(shù)據(jù),最后得到的就是不正確的結(jié)果。問題是,你甚至可能沒有馬上意識(shí)到結(jié)果錯(cuò)了。有什么解決辦法呢?讓我們?cè)囍页龃鸢浮?/p>

通常首先想到的就是在調(diào)用這兩個(gè)函數(shù)的地方上鎖。讓我們?cè)囍薷纳厦娴睦?,修改成如下所示?/p>

  1. import threading 
  2.  
  3. total = 0 
  4.  
  5. lock = threading.RLock 
  6. def do_something: 
  7.  
  8. with lock: 
  9. print('Lock acquired in the do_something function'
  10. print('Lock released in the do_something function'
  11. return "Done doing something" 
  12.  
  13.  
  14. def do_something_else: 
  15. with lock: 
  16. print('Lock acquired in the do_something_else function'
  17. print('Lock released in the do_something_else function'
  18. return "Finished something else" 
  19.  
  20. def main: 
  21. with lock: 
  22. result_one = do_something 
  23. result_two = do_something_else 
  24. print (result_one) 
  25. print (result_two) 
  26.  
  27. if __name__ == '__main__'
  28. main 

當(dāng)你真正運(yùn)行這段代碼時(shí),你會(huì)發(fā)現(xiàn)它只是掛起了。究其原因,是因?yàn)槲覀冎桓嬖V threading 模塊獲取鎖。所以當(dāng)我們調(diào)用第一個(gè)函數(shù)時(shí),它發(fā)現(xiàn)鎖已經(jīng)被獲取,隨后便把自己掛起了,直到鎖被釋放,然而這將永遠(yuǎn)不會(huì)發(fā)生。

真正的解決辦法是使用重入鎖(Re-Entrant Lock)。threading 模塊提供的解決辦法是使用RLock函數(shù)。即把lock = threading.lock替換為lock = threading.RLock,然后重新運(yùn)行代碼,現(xiàn)在代碼就可以正常運(yùn)行了。

如果你想在線程中運(yùn)行以上代碼,那么你可以用以下代碼取代直接調(diào)用 main函數(shù):

  1. if __name__ == '__main__'
  2. for i in range(10): 
  3. my_thread = threading.Thread( 
  4. target=main) 
  5. my_thread.start 

每個(gè)線程都會(huì)運(yùn)行 main 函數(shù),main 函數(shù)則會(huì)依次調(diào)用另外兩個(gè)函數(shù)。最終也會(huì)產(chǎn)生 10 組結(jié)果集。

定時(shí)器

Threading 模塊有一個(gè)優(yōu)雅的 Timer類,你可以用它來實(shí)現(xiàn)在指定時(shí)間后要發(fā)生的動(dòng)作。它們實(shí)際上會(huì)啟動(dòng)自己的自定義線程,通過調(diào)用常規(guī)線程上的start方法即可運(yùn)行。你也可以調(diào)用它的cancel方法停止定時(shí)器。值得注意的是,你甚至可以在開始定時(shí)器之前取消它。

有一天,我遇到一個(gè)特殊的情況:我需要與已經(jīng)啟動(dòng)的子進(jìn)程通信,但是我需要它有超時(shí)處理。雖然處理這種特殊問題有很多不同的方法,不過我最喜歡的解決方案是使用 threading 模塊的 Timer 類。

在下面這個(gè)例子中,我們將使用 ping指令作為演示。在 Linux 系統(tǒng)中,ping 命令會(huì)一直運(yùn)行下去直到你手動(dòng)殺死它。所以在 Linux 世界里,Timer 類就顯得非常方便。示例如下:

  1. import subprocess 
  2. from threading import Timer 
  3.  
  4. kill = lambda process: process.kill 
  5. cmd = ['ping''www.google.com'
  6. ping = subprocess.Popen( 
  7. cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
  8.  
  9. my_timer = Timer(5, kill, [ping]) 
  10. try: 
  11. my_timer.start 
  12. stdout, stderr = ping.communicate 
  13. finally: 
  14. my_timer.cancel 
  15. print (str(stdout)) 

這里我們?cè)?lambda 表達(dá)式中調(diào)用 kill 殺死進(jìn)程。接下來啟動(dòng) ping 命令,然后創(chuàng)建 Timer 對(duì)象。你會(huì)注意到,第一個(gè)參數(shù)就是需要等待的秒數(shù),第二個(gè)參數(shù)是需要調(diào)用的函數(shù),緊跟其后的參數(shù)是要調(diào)用函數(shù)的入?yún)?。在本例中,我們的函?shù)是一個(gè) lambda 表達(dá)式,傳入的是一個(gè)只有一個(gè)元素的列表。如果你運(yùn)行這段代碼,它應(yīng)該會(huì)運(yùn)行 5 秒鐘,然后打印出 ping 的結(jié)果。

其他線程組件

Threading 模塊包含對(duì)其他功能的支持。例如,你可以創(chuàng)建信號(hào)量(Semaphore),這是計(jì)算機(jī)科學(xué)中最古老的同步原語之一?;旧?,一個(gè)信號(hào)量管理一個(gè)內(nèi)置的計(jì)數(shù)器。當(dāng)你調(diào)用acquire時(shí)計(jì)數(shù)器就會(huì)遞減,相反當(dāng)你調(diào)用release時(shí)就會(huì)遞增。根據(jù)其設(shè)計(jì),計(jì)數(shù)器的值無法小于零,所以如果正好在計(jì)數(shù)器為零時(shí)調(diào)用 acquire 方法,該方法將阻塞線程。

譯者注:通常使用信號(hào)量時(shí)都會(huì)初始化一個(gè)大于零的值,如 semaphore = threading.Semaphore(2)

另一個(gè)非常有用的同步工具就是事件(Event)。它允許你使用信號(hào)(signal)實(shí)現(xiàn)線程通信。在下一節(jié)中我們將舉一個(gè)使用事件的實(shí)例。

最后,在 Python 3.2 中加入了 Barrier對(duì)象。Barrier 是管理線程池中的同步原語,在線程池中多條線程需要相互等待對(duì)方。如果要傳遞 barrier,每一條線程都要調(diào)用wait方法,在其他線程調(diào)用該方法之前線程將會(huì)阻塞。全部調(diào)用之后將會(huì)同時(shí)釋放所有線程。

線程通信

某些情況下,你會(huì)希望線程之間互相通信。就像先前提到的,你可以通過創(chuàng)建 Event對(duì)象達(dá)到這個(gè)目的。但更常用的方法是使用隊(duì)列(Queue)。在我們的例子中,這兩種方式都會(huì)有所涉及。下面讓我們看看到底是什么樣子的:

  1. import threading 
  2. from queue import Queue 
  3.  
  4. def creator(data, q): 
  5. ""
  6. 生成用于消費(fèi)的數(shù)據(jù),等待消費(fèi)者完成處理 
  7. ""
  8. print('Creating data and putting it on the queue'
  9. for item in data: 
  10. evt = threading.Event 
  11. q.put((item, evt)) 
  12.  
  13. print('Waiting for data to be doubled'
  14. evt.wait 
  15.  
  16. def my_consumer(q): 
  17. ""
  18. 消費(fèi)部分?jǐn)?shù)據(jù),并做處理 
  19.  
  20. 這里所做的只是將輸入翻一倍 
  21.  
  22. ""
  23. while True
  24. data, evt = q.get 
  25. print('data found to be processed: {}'.format(data)) 
  26. processed = data * 2 
  27. print(processed) 
  28. evt.set 
  29. q.task_done 
  30.  
  31. if __name__ == '__main__'
  32. q = Queue 
  33. data = [5, 10, 13, -1] 
  34. thread_one = threading.Thread(target=creator, args=(data, q)) 
  35. thread_two = threading.Thread(target=my_consumer, args=(q,)) 
  36. thread_one.start 
  37. thread_two.start 
  38.  
  39. q.join 

讓我們掰開揉碎分析一下。首先,我們有一個(gè)創(chuàng)建者(creator)函數(shù)(亦稱作生產(chǎn)者(producer)),我們用它來創(chuàng)建想要操作(或者消費(fèi))的數(shù)據(jù)。然后用另外一個(gè)函數(shù) my_consumer來處理剛才創(chuàng)建出來的數(shù)據(jù)。Creator 函數(shù)使用 Queue 的put方法向隊(duì)列中插入數(shù)據(jù),消費(fèi)者將會(huì)持續(xù)不斷的檢測有沒有更多的數(shù)據(jù),當(dāng)發(fā)現(xiàn)有數(shù)據(jù)時(shí)就會(huì)處理數(shù)據(jù)。Queue 對(duì)象處理所有的獲取鎖和釋放鎖的過程,這些不用我們太關(guān)心。

在這個(gè)例子中,先創(chuàng)建一個(gè)列表,然后創(chuàng)建兩個(gè)線程,一個(gè)用作生產(chǎn)者,一個(gè)作為消費(fèi)者。你會(huì)發(fā)現(xiàn),我們給兩個(gè)線程都傳遞了 Queue 對(duì)象,這兩個(gè)線程隱藏了關(guān)于鎖處理的細(xì)節(jié)。隊(duì)列實(shí)現(xiàn)了數(shù)據(jù)從第一個(gè)線程到第二個(gè)線程的傳遞。當(dāng)?shù)谝粋€(gè)線程把數(shù)據(jù)放入隊(duì)列時(shí),同時(shí)也傳遞一個(gè) Event 事件,緊接著掛起自己,等待該事件結(jié)束。在消費(fèi)者側(cè),也就是第二個(gè)線程,則做數(shù)據(jù)處理工作。當(dāng)完成數(shù)據(jù)處理后就會(huì)調(diào)用 Event 事件的 set方法,通知第一個(gè)線程已經(jīng)把數(shù)據(jù)處理完畢了,可以繼續(xù)生產(chǎn)了。

最后一行代碼調(diào)用了 Queue 對(duì)象的 join方法,它會(huì)告知 Queue 等待所有線程結(jié)束。當(dāng)?shù)谝粋€(gè)線程把所有數(shù)據(jù)都放到隊(duì)列中,它也就運(yùn)行結(jié)束了。

結(jié)束語

以上涵蓋了關(guān)于線程的諸多方面,主要包括:

  • 線程基礎(chǔ)知識(shí)
  • 鎖的工作方式
  • 什么是事件以及如何使用
  • 如何使用定時(shí)器
  • 通過 Queues/Events 實(shí)現(xiàn)線程間通信

現(xiàn)在你們知道如何使用線程以及線程擅長什么了,希望在你們的代碼中能有它們的用武之地。

責(zé)任編輯:華軒 來源: 今日頭條
相關(guān)推薦

2021-02-15 13:38:38

多線程異步模型

2018-04-09 16:35:10

數(shù)據(jù)庫MySQLInnoDB

2020-04-22 13:27:20

數(shù)據(jù)分析模塊解決

2021-05-09 09:06:24

Python批處理命令

2017-09-04 13:44:00

Java

2017-06-08 22:41:34

框架標(biāo)簽

2015-10-22 14:32:44

微服務(wù)PaaS應(yīng)用開發(fā)

2020-10-09 08:15:11

JsBridge

2021-05-15 09:18:04

Python進(jìn)程

2017-09-05 08:52:37

Git程序員命令

2021-04-24 10:19:41

Python函數(shù)指向

2025-02-19 10:39:26

Python高階函數(shù)函數(shù)名

2022-02-21 09:44:45

Git開源分布式

2023-05-12 08:19:12

Netty程序框架

2021-06-30 00:20:12

Hangfire.NET平臺(tái)

2019-04-17 15:16:00

Sparkshuffle算法

2021-04-09 08:40:51

網(wǎng)絡(luò)保險(xiǎn)網(wǎng)絡(luò)安全網(wǎng)絡(luò)風(fēng)險(xiǎn)

2024-06-25 08:18:55

2021-03-24 10:00:32

Python遞歸函數(shù)Python基礎(chǔ)

2024-04-17 13:21:02

Python匿名函數(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)