多线程
1.线程的概念
多个线程可以同时在一个程序中运行,并且每一个线程完成不同的任务。
传统的程序设计语言同一时刻只能执行单任务操作,效率非常低,如果网络程序在接收数据时发生阻塞(就是管道被堵住了),只能等到程序接收数据之后才能继续运行。随着Internet的飞速发展,这种单任务运行的状况越来越不被接受,如果网络接收数据阻塞,后台服务程序就会一直处于等待状态而不能继续任何操作,这种阻塞情况经常发生,这时的CPU资源完全处于闲置状况。
多线程实现后台服务程序可以同时处理多个任务,并不发生阻塞现象。多线程程序设计的特点就是能够提高程序执行效率和处理速度。Python程序可以同时并行运行多个相对独立的线程。例如,在开发一个Email电邮系统时,通常需要创建一个线程接收数据,另一个线程发送数据,既使发送线程在接收数据时被阻塞,接受数据线程仍然可以运行。
线程(Thread)是CPU分配资源的基本单位。当一个程序开始运行,这个程序就变成了一个进程,而一个进程相当于一个或多个线程。当没有多线程编程时,一个进程也是一个主线程;当有多线程编程时,一个进程包括多个线程(含主线程)。使用线程可以实现程序的并发。
2.创建多线程
Python 3实现多线程的是threading模块,使用它可以创建多线程程序,并且在多线程间进行同步和通信。因为是模块,使用前必须先导入:
import threading
Python支持两种创建多线程的方式:
• 通过threading.Thread()创建。
• 通过继承threading.Thread类创建。
【1】通过threading.Thread()创建
threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
• group:必须为None,与ThreadGroup类相关,一般不使用。
• target:线程调用的对象,就是目标函数。
• name:为线程起个名字。默认是Thread-x,x是序号,由1开始,第一个创建的线程名字就是Thread-1。
• args:为目标函数传递实参,元组。
• kwargs:为目标函数传递关键字参数,字典。
• daemon:用来设置线程是否随主线程退出而退出。
参数虽然很多,但实际常用的就是target和args,下面举例:
【2】通过继承threading.Thread类创建线程
threading.Thread是一个类,可以继承它。下面使用单继承的方式创建一个属于自己的类:
第03行自定义一个类继承自threading.Thread,然后重写父类的run方法,线程启动时(执行start())会自动执行该方法。如果把第10行和第11行的start换为run,会发现run仅仅是被当作一个普通的函数使用。只有在线程start时,它才是多线程的一种调用函数。
ps:Python的线程没有优先级,不能被销毁、停止、挂起,也没有恢复、中断,这与其他基础开发语言不同。
3.主线程
在Python中,主线程是第一个启动的线程。我们需要了解两个概念:
• 父线程:如果线程A中启动了一个线程B,A就是B的父线程。
• 子线程:B就是A的子线程。
创建线程时有一个daemon属性,用它来判断主线程。当daemon设置False时,线程不会随主线程退出而退出,主线程会一直等着子线程执行完;当daemon设置为True时,主线程结束,其他子线程就会被强制结束。
使用daemon属性有几个注意事项:
• daemon属性必须在start()之前设置,否则会引发RuntimeError异常。
• 每个线程都有daemon属性,可以显式设置也可以不设置,不设置则取默认值None。
• 如果子子线程不设置daemon属性,就取当前线程的daemon来设置它。子子线程继承子线程的daemon值,作用和设置None一样。
• 从主线程创建的所有线程不设置daemon属性,则默认都是daemon=False。
例如:
将上述代码保存为thread1.py,然后打开命令行,执行:
python thread1.py
结果如图所示,主线程退出后,子线程也跟着退出了,不会输出0~9。
4.阻塞线程
多线程还提供了一个方法join,简单来说这是一个阻塞线程。一个线程中调用另一个线程的join方法,调用者将被阻塞,直到被调用线程终止。其语法是:
join(timeout=None)
timeout参数指定调用者等待多久,没有设置时,就一直等待被调用线程结束。其中,一个线程可以被join多次。下面是一个例子:
当daemon的值是默认或设置为False时,主线程退出,子线程依然执行。因为子线程当时设置了sleep(),所以先执行主线程的print输出,然后才会输出0~9。
此时,如果在第10行后面添加join方法:
thread1.join()
输出时,主线程就会等待输出完0~9后再执行自己的print输出。
5.线程同步
Python应用程序中的多线程可以共享资源,如文件、数据库、内存等。当线程以并发模式访问共享数据时,共享数据可能会发生冲突。Python引入线程同步的概念,以实现共享数据的一致性。线程同步机制让多个线程有序地访问共享资源,而不是同时操作共享资源。
1.同步的概念
在线程异步模式的情况下,同一时刻有一个线程在修改共享数据,另一个线程在读取共享数据,当修改共享数据的线程没有处理完毕,读取数据的线程肯定会得到错误的结果。如果采用多线程的同步控制机制,当处理共享数据的线程完成处理数据之后,读取线程就读取数据。
基本每种语言都会提供方案来解决这种因同步导致的错误,常用的方案就是“锁”,简单来说,就是锁住线程,只允许一个线程操作,其他线程排队等待,待当前线程操作完毕后,再按排队顺序一个一个来。
2.Python中的锁
Python的threading模块提供了RLock锁(可重入锁)解决方案。在某一时间只能让一个线程操作的语句放到RLock的acquire方法和release方法之间,即acquire相当于给RLock上锁,而release相当于解锁。
代码首先定义了一个类mythread,继承自threading.Thread,然后重写父类的run()方法,当线程启动时自动执行该方法。第08行输出线程名称和x的值。x是全局变量,用global定义,这个变量的作用域是整个代码执行期间,第11行设置x的初始值。
第14~15行使用for…in语句创建5个线程,第17行启动这5个线程,它们都会设置x的值并输出,为了保证输出的正确(读取x的值时不会产生错误),使用lock.acquire()和lock.release()将设置x值和读取x值的语句锁起来,保证了线程的同步,也就是数据的正确性。
3.条件锁
Python的threading提供了一个方法Condition(),一般称为Python中的条件变量。简单地说,这个条件变量必须与一个锁关联,故也可以称为条件锁,它一般用于比较复杂的同步。例如,一个线程在上锁后、解锁前,因为某一条件一直阻塞着,那么锁就一直解不开,此时其他线程也就因为一直获取不了锁而被迫阻塞着,这就可能导致“死锁”现象。这种情况下,变量锁可以让该线程先解锁,然后阻塞,等待条件满足了,再重新唤醒并获取锁(上锁)。这样就不会因为一个线程有问题而影响其他线程了。变量锁的使用方法一般是:
con = threading.Condition()