Python 多线程编程

原创 tiangr  2016-11-26 16:29  阅读 178 次

进程线程

什么是进程?

计算机程序只不过是磁盘中可执行的,二进制(或其它类型)的数据。它们只有在被读取到内存中,被操作系统调用的时候才开始它们的生命期。进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间。进程也可以通过 fork 和 spawn 操作来完成其它的任务。不过各个进程有自己的内存空间,数据栈等,所以只能使用进程间通讯(IPC),而不能直接共享信息。

什么是线程?

线程(有时被称为轻量级进程)跟进程有些相似,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。它们可以想像成是在主进程或“主线程”中并行运行的“迷你进程”。线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占(中断),或暂时的被挂起(也叫睡眠),让其它的线程运行,这叫做让步。一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。线程一般都是并发执行的,正是由于这种并行和数据共享的机制使得多个任务的合作变为可能。实际上,在单 CPU 的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把 CPU 让出来,让其它的线程去运行。在进程的整个运行过程中,每个线程都只做自己的事,在需要的时候跟其它的线程共享运行的结果。当然,这样的共享并不是完全没有危险的。如果多个线程共同访问同一片数据,则由于数据访问的顺序不一样,有可能导致数据结果的不一致的问题。这叫做竞态条件(race condition)。幸运的是,大多数线程库都带有一系列的同步原语,来控制线程的执行和数据的访问。另一个要注意的地方是,由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情况下,这种“贪婪”的函数会让 CPU 的时间分配有所倾斜。导致各个线程分配到的运行时间可能不尽相同,不尽公平。

关于进程与线程的区别。

Python、线程和全局解释器锁

全局解释器锁(GIL)

Python 代码的执行由 Python 虚拟机(也叫解释器主循环)来控制。Python 在设计之初就考虑到要在主循环中,同时只有一个线程在执行,就像单 CPU 的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在 CPU 中运行。同样地,虽然 Python 解释器中可以“运行”多个线程,但在任意时刻,只有一个线程在解释器中运行。对 Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程环境中,Python 虚拟机按以下方式执行:
1. 设置 GIL
2. 切换到一个线程去运行
3. 运行:
a. 指定数量的字节码指令,或者
b. 线程主动让出控制(可以调用 time.sleep(0))
4. 把线程设置为睡眠状态
5. 解锁 GIL
6. 再次重复以上所有步骤
在调用外部代码(如 C/C++扩展函数)的时候,GIL 将会被锁定,直到这个函数结束为止(由于在这期间没有 Python 的字节码被运行,所以不会做线程切换)。编写扩展的程序员可以主动解锁 GIL。不过,Python 的开发人员则不用担心在这些情况下你的 Python 代码会被锁住。
例如,对所有面向 I/O 的(会调用内建的操作系统 C 代码的)程序来说,GIL 会在这个 I/O 调用之前被释放, 以允许其它的线程在这个线程等待 I/O 的时候运行。 如果某线程并未使用很多 I/O 操作,它会在自己的时间片内一直占用处理器(和 GIL)。也就是说,I/O 密集型的 Python 程序比计算密集型的程序更能充分利用多线程环境的好处。

退出线程

当一个线程结束计算,它就退出了。线程可以调用 thread.exit()之类的退出函数,也可以使用Python 退出进程的标准方法,如 sys.exit()或抛出一个 SystemExit 异常等。不过,你不可以直接
“杀掉”("kill")一个线程。

Python 的 threading 模块

Python 提供了几个用于多线程编程的模块,包括 thread, threading 和 Queue 等。thread 和 threading 模块允许程序员创建和管理线程。thread 模块提供了基本的线程和锁的支持,而 threading 提供了更高级别,功能更强的线程管理的功能。Queue 模块允许用户创建一个可以用于多个线程之间 共享数据的队列数据结构。

避免使用 thread 模块

出于以下几点考虑,我们不建议您使用 thread 模块。

首先,更高级别的 threading 模块更为先 进, 对线程的支持更为完善, 而且使用 thread 模块里的属性有可能会与 threading 出现冲突。

其次, 低级别的 thread 模块的同步原语很少(实际上只有一个),而 threading 模块则有很多。 另一个不要使用 thread 原因是,对于你的进程什么时候应该结束完全没有控制,当主线程结束 时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作。至少 threading 模块能确保重要的子线程退出后进程才退出。

