洗禮靈魂,修煉python(87)-- 知識拾遺篇 —— 線程(1)

来源:https://www.cnblogs.com/yangva/archive/2018/02/17/8446995.html
-Advertisement-
Play Games

線程(上) 1.線程含義:一段指令集,也就是一個執行某個程式的代碼。不管你執行的是什麼,代碼量少與多,都會重新翻譯為一段指令集。可以理解為輕量級進程 比如,ipconfig,或者, python XX.py(執行某個py程式),這些都是指令集和,也就是各自都是一個線程。 2.線程的特性: 線程之間可 ...


線程(上)

1.線程含義:一段指令集,也就是一個執行某個程式的代碼。不管你執行的是什麼,代碼量少與多,都會重新翻譯為一段指令集。可以理解為輕量級進程

比如,ipconfig,或者, python   XX.py(執行某個py程式),這些都是指令集和,也就是各自都是一個線程。

 

2.線程的特性:

  • 線程之間可以相互通信,數據共用

  • 線程並不等同於進程

  • 線程有一定局限性

  • 線程的速度由CPU和GIL決定。

 

GIL,GIL全稱Global Interpreter Lock,全局解釋鎖,此處暫且不談,再下麵該出現的地方會做仔細的講解。

 

3.python中的線程由內置模塊Threading整合

 

例1:簡答的線程應用:

我們先看看這段代碼

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva

import threading,time

begin = time.time()
def func1():
    time.sleep(2)
    print(func1.__name__)

def func2():
    time.sleep(2)
    print(func2.__name__)

func1()
func2()

end = time.time()
print(end-begin)

  

結果:

用時差不多4s對吧。好的,當我們使用線程來修改這段代碼

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva

import threading,time

begin = time.time()
def func1():
    time.sleep(2)
    print(func1.__name__)

def func2():
    time.sleep(2)
    print(func2.__name__)

'''創建線程對象,target參數為函數名,args可以為列表或元組,列表/元組
內的參數即為函數的參數,這裡兩個函數本就沒有參數,所以設定為空,'''

t1 = threading.Thread(target=func1,args=[]) 
t2 = threading.Thread(target=func2,args=[])

#開始進程
t1.start()
t2.start()

end = time.time()
print(end-begin)

  

運行結果:

 

卧槽?啥情況?咋成了0s。這裡要註意了,這裡的是時間先出來,函數的列印語句後出來,那麼就表示整個程式里的兩個線程是同時進行的,並且沒有等線程運行結束就運行到下麵的列印用時語句了。註意這裡的幾個字“沒有等線程運行結束”。所以這裡就有問題對吧?沒關係的,線程給我們準備了一個方法——join,join方法的用意就是等線程運行結束再執行後面的代碼,那麼我們加上join再看

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva

import threading,time

begin = time.time()
def func1():
    time.sleep(2)
    print(func1.__name__)

def func2():
    time.sleep(2)
    print(func2.__name__)

'''創建線程對象,target參數為函數名,args可以為列表或元組,列表/元組
內的參數即為函數的參數,這裡兩個函數本就沒有參數,所以設定為空,'''

t1 = threading.Thread(target=func1,args=[])
t2 = threading.Thread(target=func2,args=[])

#開始進程
t1.start()
t2.start()

#等待線程運行結束
t1.join()
t2.join()

end = time.time()
print(end-begin)

  

看看結果呢?

 

正常了對吧?時間最後出現,並且和沒使用線程時節省了整整一倍對吧,那麼按照常理我們都會認為這兩個線程是同時運行的對吧?那麼真的是這樣嗎?

因為都知道一個常識,一個CPU只能同時處理一件事(這裡暫且設定這個CPU是單核),而這整個程式其實就是一個主線程,此處的主線程包括了有兩個線程。這整個下來,程式運行的每個步驟是這樣的:

 

第一步:先運行func1,因為線程t1在前面。

第二步:運行到睡眠語句時,因為睡眠語句時不占CPU,所以立馬切換到func2

第三部:運行func2

第四步:運行到睡眠語句,立馬又切換到func1的列印語句

第五部:func1整個運行完,立馬切換到func2的列印語句,結束整個程式

 

 

所以你看似是同時,其實並不是同時運行,只是誰沒有占用CPU就會立馬把運行權利放開給其他線程運行,這樣交叉運行下來就完成了整個程式的運行。就這麼簡單,沒什麼難度對吧?

