前言

  最近开发 mpdb 插件,研究多线程加载插件 UI ,避免 Maya 主界面卡死的问题。
  最初测试的时候,不知道是哪里操作不对,我对比测试发现多线程操作和不使用多线程的体验是一样的。

  后来插件顺利发布之后,我打算还是再重新研究一下,看看能否抢救一下。
  结果这一次的测试就有了很大的收获。

多线程的意义

  其实 Python 本身是存在 GIL 全局锁,因此性能瓶颈一直是 Python 的坑。
  当然GIL锁死了多核调用,面对 CPU 密集型的任务时会显得力不从心。
  Maya 也因为自身框架设计问题,很多调用都是非 threadsafe 的,只有一些特殊的计算节点可以完美调用多核来提高性能 具体参照文章
  虽然多核调度很麻烦,但是多线程调度则是 Python 的强项,这种特别时候。

  以下是我个人目前的看法,有错之处欢迎大家指正。
  Python 可以调用不同的线程使任务并行响应,其实本质上的算力并没有提高,还是只有一个核的运算。
  只是多任务交替执行,由于计算机的运算速度够快,给我们的感觉任务是同时执行的。
  但是交互体验上则会有巨大的提升。

Python 多线程 Demo

  不逼逼,上代码~

1
2
3
4
5
6
7
8
9
10
import time
curr = time.time()
while True:
elapsed = abs(time.time() - curr)
# NOTE 每隔 0.5s 打印输出时间
if elapsed%0.5 < 0.001:
print elapsed
if elapsed > 3:
break
print "done"

alt

  如果直接在 Maya 里面运行上面的代码。
  由于 while 死循环的设置, Maya 将会直接卡死 3s 钟并且输出对应的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sys
import time
import threading

def processFunc(runCallback=None):
curr = time.time()
while True:
elapsed = abs(time.time() - curr)
if elapsed%0.5 < 0.001:
if runCallback:
runCallback(elapsed)
if elapsed > 3:
break

def porcessPyThread(blocking=False):
thread = threading.Thread(target=processFunc,kwargs={"runCallback":lambda t: sys.stdout.write("PyThread: %s \n" % t)})
thread.start()
if blocking:
thread.join()

porcessPyThread()
print "done"

  如果将代码改为多线程调用,那么多线程的部分可以看做是异步调用代码
  执行上面的代码可以看到 done 会先被执行,和 JavaScript 的异步有异曲同工之妙(可能就是一样的)

  因此问题也类似,虽然多线程不会阻塞到主线程,但是如果想获取到多线程返回的数据,就需要等待线程执行完毕了。
  因此 python thread 提供了 join 方法,可以让主线程等待其他线程执行完毕获取到数据。 代码参考


  多线程不适合加入 CPU 密集型的操作,会影响到主线程调用。
  如果我们调整 while 循环的打印输出条件, print 打印会更急密集,达到一定程度之后会卡到主线程的。

alt
alt

Qt 多线程

  除了 Python 本身实现多线程调度之外,也可以利用 Qt 的多线程来实现UI响应。
  而且关于 Qt GUI 相关的部分,网上更加推荐使用 Qt 的模块来做。

  不过我查过,Qt 和 Python 多线程的底层 在windows下 都是调用了 Win32 相关的模块实现的。
  除了 Qt 提供了更多函数,对 Qt 组件更加友好之外,其实两者也没有太大的区别。

文件参考

  上面这个脚本就是我测试 Maya 和 Qt 多线程的两个脚本,其实运行体验差不多的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import sys
import time
import threading
from PySide2 import QtCore
from PySide2 import QtWidgets

def processFunc(runCallback=None):
curr = time.time()
while True:
elapsed = abs(time.time() - curr)
if elapsed%0.5 < 0.001:
if runCallback:
runCallback(elapsed)
if elapsed > 3:
break

class Worker(QtCore.QRunnable):

def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
processFunc(lambda t: sys.stdout.write("QtThread: %s \n" % t))