thread 模块

除了产生线程外,thread 模块也提供了基本的同步数 据结构锁对象(lock object,也叫原语锁,简单锁,互斥锁,互斥量,二值信号量)。如之前所说, 同步原语与线程的管理是密不可分的。start_new_thread()函数是 thread 模块的一个关键函数,它的语法与内建的 apply()函数完全 一样,其参数为:函数,函数的参数以及可选的关键字参数。不同的是,函数不是在主线程里运行, 而是产生一个新的线程来运行这个函数。

函数描述

函数 描述
thread 模块函数
start_new_thread(function,args, kwargs=None) 产生一个新的线程,在新线程中用指定的参数和可选的 kwargs 来调用这个函数
allocate_lock() 分配一个 LockType 类型的锁对象
exit() 让线程退出
LockType 类型锁对象方法
acquire(wait=None) 尝试获取锁对象
locked() 如果获取了锁对象返回 True,否则返回 False
release() 释放锁

未使用多线程编程:

  1. # -*- coding: UTF-8 -*-
  2. from time import sleep, ctime
  3. def loop0():
  4.     print('start loop 0 at:', ctime())
  5.     sleep(4)
  6.     print('loop 0 done at:', ctime())
  7. def loop1():
  8.     print('start loop 1 at:', ctime())
  9.     sleep(2)
  10.     print('loop 1 done at:', ctime())
  11. def main():
  12.     print('starting at:', ctime())
  13.     loop0()
  14.     loop1()
  15.     print('all DONE at:', ctime())
  16. if __name__ == '__main__':
  17.     main()

输出结果:

  1. starting at: Fri Dec  2 11:42:25 2016
  2. start loop 0 at: Fri Dec  2 11:42:25 2016
  3. loop 0 done at: Fri Dec  2 11:42:29 2016
  4. start loop 1 at: Fri Dec  2 11:42:29 2016
  5. loop 1 done at: Fri Dec  2 11:42:31 2016
  6. all DONE at: Fri Dec  2 11:42:31 2016

代码示例1 :

  1. # -*- coding: UTF-8 -*-
  2. import _thread
  3. from time import sleep, ctime
  4. def loop0():
  5.     print('start loop 0 at:', ctime())
  6.     sleep(4)
  7.     print('loop 0 done at:', ctime())
  8. def loop1():
  9.     print('start loop 1 at:', ctime())
  10.     sleep(2)
  11.     print('loop 1 done at:', ctime())
  12. def main():
  13.     print('starting at:', ctime())
  14.     _thread.start_new_thread(loop0, ())
  15.     _thread.start_new_thread(loop1, ())
  16.     sleep(6)
  17.     print('all DONE at:', ctime())
  18. if __name__ == '__main__':
  19.     main()

start_new_thread()要求一定要有前两个参数。所以,就算我们想要运行的函数不要参数,我们也要传一个空的元组。输出结果:

  1. starting at: Fri Dec  2 11:37:25 2016
  2. start loop 1 at: Fri Dec  2 11:37:25 2016
  3. start loop 0 at: Fri Dec  2 11:37:25 2016
  4. loop 1 done at: Fri Dec  2 11:37:27 2016
  5. loop 0 done at: Fri Dec  2 11:37:29 2016
  6. all DONE at: Fri Dec  2 11:37:31 2016

睡眠 4 秒和 2 秒的代码现在是并发执行的。这样,就使得总的运行时间被缩短了。你可以看到,loop1 甚至在 loop0 前面就结束了。程序的一大不同之处就是多了一个“sleep(6)”的函数调用。为什么要加上这一句呢?因为,如果我们没有让主线程停下来,那主线程就会运行下一条语句,显示“all done”,然后就关闭运行着 loop0()和 loop1()的两个线程,退出了。我们没有写让主线程停下来等所有子线程结束之后再继续运行的代码。这就是我们之前说线程需要同步的原因。在这里,我们使用了 sleep()函数做为我们的同步机制。我们使用 6 秒是因为我们已经知道,两个线程(你知道,一个要 4 秒,一个要 2 秒)在主线程等待 6 秒后应该已经结束了。你也许在想,应该有什么好的管理线程的方法,而不是在主线程里做一个额外的延时 6 秒的操作。因为这样一来,我们的总的运行时间并不比单线程的版本来得少。而且,像这样使用 sleep()函数做线程的同步操作是不可靠的。如果我们的循环的执行时间不能事先确定的话,那怎么办呢?这可能造成主线程过早或过晚退出。这就是锁的用武之地了。