此時我設定的函數是不帶參數,當然你可以試試帶參數,效果也是一樣的

 

再說明一下join的特性,join的字面意思就是加入某個組織,線程里的join意思就是加入隊列。

就好比去票站排隊買票一樣,前面的人完了才到你,票站開設一天為排好隊的人售票,那麼這裡的票站就是一個主線程,隊伍中的每個人各自都是一個線程,不過這個買票站不止有一個視窗,當前面的正在買票的人耗費很多時間時,那麼後面排隊的人如果看到其他的視窗人少就會重新排到新的隊伍中以此來節省排隊時間,儘快買到票,直到票站里的工作人員下班結束售票(整個進程結束)。我這麼說的話,相信很多人就懂了吧?生活常識對吧?

而這裡的兩個線程(或者你可以給三個、四個以上)結合起來就叫多線程(並不是真正意義上的,看後面可得),此時的兩個線程並不是同時進行,也不是串列(即一個一個來),而是併發的

 

例2:對比python2和python3中線程的不同

先看python3下的:

 

不使用線程:

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva

import threading,time

begin = time.time()
def func(n):
    res = 0
    for i in range(n):
        res += i
    print('結果為:',res)

func(10000000)
func(20000000)

end = time.time()
print(end-begin)

  

運行結果:

 

使用線程:

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva

import threading,time

begin = time.time()
def func(n):
    res = 0
    for i in range(n):
        res += i
    print('結果為:',res)

t1 = threading.Thread(target=func,args=(10000000,))
t2 = threading.Thread(target=func,args=(20000000,))

#開始進程
t1.start()
t2.start()

#等待線程運行結束
t1.join()
t2.join()

end = time.time()
print(end-begin)

  

運行結果:

 

差距竟然很小了對吧?和前面使用sleep的結果完全不一樣了。

 

再看python2下:

不使用線程:

代碼和前面的一樣,不浪費時間了,運行結果:

 

使用線程:

 

發現居然還比不使用線程還慢,卧槽,那我還搞毛的線程啊。不急著說這個

 

從python2和python3的對比下,相信你已經知道了,python3優化的很不錯了,基本能和不使用線程鎖耗時間一樣。並且同樣的代碼,不使用線程下的版本2和版本3的對比都感覺時間縮短了,這就是python3的優化。

那麼這種為何不能和前面的sleep運行的結果成倍的減少呢?在版本2里反而還不減反增。這一種就是計算密集型線程。而前面的例子使用time模塊的就是IO密集型線程

 

IO密集型:IO占用的操作,前面的time.sleep的操作和文件IO占用的則為IO密集型

計算密集型:通過計算的類型

 

好的,開始說說這個使用線程為何還是沒有很明顯節省資源了,前面我提到的,一個CPU只能同時處理一件事(這裡暫且設定這個CPU是單核)關鍵就在於CPU是單核,但相信大家對自己的電腦都很瞭解,比如我的電腦是四核的,還有的朋友的CPU可能是雙核,但再怎麼也不可能是單核對吧?單核CPU的時代已經過去了。

但是這裡它就是一個BUG,究其根源也就是前面提到的GIL,全局解釋鎖

 4.全局解釋鎖GIL

1)含義:

GIL,全局解釋鎖,由解釋器決定有無。常規里我們使用的是Cpython,python調用的底層指令就是藉助C語言來實現的,即在C語言基礎上的python,還有Jpython等等的,而只有Cpython才有這個GIL,而這個GIL並不是Python的特性,也就是這個問題並不是python自身的問題,而是這個C下的解釋器問題。

在Cpython下的運行流程就是這樣的

 

由於有這個GIL,所以在同一時刻只能有一個線程進入解釋器。

龜數在開發Cpython時,就已經有這個GIL了,當他開發時,由於有可能會有一些數據操作風險,比如同時又兩個線程拿一個數據,那麼操作後就會有不可預估的後患了,而龜數當時為了避免這個問題,而當時也正是CPU單核時期,所以直接就加了這個GIL,防止同一時刻多個線程去操作同一個數據。

那麼到了多核CPU時代,這個解決辦法在現在來看就是一個BUG了。

