当需要处理多任务时,多线程编程比起多进程编程往往更有优势,每一个进程都至少有一个线程,一个进程多个线程就可以实现多个任务。线程是操作系统直接支持的执行单元。许多高级语言都支持多线程编程,Python也提供了多个模块支持多线程编程,包括_thread、threading、Queue等模块。
而想要创建一个线程,则需要用到其中的_thread、或者threading模块。
Python标准库中有两个模块提供对创建线程的支持,其中_thread模块是低级模块,功能十分有限。它在Python旧版本原叫做thread模块,后被更名为_thread,意为尽量在多线程编程中不要使用该模块。
而threading模块是高级模块,提供了丰富的多线程功能支持。
本篇文章主要介绍threading模块,对_thread模块感兴趣的可以私下了解。
创建线程的方法与创建进程的方法很像,创建进程时通过实例化multiprocessing模块下的Process对象,或者派生Process的子类,重写其run()方法创建进程,这两种方法与创建线程的方式非常像。
如果有兴趣的可以看我前两篇关于实现多进程的文章。
Thread类对象属性:
Thread类的构造方法的参数如下:
Thread(self,group=None,target=None,name=None,args=(),kwargs={},*,daemon=None)
示例:
import threading
def text():
print('thread %s is running.' % (threading.current_thread().name)
print('thread %s end.' % (threading.current_thread().name))
print('thread %s is running.' % (threading.current_thread().name))
t = threading.Thread(target=text)
t.start()
t.join()
print('thread %s end.' % (threading.current_thread().name))
运行结果:
thread MainThread is running.
thread Thread-1 is running.
thread Thread-1 end.
thread MainThread end.
<分析>
这个例子比较简单,只是简单的展示了创建线程的方法。每一个进程都有一个主线程,主线程是默认启动的,于是可以看到我们只实例化Thread类创建了一个线程,但运行结果却有两个线程。
另外可以发现每一个线程都有一个name,比方说MainThread(主线程)、Thread-1,这个name是哪来的呢?threading模块内有一个current_thread()函数,该函数总是返回当前线程的实例,那么current_thread().name不就是返回当前线程实例的name了么。另外,主线程默认的name就是MainThread,子线程如果没有在创建时指定,那么默认就是Thread-1、Thread-2…以此类推。
另外,启动一个线程的start()方法、用以表示现线程活动的run()方法、等待一个线程结束的join(timeout)方法,判断线程是否"存活"的is_alive()方法…这些方法都和进程中的这些方法完全类似,鉴于有关进程我已经在前两篇介绍过,这里就不再细说。
唯一提一点就是,进程中关于强制终止进程的terminate()方法不适用于Thread对象,唯独这一个不要在线程中去用。
最后,threading模块下除current_thread()之外的其他常用函数:
Thread类的一些其它方法:
以上方法都已经不常用,在Python的新版本中,设定name或者daemon属性可以不借助方法直接设定。
进程和线程都可以并发执行,但线程由于划分尺度小,因此并发性相比于进程是要高的;另外,创建线程要更加简单,也更易于维护,由于多个线程共享进程的内存空间,这也使得线程更易于通信;除此之外,线程是Python直接支持的,这也简化了多线程的编程。
Python中的多线程也并不全是优点,事实上,Python中的多线程只是表面上的多线程,它和C++或者java中的不同,由于全局解释器锁GIL的存在,Python在一个时刻只会有一个线程在运行,这只是并发而不是并行。所以同样是多线程,java可以充分利用CPU的多核,但Python自始至终只会用到一个核,这就像单核CPU系统的多进程一样,Python的多线程是名不副实的。
这里展示一个多线程并发执行的例子:
import threading
from time import sleep
def text(n):
for i in range(n):
print('thread %s is running >> %d' % (threading.current_thread().name,i))
if i == n-1:
print('thread %s end.' % (threading.current_thread().name))
break
sleep(2)
print('thread %s is running.' % (threading.current_thread().name))
t1 = threading.Thread(target=text,args=(4,))
t2 = threading.Thread(target=text,args=(4,))
t1.start()
t2.start()
t1.join()
t2.join()
print('thread %s end' % (threading.current_thread().name))
运行结果:
thread MainThread is running.
thread Thread-1 is running >> 0
thread Thread-2 is running >> 0
thread Thread-2 is running >> 1
thread Thread-1 is running >> 1
thread Thread-2 is running >> 2
thread Thread-1 is running >> 2
thread Thread-2 is running >> 3
thread Thread-2 end.
thread Thread-1 is running >> 3
thread Thread-1 end.
thread MainThread end
<分析>
这个示例中我创建了两个子线程,并且每个子线程都调用text()函数,为了更好的体现并发,我在text()函数中设置了一个循环,按照我们以往程序顺序执行的思想,一个程序调用text()函数,循环没有结束那么该程序也将无法跳出循环。可是从运行结果中我们看到,线程一和线程二在交替执行,为了让这个效果放大,我在循环中加了个休眠,之后就正如我们发现的,两个线程似乎在同时进行一样,并不存在说一个线程结束之后才轮到下一个线程开始执行的情况。
这就是一个非常明显的多线程并发执行的例子。在执行并发时,CPU以快速轮换的方式交替执行每一个线程,从而给使用者一种错觉,即感觉像是所有线程在一起运行,实则不然,但所有的线程确实是在很短的时间内全部启动了。
通过继承Thread,重写Thread类的run()方法得到Thread类的子类,实例化子类也能创建线程。
示例:
import threading
class Another_thread(threading.Thread):
def __init__(self):
super().__init__()
def run(self):
print('thread %s is running.' % (threading.current_thread().name))
print('thread %s end.' % (threading.current_thread().name))
print('thread %s is running.' % (threading.current_thread().name))
t = Another_thread()
t.start()
t.join()
print('thread %s end.' % (threading.current_thread().name))
运行结果:
thread MainThread is running.
thread Thread-1 is running.
thread Thread-1 end.
thread MainThread end.
<分析>
如上,通过重写Thread类的run()方法,就可以在接下来直接使用Thread的子类创建一个线程,当然也可以重写Thread类的__init__()方法,得到一个令人满意、功能完备的Thread类的子类。这种创建线程的方式更加适合面向对象编程。
我们一般更倾向于使用上面介绍的两种方法创建线程,在第一种方法中,我们实例化Thread类,并传给它一个函数,第二种方法通过继承Thread得到子类创建线程,而接下来介绍的第三种方法,你会发现传入一个可调用的类实例同样可以创建线程。
这种创建线程的方法更加接近于面向对象的多线程编程,相比于传入函数而言具有更高的灵活性。
示例:
import threading
from time import sleep
loops = [1,2,3]
class MyThread(object):
def __init__(self,func,args,name=''):
self.name = name
self.func = func
self.args = args
def __call__(self):
self.func(*self.args)
def text(x,sp):
print('thread %s is running >> %d.' % (threading.current_thread().name,x))
sleep(sp)
print('thread %s end.' % (threading.current_thread().name))
def main():
print('START!')
threads = []
for i in range(3):
t = threading.Thread(target=MyThread(text,(i,loops[i])))
threads.append(t)
for i in range(3):
threads[i].start()
for i in range(3):
threads[i].join()
print('ALL END!')
if __name__ == '__main__':
main()
运行结果:
START!
thread Thread-1 is running >> 0.
thread Thread-2 is running >> 1.
thread Thread-3 is running >> 2.
thread Thread-1 end.
thread Thread-2 end.
thread Thread-3 end.
ALL END!
<分析>
这种创建线程的方法比起前两种应该稍微复杂了一点,但实际上也就是相比于传入函数改为传入一个类,类完全由我们自己定制,构造方法__init__()可以添加任何我们需要的参数,之后注意这个特殊方法__call__(),它调用传入类的函数,并使用我们传入类的参数。
如果我们力求逻辑清晰地快速创建一个线程,任务要求不高,我们倾向于使用第一种创建线程的方法;如果我们需要完全面向对象的多线程编程,我们倾向于使用继承Thread类、实例化子类创建线程的方法,所以这第三种方法的地位有点尴尬,它并不被常用,这里了解一下知道就好。