使用线程和锁,代码示例2:

  1. # -*- coding: UTF-8 -*-
  2. import _thread
  3. from time import sleep, ctime
  4. loops = [4, 2]
  5. def loop(nloop, nsec, lock):
  6.     """
  7.     循环函数,在函数中记录下循环的号码和要睡眠的时间。
  8.     :param nloop: 循环序号
  9.     :param nsec: 休眠时间
  10.     :param lock: 对象锁
  11.     :return: 每个线程都会被分配一个事先已经获得的锁,在 sleep()的时间到了之后就释放相应的锁以通知主线程,这个线程已经结束了。
  12.     """
  13.     print('start loop', nloop, 'at:', ctime())
  14.     sleep(nsec)
  15.     print('loop', nloop, 'done at:', ctime())
  16.     lock.release()
  17. def main():
  18.     print('starting at:', ctime())
  19.     locks = []  # 创建一个锁的列表
  20.     nloops = range(len(loops))
  21.     for i in nloops:
  22.         lock = _thread.allocate_lock()  # 调用 thread.allocate_lock()函数创建一个锁的列表
  23.         lock.acquire()  # 分别调用各个锁的 acquire()函数获得锁。获得锁表示“把锁锁上”。
  24.         locks.append(lock)  # 锁上后,我们就把锁放到锁列表 locks 中。
  25.     """
  26.     为什么我们不在创建锁的循环里创建线程呢?有以下几个原因: (1) 我们想到实现线程的同步,所以要让“所有的马同时冲出栅栏”。(2) 获取锁要花一些时间,如果你的线程退出得“太快”,可能会导致还没有获得锁,线程就已经结束了的情况。
  27.     """
  28.     for i in nloops:    # 循环创建线程,每个线程都用各自的循环号,睡眠时间和锁为参数去调用 loop()函数。
  29.         _thread.start_new_thread(loop, (i, loops[i], locks[i]))
  30.     """
  31.     在线程结束的时候,线程要自己去做解锁操作。最后一个循环只是坐在那一直等(达到暂停主线程的目的),直到两个锁都被解锁为止才继续运行。
  32.     由于我们顺序检查每一个锁,所以我们可能会要长时间地等待运行时间长且放在前面的线程,当这些线程的锁释放之后,后面的锁可能早就释放了(表示对应的线程已经运行完了)。结果主线程只能毫不停歇地完成对后面这些锁的检查。
  33.     """
  34.     for i in nloops:
  35.         while locks[i].locked():
  36.             pass
  37.     print('all DONE at:', ctime())
  38. if __name__ == '__main__':
  39.     main()

*使用threading模块

threading 模块对象 描述
Thread 表示一个线程的执行的对象
Lock 锁原语对象(跟 thread 模块里的锁对象相同)
RLock 可重入锁对象。使单线程可以再次获得已经获得了的锁(递归锁定)。
Condition 条件变量对象能让一个线程停下来, 等待其它线程满足了某个 “条件”。如,状态的改变或值的改变。
Event 通用的条件变量。多个线程可以等待某个事件的发生,在事件发生后,所有的线程都会被激活。
Semaphore 为等待锁的线程提供一个类似“等候室”的结构
BoundedSemaphore 与 Semaphore 类似,只是它不允许超过初始值。
Timer 与 Thread 相似,只是,它要等待一段时间后才开始运行。

守护线程

另一个避免使用 thread 模块的原因是,它不支持守护线程。当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。有时,我们并不期望这种行为,这时,就引入了守护线程的概念threading 模块支持守护线程, 它们是这样工作的: 守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求,它就在那等着。如果你设定一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。 就像在网络编程,服务器线程运行在一个无限循环中,一般不会退出。

