为什么有人说 Python 的多线程是鸡肋

Python014

为什么有人说 Python 的多线程是鸡肋,第1张

因为 Python 中臭名昭著的 GIL。

那么 GIL 是什么?为什么会有 GIL?多线程真的是鸡肋吗? GIL 可以去掉吗?带着这些问题,我们一起往下看,同时需要你有一点点耐心。

多线程是不是鸡肋,我们先做个实验,实验非常简单,就是将数字 “1亿” 递减,减到 0 程序就终止,这个任务如果我们使用单线程来执行,完成时间会是多少?使用多线程又会是多少?show me the code

单线程

在我的4核 CPU 计算机中,单线程所花的时间是 6.5 秒。可能有人会问,线程在哪里?其实任何程序运行时,默认都会有一个主线程在执行。(关于线程与进程这里不展开,我会单独开一篇文章)

多线程

创建两个子线程 t1、t2,每个线程各执行 5 千万次减操作,等两个线程都执行完后,主线程终止程序运行。结果,两个线程以合作的方式执行是 6.8 秒,反而变慢了。按理来说,两个线程同时并行地运行在两个 CPU 之上,时间应该减半才对,现在不减反增。

是什么原因导致多线程不快反慢的呢?

原因就在于 GIL ,在 Cpython 解释器(Python语言的主流解释器)中,有一把全局解释锁(Global Interpreter Lock),在解释器解释执行 Python 代码时,先要得到这把锁,意味着,任何时候只可能有一个线程在执行代码,其它线程要想获得 CPU 执行代码指令,就必须先获得这把锁,如果锁被其它线程占用了,那么该线程就只能等待,直到占有该锁的线程释放锁才有执行代码指令的可能。

因此,这也就是为什么两个线程一起执行反而更加慢的原因,因为同一时刻,只有一个线程在运行,其它线程只能等待,即使是多核CPU,也没办法让多个线程「并行」地同时执行代码,只能是交替执行,因为多线程涉及到上线文切换、锁机制处理(获取锁,释放锁等),所以,多线程执行不快反慢。

什么时候 GIL 被释放呢?

当一个线程遇到 I/O 任务时,将释放GIL。计算密集型(CPU-bound)线程执行 100 次解释器的计步(ticks)时(计步可粗略看作 Python 虚拟机的指令),也会释放 GIL。可以通过设置计步长度,查看计步长度。相比单线程,这些多是多线程带来的额外开销

CPython 解释器为什么要这样设计?

多线程是为了适应现代计算机硬件高速发展充分利用多核处理器的产物,通过多线程使得 CPU 资源可以被高效利用起来,Python 诞生于1991年,那时候硬件配置远没有今天这样豪华,现在一台普通服务器32核64G内存都不是什么司空见惯的事

但是多线程有个问题,怎么解决共享数据的同步、一致性问题,因为,对于多个线程访问共享数据时,可能有两个线程同时修改一个数据情况,如果没有合适的机制保证数据的一致性,那么程序最终导致异常,所以,Python之父就搞了个全局的线程锁,不管你数据有没有同步问题,反正一刀切,上个全局锁,保证数据安全。这也就是多线程鸡肋的原因,因为它没有细粒度的控制数据的安全,而是用一种简单粗暴的方式来解决。

这种解决办法放在90年代,其实是没什么问题的,毕竟,那时候的硬件配置还很简陋,单核 CPU 还是主流,多线程的应用场景也不多,大部分时候还是以单线程的方式运行,单线程不要涉及线程的上下文切换,效率反而比多线程更高(在多核环境下,不适用此规则)。所以,采用 GIL 的方式来保证数据的一致性和安全,未必不可取,至少在当时是一种成本很低的实现方式。

那么把 GIL 去掉可行吗?

还真有人这么干多,但是结果令人失望,在1999年Greg Stein 和Mark Hammond 两位哥们就创建了一个去掉 GIL 的 Python 分支,在所有可变数据结构上把 GIL 替换为更为细粒度的锁。然而,做过了基准测试之后,去掉GIL的 Python 在单线程条件下执行效率将近慢了2倍。

Python之父表示:基于以上的考虑,去掉GIL没有太大的价值而不必花太多精力。

可以,用PYQT+CV2,四个USB连接成功,程序如下,UI要自己搞了,放不下

# -*- coding: utf-8 -*-

import sys#, time

from PyQt5 import QtWidgets

from PyQt5.QtCore import QTimer, QThread, pyqtSignal

from Ui_cv2ui_thread import Ui_MainWindow

import cv2 as cv

from PyQt5.QtGui import QImage, QPixmap

from PyQt5.QtWidgets import (QApplication, QDialog, QFileDialog, QGridLayout,

QLabel, QPushButton, QColorDialog)

import numpy as np

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):

def __init__(self, parent=None):

super(MainWindow, self).__init__(parent=parent)

self.setupUi(self) #这个一定要在这个最前面位置

# define the slot for pushbutton to save the merged image

self.pushButton.clicked.connect(self.savemergeimage)

self.img = np.ndarray(())#空的numpy array

self.img1 = np.ndarray(())

self.img2= np.ndarray(())

self.img3= np.ndarray(())

self.img4= np.ndarray(())

self.img4= np.empty([960,1280, 3], int)

self.cap = cv.VideoCapture(3) #注意,由大开到小,很重要

self.cap.set(3, 640) # setup the resolution of CCD

self.cap.set(4, 480)

ret, self.img=self.cap.read()

self.cap1 = cv.VideoCapture(2)

self.cap1.set(3, 640)

self.cap1.set(4, 480)

ret, self.img1=self.cap1.read()

self.cap2 = cv.VideoCapture(1)

self.cap2.set(3, 640)