總之,python到目前為止,沒有真正意義上的多線程,不能同時有多個線程操作一個數據,並且這個GIL也已經去不掉了,很早就有人為了取消GIL而奮鬥著,但是還是失敗了,反正Cpython下,就是有這麼個問題,在python3中只是相對的優化了,也沒有根本的解決GIL。並且只在計算密集型里體現的很明顯

 

那麼有朋友覺得,卧槽,好XX坑啊,那我XX還學個啥玩意兒啊,崩潰中,哈哈哈

沒法啊,就是這麼個現狀,但是多線程既然開不了,可以開多進程和協程啊。而且在以後還是有很多替代方案的。

 

 

總結:

 

根據需求選擇方案。

 

如果是IO密集型:使用線程

 

如果是計算密集型:使用多進程/C語言指令/協程

 

 

5.setDaemon特性

好的,來點實際的

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva

import threading,time

begin = time.time()
def music(name):
    for i in range(2):
        print('I listenning the music %s,%s'%(name,time.ctime()))
        time.sleep(2)
        print('end listenning %s'%time.ctime())

def movie(name):
    for i in range(2):
        print('I am watching the movie %s,%s'%(name,time.ctime()))
        time.sleep(3)
        print('end wachting %s'%time.ctime())

t1 = threading.Thread(target=music,args = ('晴天-周傑倫',) )
t2 = threading.Thread(target=movie,args=('霸王別姬',))
t1.start()
t2.start()
t1.join()
t2.join()

end = time.time()
print(end - begin)

  

查看運行結果:

 

因為這是IO密集型的,所以可以有多線程的效果。

 

那麼在很多的開發中,還有另一種寫法

 

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva

import threading,time

begin = time.time()
def music(name):
    for i in range(2):
        print('I listenning the music %s,%s'%(name,time.ctime()))
        time.sleep(2)
        print('end listenning %s'%time.ctime())

def movie(name):
    for i in range(2):
        print('I am watching the movie %s,%s'%(name,time.ctime()))
        time.sleep(3)
        print('end wachting %s'%time.ctime())

threads = []
t1 = threading.Thread(target=music,args = ('晴天-周傑倫',) )
t2 = threading.Thread(target=movie,args=('霸王別姬',))
threads.append(t1)
threads.append(t2)

for i in threads:
    i.start()
    i.join()

end = time.time()
print(end - begin)

  

而這種寫法的運行結果:

 

咋回事,10s,註意了,這是很多人容易犯的錯

首先要說下,join是等程式執行完再往下走,所以join帶有阻塞功能,當你把i.join()放到for迴圈裡面, 那麼聽音樂的線程必須結束後再執行看電影的線程,也就是整個程式變成串列了對吧?

所以正確的寫法是這樣:

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva

import threading,time

begin = time.time()
def music(name):
    for i in range(2):
        print('I listenning the music %s,%s'%(name,time.ctime()))
        time.sleep(2)
        print('end listenning %s'%time.ctime())

def movie(name):
    for i in range(2):
        print('I am watching the movie %s,%s'%(name,time.ctime()))
        time.sleep(3)
        print('end wachting %s'%time.ctime())

threads = []
t1 = threading.Thread(target=music,args = ('晴天-周傑倫',) )
t2 = threading.Thread(target=movie,args=('霸王別姬',))
threads.append(t1)
threads.append(t2)

for i in threads:
    i.start()

i.join()

end = time.time()
print(end - begin)

  

運行結果:

 

結果和前面的寫法一樣了對吧?說下,for迴圈下的i,我們可以知道i一定是for結束後的最後的值,不信的話可以試試這個簡單的:

 

那麼說回上面的問題,當i.join()時,此時的i一定是t2對不對?那麼整個程式就在t2阻塞住了,直到t2執行完了才執行列印總用時語句,既然執行t2,因為執行t2要6秒,而t1要4秒,那麼可以確定,在t2執行完時,t1絕對執行完了的。或者換個說法,for迴圈開始,t1和t2誰先開始不一定,因為線程都是搶著執行,但一定是t1先結束,然後再是t2結束,再結束整個程式。所以說,只有把i.join()放在for迴圈外,才真的達到了多線程的效果。

 

 

好的,再說一個有趣的東西,不多說,直接看

 