如果你的主线程要退出的时候,不用等待那些子线程完成,那就设定这些线程的 daemon 属性。即,在线程开始(调用 thread.start())之前, 调用 setDaemon()函数设定线程的 daemon 标志(thread.setDaemon(True)) 就表示这个线程“不重要”。如果你想要等待子线程完成再退出 , 那就什么都不用做 , 或者显式地调用thread.setDaemon(False)以保证其 daemon 标志为 False。 你可以调用 thread.isDaemon()函数来判断其 daemon 标志的值。新的子线程会继承其父线程的 daemon 标志。整个 Python 会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。

Thread 类

用 Thread 类,你可以用多种方法来创建线程。

Thread 对象的函数:

函数 描述
start() 开始线程的执行
run() 定义线程的功能的函数(一般会被子类重写)
join(timeout=None) 程序挂起,直到线程结束;如果给了 timeout,则最多阻塞 timeout 秒
getName() 返回线程的名字
setName(name) 设置线程的名字
isAlive() 布尔标志,表示这个线程是否还在运行中
isDaemon() 返回线程的 daemon 标志
setDaemon(daemonic) 把线程的 daemon 标志设为 daemonic (一定要在调用 start()函数前调用)

使用 threading 模块

1.创建一个 Thread 的实例,传给它一个函数

  1. # -*- coding: UTF-8 -*-
  2. import threading
  3. from time import sleep, ctime
  4. loops = [4, 2]
  5. def loop(nloop, nsec):
  6.     print('start loop', nloop, 'at:', ctime())
  7.     sleep(nsec)
  8.     print('loop', nloop, 'done at:', ctime())
  9. def main():
  10.     print('starting at:', ctime())
  11.     threads = []
  12.     nloops = range(len(loops))
  13.     for i in nloops:
  14.         t = threading.Thread(target=loop, args=(i, loops[i]))
  15.         threads.append(t)
  16.     for i in nloops:    # 启动线程
  17.         threads[i].start()
  18.     for i in nloops:    # 等待所有的线程结束
  19.         threads[i].join()   # threads to finish
  20.     print('all DONE at:', ctime())
  21. if __name__ == '__main__':
  22.     main()

所有的线程都创建了之后,再一起调用 start()函数启动,而不是创建一个启动一个。而且,不用再管理一堆锁 (分配锁, 获得锁, 释放锁, 检查锁的状态等), 只要简单地对每个线程调用 join()函数就可以了。join()会等到线程结束,或者在给了 timeout 参数的时候,等到超时为止。使用 join()看上去会比使用一个等待锁释放的无限循环清楚一些(这种锁也被称为"spinlock")join()的另一个比较重要的方面是它可以完全不用调用。一旦线程启动后,就会一直运行,直到线程的函数结束,退出为止。如果你的主线程除了等线程结束外,还有其它的事情要做(如处理或等待其它的客户请求), 那就不用调用 join(), 只有在你要等待线程结束的时候才要调用 join()。

2.创建一个 Thread 的实例,传给它一个可调用的类对象
与传一个函数很相似的另一个方法是在创建线程的时候,传一个可调用的类的实例供线程启动的时候执行——这是多线程编程的一个更为面向对象的方法。

  1. # -*- coding: UTF-8 -*-
  2. import threading
  3. from time import sleep, ctime
  4. loops = [4, 2]
  5. class ThreadFunc(object):
  6.     def __init__(self, func, args, name=''):
  7.         self.name = name
  8.         self.func = func
  9.         self.args = args
  10.     def __call__(self):
  11.         self.func(*self.args)
  12. def loop(nloop, nsec):
  13.     print('start loop', nloop, 'at:', ctime())
  14.     sleep(nsec)
  15.     print('loop', nloop, 'done at:', ctime())
  16. def main():
  17.     print('starting at:', ctime())
  18.     threads = []
  19.     nloops = range(len(loops))
  20.     for i in nloops:    # create all threads
  21.         t = threading.Thread(target=ThreadFunc(loop, (i, loops[i]), loop.__name__))
  22.         threads.append(t)
  23.     for i in nloops:    # start all threads
  24.         threads[i].start()
  25.     for i in nloops:    # wait for completion
  26.         threads[i].join()
  27.     print('all DONE at:', ctime())
  28. if __name__ == '__main__':
  29.     main()