self.cap2.set(4, 480)

ret, self.img2=self.cap2.read()

self.cap3 = cv.VideoCapture(0)

self.cap3.set(3, 640)

self.cap3.set(4, 480)

ret, self.img3=self.cap3.read()

#time.sleep(1)也许需要延迟,等他准备好

# 初始化一个定时器,在其他条件下用的

#self.timer = QTimer(self)

# 实例化一个线程

self.work0= WorkThread()

self.work0.trigger.connect(self.ccd2)

# 定义时间任务是一次性任务就设定下一行

#self.timer.setSingleShot(True)

# 启动时间任务,注意一致性

self.work0.start()

# 实例化一个线程

self.work= WorkThread()

# 多线程的信号触发连接到ccd3

self.work.trigger.connect(self.ccd3)

self.work.start()

# 实例化一个线程

self.work2 = WorkThread()

# 多线程的信号触发连接到ccd4

self.work2.trigger.connect(self.ccd4)

self.work2.start()

# 实例化一个线程

self.work3 = WorkThread()

# 多线程的信号触发连接到ccd1

self.work3.trigger.connect(self.ccdmerge)

self.work3.start()

self.work4 = WorkThread()

# 多线程的信号触发连接到ccd1

self.work4.trigger.connect(self.ccd1)

self.work4.start()

def refreshShowa(self):#显示ccd1到label1

# 提取图像的尺寸和通道, 用于将opencv下的image转换成Qimage

height, width, channel = self.img.shape

bytesPerLine = 3 * width

self.qImg = QImage(self.img.data, width, height, bytesPerLine,

QImage.Format_RGB888).rgbSwapped()

# 将Qimage显示出来

self.label.setPixmap(QPixmap.fromImage(self.qImg))

def refreshShowb(self):#显示ccd2到label2

# 提取图像的尺寸和通道, 用于将opencv下的image转换成Qimage

height, width, channel = self.img1.shape

bytesPerLine = 3 * width

self.qImg1 = QImage(self.img1.data, width, height, bytesPerLine,

QImage.Format_RGB888).rgbSwapped()

# 将Qimage显示出来

self.label_2.setPixmap(QPixmap.fromImage( self.qImg1))

def refreshShowc(self):#显示ccd3到label3

# 提取图像的尺寸和通道, 用于将opencv下的image转换成Qimage

height, width, channel = self.img2.shape

bytesPerLine = 3 * width

self.qImg2 = QImage(self.img2.data, width, height, bytesPerLine,

QImage.Format_RGB888).rgbSwapped()

# 将Qimage显示出来

self.label_3.setPixmap(QPixmap.fromImage( self.qImg2))

def refreshShowd(self):#显示ccd4到label4

# 提取图像的尺寸和通道, 用于将opencv下的image转换成Qimage

height, width, channel = self.img3.shape

bytesPerLine = 3 * width

self.qImg3 = QImage(self.img3.data, width, height, bytesPerLine,

QImage.Format_RGB888).rgbSwapped()

# 将Qimage显示出来

self.label_4.setPixmap(QPixmap.fromImage( self.qImg3))

def refreshShowe(self):#显示合并的影像到label6

# 提取图像的尺寸和通道, 用于将opencv下的image转换成Qimage

height, width, channel = self.img4.shape

bytesPerLine = 3 * width

self.qImg4 = QImage(self.img4.data, width, height, bytesPerLine,

QImage.Format_RGB888).rgbSwapped()

# 将Qimage显示出来

self.label_6.setPixmap(QPixmap.fromImage( self.qImg4))

def ccd1(self):

self.cap.set(3, 640)

self.cap.set(4, 480)

ret, self.img = self.cap.read()

self.refreshShowa()

# 启动另一个线程

self.work0.start()#注意一致性

def ccd2(self, str):

self.cap1.set(3, 640)

self.cap1.set(4, 480)

ret, self.img1 = self.cap1.read()

self.refreshShowb()

self.work.start()#注意一致性

def ccd3(self, str):

self.cap2.set(3, 640)

self.cap2.set(4, 480)

ret, self.img2= self.cap2.read()

self.refreshShowc()

self.work2.start()#注意一致性

def ccd4(self, str):

self.cap3.set(3, 640)

self.cap3.set(4, 480)

ret, self.img3 = self.cap3.read()

self.refreshShowd()

self.work3.start()#注意一致性

def ccdmerge(self, str):

self.img4=np.hstack((self.img, self.img1))

self.img4=np.vstack((self.img4, np.hstack((self.img2, self.img3))))

#print ('here is a merge process') 可以用来判断多线程的执行

self.refreshShowe() #later to remove the remark

self.work4.start()#注意一致性

def savemergeimage(self):

# 调用存储文件dialog

fileName, tmp = QFileDialog.getSaveFileName(

self, 'Save Image', './__data', '*.png *.jpg *.bmp', '*.png')

if fileName == '':

return

if self.img.size == 1:

return

# 调用opencv写入图像

cv.imwrite(fileName,self.img4)

class WorkThread(QThread): #多线程核心,非常重要

# 定义一个信号

trigger = pyqtSignal(str)

def __int__(self):

# 初始化函数,默认

super(WorkThread, self).__init__()

def run(self):

self.trigger.emit('')

if __name__ == "__main__":

app = QtWidgets.QApplication(sys.argv)

w = MainWindow()

w.show()

sys.exit(app.exec_())

由于python是一种解释性脚本语言,python的多线程在运行过程中始终存在全局线程锁。简单的来说就是在实际的运行过程中,python只能利用一个线程,因此python的多线程并不达到C语言多线程的性能。可以使用多进程来代替多线程,但需要注意的是多进程最好不要涉及到例如文件操作的频繁操作IO的功能。