未截到圖的區域和上面的一樣,不浪費時間了。看到了嗎?最後列印的時間居然在第三排,如果你們自己測試了的話,就知道這列印時間語句和上面兩個是同時出現的,咋回事,因為這是主線程啊,主線程和兩個子線程同時運行的,所以這樣,那麼我們加一個東西

 加了一個setDaemon(True),這個方法的意思是設置守護進程,並且要註意,這個必須在設置的線程start()方法之前

 

 咦?主線程運行後就直接結束了,這啥情況呢?那再設置在子線程上呢:

設置在t1(聽音樂)上:

 

再設置在t2(看電影)上:

 

看出什麼問題了嗎?

好的,不廢話,直接說作用吧,setDaemon是守護進程的意思,而這裡我們用線上程上,也就是對線程的守護。設置誰做為守護線程(進程),那麼當此線程結束後就不管被守護的線程(進程)結束與否,程式是否結束全在於其他線程運行結束與否,但被守護的線程也一直正常的在運行。所以上面的主線程設置守護線程後,因為等不到其他同級別的線程運行所以就直接結束了。而當設置t1作為守護線程時,程式就不管t1了,開始在意其他線程t2運行結束與否,但同時還是在運行自己,因為t2運行時間比t1久,所以t1和t2還是正常的運行了。而當設置t2作為守護線程時,當t1聽完音樂結束,整個程式也結束了,而t2並沒有正常的結束,不過一直存在的,就是這麼個意思

 

6.通過自定義類設置線程

 

#!usr/bin/env python
#-*- coding:utf-8 -*-

# author:yangva
import threading,time


class mythread(threading.Thread):
    def __init__(self,name):
        super(mythread,self).__init__()
        self.name = name

    def run(self): #對繼承threading的重寫方法
        print('%s is rurning'%self.name)
        time.sleep(2)

t = mythread('yang')
t.start()

  

運行結果:

 

沒啥特點對不對,其實就是寫了一個類繼承thread,然後運行而已。本質上以上的代碼和下麵這一段沒區別:

 

 

 好的,本篇博文暫且到這裡,還沒完,下一篇的才是重頭戲

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 命令模式下輸入如下命令可實現替換: ~~~~ s/str1/str2/ 替換當前行第一個 str1 為 str2 s/str1/str2/g 替換當前行中所有的 str1 為 str2 m,ns/str1/str2/ 替換第 m 行到第 n 行中每一行的第一個 str1 為 str2 m,ns/st ...
  • 文檔版本號:20180216最近在Ubuntu Linux 14.04上和CentOS Linux 7.4上成功安裝了Harbor,現將過程整理如下,供大家參考: 備註:使用非root用戶操作Docker,需要創建docker組 sudo groupadd docker 將當前用戶加入docker組 ...
  • wget命令用來從指定的URL下載文件。wget非常穩定,它在帶寬很窄的情況下和不穩定網路中有很強的適應性,如果是由於網路的原因下載失敗,wget會不斷的嘗試,直到整個文件下載完畢。如果是伺服器打斷下載過程,它會再次聯到伺服器上從停止的地方繼續下載。這對從那些限定了鏈接時間的伺服器上下載大文件非常有 ...
  • 本文目錄:1. LVS簡介2. LVS-ipvs三種模式的工作原理 2.1 VS/NAT模式 2.2 VS/TUN模式 2.3 VS/DR模式 2.4 lvs-ipvs的三種模式比較3. VS/TUN和VS/DR模式中的ARP問題4. LVS負載均衡的調度演算法 網站架構中,負載均衡技術是實現網站架構 ...
  • PAM(Pluggable Authentication Modules) PAM架構 pam 認證原理 PAM 認證過程 PAM的配置文件 pam文檔 限制的實現方式 共有3種方式: 1)通過ulimit命令 2)在/etc/security/limits.d/目錄下創建限制文件來實現 3)修改/ ...
  • AIDE ...
  • JavaScript的數據類型 基本區分方法 ECMAScript標准定義了7種數據類型 6 種 基本類型: Boolean,兩種取值:true和false Null,一種取值:null Undefined,一種取值:undefined Number,JS的數值為基於 IEEE 754 標準的雙精度 ...
  • JDK8已經發佈快4年的時間了,現在來談它的新特性顯得略微的有點“不合時宜”。儘管JDK8已不再“新”,但它的重要特性之一——Lambda表達式依然是不被大部分開發者所熟練運用,甚至不被開發者所熟知。 國內的開發環境大家都知道,有各種的老項目,有各種各樣的發佈風險,讓公司以及項目組對新的技術往往望而 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...