def processQtThread(blocking=False):
thread = QtCore.QThreadPool()
thread.start(Worker())
if blocking:
thread.waitForDone()

return thread

if __name__ == "__main__":
# NOTE 避免垃圾回收
thread = processQtThread()
print "done"

  当然 Qt 也麻烦一点,需要避免 Python 对自身的垃圾回收,否则 thread 的执行将不太正常。

多线程提升交互体验

  下面就回归到正题,通过上面 demo 可以看到。
  我们可以在 Maya 里面执行一些其他的操作,同时还保持 Maya 主界面的响应。
  我在想 UI 创建的过程是否也可以这样,插件界面在后台生成,生成好了之后再返回到Maya的主界面上执行显示操作。
  这样就可以不卡主 Maya 的同时生成出插件界面。


  想法是很美好,但是实验过后,现实很骨感。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import sys, time
from PyQt5 import QtGui
from PyQt5 import QtCore
from PyQt5 import QtWidgets

app = QtWidgets.QApplication(sys.argv)
class widget(QtWidgets.QWidget):
signal = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
QtWidgets.QWidget.__init__(self)
# self.button = QtWidgets.QPushButton(u"clicked",self)

def appinit(self):
thread = worker(self)
thread.start()

def testfunc(self):
print "clicked"

class worker(QtCore.QThread):

def __init__(self,widget):
QtCore.QThread.__init__(self, parent=app)
self.widget = widget

def run(self):
print "in thread"
# NOTE 在另一个线程中创建 UI
self.widget.button = QtWidgets.QPushButton(u"clicked",self.widget)
self.widget.button.setText("hello")
self.widget.button.clicked.connect(self.widget.testfunc)

def main():
w = widget()
w.show()
w.appinit()
sys.exit(app.exec_())

if __name__ == "__main__":
main()

  这段代码需要在 Maya 外执行,我是使用 anaconda2 测试的。
  在 Maya 里面切换成 PySide2 执行的话,会发现没有报错,但是另一个线程创建 按钮 并没有依附到显示的窗口上。
  当我在Maya外部执行的时候,命令行会出现这个警告。

QObject::setParent: Cannot set parent, new parent is in a different thread

  经过网上的搜索, Stack Overflow 的回答非常到位 链接
  Qt 的 Gui 创建在 __init__ 函数执行的时候会依附到当前的线程上,如果当前的线程不是主线程,那么这个 UI 就无法集中管理了。
  尽管 Qt 的 QObject 提供了 moveToThread 方法,但是并不是所有的操作都是 threadsafe ,特别是 Maya 这种古老框架 对多线程支持不太友好。
  因此 Gui 创建必须在主线程上,但是一些 Gui 更新、初始化的操作可以放到其他线程执行。


  因此 mpdb 插件基于这样的考量,将大部分 __init__ 函数执行的代码抽到 initialize 函数中去,通过多线程完成 UI 的初始化。
  这样的确就解决了 Maya 卡死 2-3s 的问题。

遇到的一些问题

  mpdb 模块修改成多线程之后还是有坑的。

  比如 Maya 的 cmds 模块所有命令 q 标签都出错了,需要改为 query 标签才不会报错,我是在 Maya2017 的版本测试的,原因不明。

  另外就是 partial 函数在多线程模式下不受支持,替代方案需要 Qt 的 Signal 来解决,的确会繁琐一点。 链接

  最后就是如何让 UI 初始化完成之后再生成 UI。
  因为不可以使用 waitForDone 或者 join 方法来让主线程等待,这样就失去多线程的意义了。
  原本想着非常棘手,好在 Maya 已经提供了 UI 生成的 队列机制。
  在线程的函数里面,完成初始化代码之后加入 cmds.evalDeferred 来生成 UI 就可以实现在 Maya 的主线程上实现 UI 的展示了。

总结

  这次多线程的探索又有新的收获,进一步学习了 Qt 框架的一些知识,挺好的。