3.从 Thread 派生出一个子类,创建一个这个子类的实例.

子类化 Thread:

  1. # -*- coding: UTF-8 -*-
  2. import threading
  3. from time import sleep, ctime
  4. loops = (4, 2)
  5. class MyThread(threading.Thread):
  6.     def __init__(self, func, args, name=''):
  7.         threading.Thread.__init__(self)
  8.         self.name = name
  9.         self.func = func
  10.         self.args = args
  11.     def run(self):
  12.         """
  13.         定义线程的功能的函数
  14.         :return:
  15.         """
  16.         self.func(*self.args)
  17. def loop(nloop, nsec):
  18.     print('start loop', nloop, 'at:', ctime())
  19.     sleep(nsec)
  20.     print('loop', nloop, 'done at:', ctime())
  21. def main():
  22.     print('starting at:', ctime())
  23.     threads = []
  24.     nloops = range(len(loops))
  25.     for i in nloops:
  26.         t = MyThread(loop, (i, loops[i]), loop.__name__)
  27.         threads.append(t)
  28.     for i in nloops:
  29.         threads[i].start()
  30.     for i in nloops:
  31.         threads[i].join()
  32.     print('all DONE at:', ctime())
  33. if __name__ == '__main__':
  34.     main()

为了让Thread 的子类更为通用,我们把子类单独放在一个模块中,并加上一 个 getResult()函数用以返回函数的运行结果。

斐波那契,阶乘和累加和

  1. # -*- coding: UTF-8 -*-
  2. import threading
  3. from time import ctime
  4. class MyThread(threading.Thread):
  5.     def __init__(self, func, args, name=''):
  6.         threading.Thread.__init__(self)
  7.         self.func = func
  8.         self.args = args
  9.         self.name = name
  10.         self.res = ''
  11.     def get_result(self):
  12.         return self.res
  13.     def run(self):
  14.         print('starting', self.name, 'at:', ctime())
  15.         self.res = self.func(*self.args)
  16.         print(self.name, 'finished at:', ctime())

引入Thread子类模块:

  1. # -*- coding: UTF-8 -*-
  2. from myThread import MyThread
  3. from time import sleep, ctime
  4. def fib(x):
  5.     sleep(0.005)
  6.     if x < 2:
  7.         return 1
  8.     return fib(x-2) + fib(x-1)
  9. def fac(x):
  10.     sleep(0.1)
  11.     if x < 2:
  12.         return 1
  13.     return x * fac(x-1)
  14. def plus(x):
  15.     sleep(0.1)
  16.     if x < 2:
  17.         return 1
  18.     return x + plus(x-1)
  19. funcs = [fib, fac, plus]
  20. n = 15
  21. def main():
  22.     nfuncs = range(len(funcs))
  23.     print('*** SINGLE THREAD')
  24.     for i in nfuncs:
  25.         print('starting', funcs[i].__name__, 'at:', ctime())
  26.         print(funcs[i](n))
  27.         print(funcs[i].__name__, 'finished at:', ctime())
  28.     print('\n*** MULTIPLE THREADS')
  29.     threads = []
  30.     for i in nfuncs:
  31.         t = MyThread(funcs[i], (n, ), funcs[i].__name__)
  32.         threads.append(t)
  33.     for i in nfuncs:
  34.         threads[i].start()
  35.     for i in nfuncs:
  36.         threads[i].join()
  37.         print(threads[i].get_result())
  38.     print('all DONE')
  39. if __name__ == '__main__':
  40.     print('start fib:', ctime())
  41.     main()

结果分析:

  1. start fib: Fri Dec  2 17:20:42 2016
  2. *** SINGLE THREAD
  3. starting fib at: Fri Dec  2 17:20:42 2016
  4. 987
  5. fib finished at: Fri Dec  2 17:21:12 2016
  6. starting fac at: Fri Dec  2 17:21:12 2016
  7. 1307674368000
  8. fac finished at: Fri Dec  2 17:21:14 2016
  9. starting plus at: Fri Dec  2 17:21:14 2016
  10. 120
  11. plus finished at: Fri Dec  2 17:21:15 2016
  12. *** MULTIPLE THREADS
  13. starting fib at: Fri Dec  2 17:21:15 2016
  14. starting fac at: Fri Dec  2 17:21:15 2016
  15. starting plus at: Fri Dec  2 17:21:15 2016
  16. fac finished at: Fri Dec  2 17:21:17 2016
  17. plus finished at: Fri Dec  2 17:21:17 2016
  18. fib finished at: Fri Dec  2 17:21:46 2016
  19. 987
  20. 1307674368000
  21. 120
  22. all DONE

