python進(jìn)程 - 調(diào)試報(bào)錯(cuò) you are not using fork to start your child processes
在走這段代碼的時(shí)候報(bào)錯(cuò)了,記錄一下我的調(diào)試過(guò)程,感覺(jué)有個(gè)思路來(lái)走就挺好的。
- 1、報(bào)錯(cuò)與解決
文件名字:ClassifierTest.py
import torch import torchvision import torchvision.transforms as transforms from torchTest import imgShow transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) trainset = torchvision.datasets.CIFAR10(root='Resources/CIFAR10', # 存放路徑 #?。?Resources/CIFAR10是絕對(duì)路徑,C:\Resources\CIFAR10 train=True, download=True, # 是否下載訓(xùn)練集 transform=transform) # 圖片轉(zhuǎn)換 trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2) testset = torchvision.datasets.CIFAR10(root='Resources/CIFAR10', train=False, download=True, transform=transform) testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2) classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') dataIter = iter(trainloader) images, labels = dataIter.next() imgShow.imshow(torchvision.utils.make_grid(images)) print(' '.join(classes[labels[j]] for j in range(4)))
報(bào)錯(cuò)
This probably means that you are not using fork to start your
child processes and you have
forgotten to use the proper idiom
in the main module:
if __name__ == '__main__':
freeze_support()
...
The "freeze_support()"
line can be omitted if the program
is not going to be frozen to
produce an executable.
關(guān)于這個(gè)報(bào)錯(cuò),涉及線程問(wèn)題,改num_workers=0,當(dāng)然就么事沒(méi)有,然而,作為一個(gè)優(yōu)秀的程序員,能止步于此嗎,不行的。
我百度了一下報(bào)錯(cuò)情況,找到這樣的解決方案,是可行:
def main(): transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) trainset = torchvision.datasets.CIFAR10(root='Resources/CIFAR10', # 存放路徑,注:/Resources/CIFAR10是絕對(duì)路徑,C:\Resources\CIFAR10 train=True, download=True, # 是否下載訓(xùn)練集 transform=transform) # 圖片轉(zhuǎn)換 trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2) testset = torchvision.datasets.CIFAR10(root='Resources/CIFAR10', train=False, download=True, transform=transform) testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2) classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck') dataIter = iter(trainloader) images, labels = dataIter.next() imgShow.imshow(torchvision.utils.make_grid(images)) print(' '.join(classes[labels[j]] for j in range(4))) if __name__=='__main__': #不加這句就會(huì)報(bào)錯(cuò) main()
- 2、為什么是main?
整段放在main里面,就安全了——為什么呢?
對(duì)于python編程我還是萌新,實(shí)在想不明白加個(gè)__name__=='__main__'判斷有什么魅力。
|
關(guān)于__name__屬性: 作為啟動(dòng)腳本,它模塊的__name__都是__main__。 此句主要作用在于有時(shí)候import,不想運(yùn)行引用模塊中某些語(yǔ)句的時(shí)候,以啟動(dòng)模塊的名字作為區(qū)別。 |
報(bào)錯(cuò)的位置在這里:
C:\Users\13723\AppData\Local\Programs\Python\Python39\Lib\multiprocessing\spawn.py
def _check_not_importing_main():
if getattr(process.current_process(), '_inheriting', False):
raise RuntimeError('''
An attempt has been made to start a new process before the
current process has finished its bootstrapping phase...''')
|
getattr(實(shí)例, 屬性名字, 默認(rèn)值) 如果有屬性,取True,否則取默認(rèn)值,沒(méi)有默認(rèn)值則取False。 |
_inheriting,查找當(dāng)前程序的可繼承性?沒(méi)用過(guò),筆者不知道呢。
看不懂(下文有解),只能從方法名字入手,它走這一段為了什么——
檢查是不是源自__main__模塊,即程序不讓由執(zhí)行腳本import的模塊走這一段。
我跑ClassifierTest.py(進(jìn)程pid1),它在走到
dataIter = iter(trainloader)
里面,由其他模塊,再導(dǎo)入了一次ClassifierTest.py(此時(shí)是進(jìn)程pid2)
而當(dāng)增加判斷 __name__==’__main__’,就避免模塊陷入執(zhí)行的死循環(huán)。
- 3、為什么多一個(gè)進(jìn)程?
3.1 現(xiàn)象
為什么會(huì)多一個(gè)進(jìn)程,num_workers=2,此句是一個(gè)進(jìn)程兩個(gè)線程worker,還是兩個(gè)進(jìn)程worker呢?

我很奇怪,為什么不是開(kāi)線程,而是開(kāi)進(jìn)程這么個(gè)重量級(jí)東西。
雖然叫做process,但它應(yīng)該只干一個(gè)事情——畢竟進(jìn)程的重量級(jí)要大于線程。

3.2 線程與進(jìn)程
這個(gè)時(shí)候就很糾結(jié)線程和進(jìn)程的區(qū)別了,
(參考:https://www.zhihu.com/question/25532384)
線程是cpu執(zhí)行的時(shí)間段顆粒,
進(jìn)程保存上下文,cpu切進(jìn)進(jìn)程里面讀取上下文(寄存器、指令內(nèi)容之類(lèi))。
這樣看來(lái),如果進(jìn)程是倉(cāng)庫(kù),線程就是倉(cāng)庫(kù)里面的機(jī)器人,等待CPU來(lái)靈魂激活。但是在一個(gè)倉(cāng)庫(kù)里面工作,必然比在多個(gè)倉(cāng)庫(kù)里面工作要省事。
所以為什么要開(kāi)多進(jìn)程呢?
一個(gè)莫名的靈感,讓我查了一下fork(),
(參考:http://www.rzrgm.cn/liyuan989/p/4279210.html)
|
因?yàn)檫M(jìn)程、線程是windows系統(tǒng)的概念,unix中只有進(jìn)程的說(shuō)法。 在windows當(dāng)中,進(jìn)程是資源管理的最小單位,而線程是程序執(zhí)行的最小單位。 fork創(chuàng)建的一個(gè)子進(jìn)程幾乎但不完全與父進(jìn)程相同。 子進(jìn)程得到與父進(jìn)程用戶(hù)級(jí)虛擬地址空間相同的(但是獨(dú)立的)一份拷貝, 包括文本、數(shù)據(jù)和bss段、堆以及用戶(hù)棧等。 子進(jìn)程還獲得與父進(jìn)程任何打開(kāi)文件描述符相同的拷貝, 這就意味著子進(jìn)程可以讀寫(xiě)父進(jìn)程中任何打開(kāi)的文件,父進(jìn)程和子進(jìn)程區(qū)別在于它們有著不同的PID。 |
fork 意為分支,分支與父進(jìn)程幾乎一樣的子進(jìn)程。子進(jìn)程區(qū)別于父進(jìn)程,兩者有不同的pid,但二者的引用均指向相同的地址。
話(huà)雖如此,Python里面確實(shí)是包含threading,和process模塊,那為什么選擇process更好?
(參考:https://zhuanlan.zhihu.com/p/20953544)
一個(gè)進(jìn)程,有一個(gè)全局鎖GIL(Global Interpreter Lock),此設(shè)定是為了數(shù)據(jù)安全。
線程執(zhí)行時(shí),先獲取GIL,執(zhí)行代碼直到sleep或掛起,釋放GIL。
所以多線程執(zhí)行,其實(shí)僅是宏觀時(shí)間上處理多任務(wù),微觀時(shí)間上仍是順序處理。
而每個(gè)進(jìn)程有各自獨(dú)立的GIL,互不干擾,多進(jìn)程才能在真正意義上實(shí)現(xiàn)并行執(zhí)行(多核CPU同時(shí)做多個(gè)任務(wù),程序在微觀時(shí)間上同時(shí)執(zhí)行)。
3.3 Python中,worker是進(jìn)程
為什么會(huì)再讀一次ClassifierTest.py,從堆棧看,是這里:
(注,以下截圖可能取自不同次調(diào)試,所以父pid會(huì)不同)

走了 exec(code, run_globals) 導(dǎo)致再此導(dǎo)入 ClassifierTest.py 。
再往前走frame not available,也即IDE只能看到spawn_main函數(shù)。
(spawn應(yīng)該就是孵化了,孵化進(jìn)程的,還挺有蛇下蛋的感覺(jué))

更之前的調(diào)用情況沒(méi)有了,可以猜是不是新進(jìn)程直接調(diào)用spawn_main了,那就找spawn_main引用。
(可能pyCharm我還沒(méi)get靈魂用法,spawn_main引用我是用notepad++查找全局的)
Python39\Lib\multiprocessing\popen_spawn_win32.py

前后呼應(yīng):
在查看堆棧的過(guò)程中,恰巧看到了_inheriting的賦值:

堆棧可以看到對(duì)_inheriting賦值,此時(shí)就很明了表示是否子進(jìn)程,此處賦值True。
再者,inheriting是ing結(jié)尾,表示進(jìn)行時(shí)狀態(tài);如果是表示繼承性,應(yīng)該叫inherited,如此看來(lái)這個(gè)編程就很細(xì)心,自己寫(xiě)程序的時(shí)候也得注意。
3.4 num_workers=2 的結(jié)果

前文設(shè)置num_workers = 2,此時(shí)就是父進(jìn)程帶著兩個(gè)子進(jìn)程,
__name__==’__main__’ 的處理,阻止了子進(jìn)程由于調(diào)用 ClassifierTest.py 而再生子子進(jìn)程的子孫無(wú)窮盡也。
主線程12012 它有兩個(gè)worker,分別是 15480 和 7036 。
(這個(gè)數(shù)值是系統(tǒng)分配的pid編號(hào),區(qū)分進(jìn)程的代號(hào),每次啟動(dòng)程序都不同)
15480 和 7036 帶著自己的Queue,dataloader.py完成了這個(gè)配置。
dataIter = iter(trainloader)
images, labels = dataIter.next()
當(dāng)執(zhí)行 next(),程序會(huì)讀取象 dataIter 當(dāng)中的 _data_queue ,這個(gè)數(shù)據(jù)由兩個(gè)子進(jìn)程各自傳入。
data = self._data_queue.get(timeout=timeout)
具體實(shí)現(xiàn)看這個(gè)類(lèi):
C:\Users\13723\PycharmProjects\pythonProject\venv\Lib\site-packages\torch\utils\data\dataloader.py
class _MultiProcessingDataLoaderIter(_BaseDataLoaderIter):
pass
- 4、結(jié)語(yǔ)
由一個(gè)小小的報(bào)錯(cuò),能“查漏補(bǔ)缺”知識(shí)漏洞就挺好的,鍛煉思維也挺好的。共勉。

浙公網(wǎng)安備 33010602011771號(hào)