在单线程中运行只要简单地逐个调用这些函数,在函数结束后,显示对应的结果。在多线程中,我们不马上显示结果。由于我们想让 MyThread 类尽可能地通用(能同时适应有输出和没输出的函数),我们会等到要结束时才会调用 get_result()函数,并在最后显示每个函数的结果。

由于这些函数运行得很快(斐波那契函数会慢一些),你会看到,我们得在每个函数中加上一个 sleep()函数,让函数慢下来,以便于我们能方便地看到多线程能在多大程度上加速程序的运行。不过实际工作中,你一般不会想在程序中加上 sleep()函数的。

threading 模块中的其它函数

函数 描述
activeCount() 当前活动的线程对象的数量
currentThread() 返回当前线程对象
enumerate() 返回当前活动线程的列表
settrace(func)a 为所有线程设置一个跟踪函数
setprofile(func)a 为所有线程设置一个 profile 函数

生产者-消费者问题和 Queue 模块

最后一个例子演示了生产者和消费者的场景。生产者生产货物,然后把货物放到一个队列之类的数据结构中,生产货物所要花费的时间无法预先确定。消费者消耗生产者生产的货物的时间也是不确定的。

函数 描述
Queue 模块函数
queue(size) 创建一个大小为 size 的 Queue 对象
Queue 对象函数
qsize() 返回队列的大小(由于在返回的时候,队列可能会被其它线程修改,所以这个值是近似值)
empty() 如果队列为空返回 True,否则返回 False
full() 如果队列已满返回 True,否则返回 False
put(item,block=0) 把 item 放到队列中,如果给了 block(不为 0),函数会一直阻塞到队列中有空间为止
get(block=0) 从队列中取一个对象,如果给了 block(不为 0),函数会一直阻塞到队列中有对象为止

Queue 模块可以用来进行线程间通讯,让各个线程之间共享数据。现在,我们创建一个队列,让生产者(线程)把新生产的货物放进去供消费者(线程)使用。

  1. # -*- coding: UTF-8 -*-
  2. from random import randint
  3. from time import sleep
  4. import queue
  5. from myThread import MyThread
  6. def write_queue(q):
  7.     print('producing object for Q...', q.put('xxx', 1))
  8.     print('size now', q.qsize())
  9. def read_queue(q):
  10.     val = q.get(1)
  11.     print('consumed object from Q... size now', q.qsize())
  12. def writer(q, loops):
  13.     for i in range(loops):
  14.         write_queue(q)
  15.         sleep(randint(16))
  16. def reader(q, loops):
  17.     for i in range(loops):
  18.         read_queue(q)
  19.         sleep(randint(25))
  20. funcs = [writer, reader]
  21. nfuncs = range(len(funcs))
  22. def main():
  23.     nloops = randint(28)
  24.     q = queue.Queue(32)
  25.     threads = []
  26.     for i in nfuncs:
  27.         t =MyThread(funcs[i], (q, nloops), funcs[i].__name__)
  28.         threads.append(t)
  29.     for i in nfuncs:
  30.         threads[i].start()
  31.     for i in nfuncs:
  32.         threads[i].join()
  33.     print('ALL DONE.')
  34. if __name__ == '__main__':
  35.     main()
特别提示:本站资源全部免费下载,因服务器需经费维护,文中部分外链点击后会进入广告,请耐心等待5秒即可跳过广告进入目标页面。如遇页面外链打不开或下载地址失效,您可以在评论中指出错误,或扫描页面底部二维码。
本文地址:http://www.tiangr.com/python-duo-xian-cheng-bian-cheng.html
版权声明:本文为原创文章,版权归 tiangr 所有,欢迎分享本文,转载请保留出处!

发表评论


表情