线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。
多任务可以由多进程完成,也可以由一个进程内的多线程完成。由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix(移植性操作系统接口) Thread,而不是模拟出来的线程。
python的标准库提供了两个模块:thread 和 threading
- thread:是低级模块
- threading:是高级模块,对thread进行了封装。绝大情况,我们只需使用threading模块。
一、创建线程:使用threading模块
单线程执行
import time
def say():
print('hello man,')
time.sleep(1)
if __name__ == '__main__':
for i in range(5):
say()
运行结果:
多线程执行(函数)
import threading
import time
def mul_thread():
print('实现多线程')
time.sleep(1)
if __name__=='__main__':
for i in range(5):
t = threading.Thread(target=mul_thread)
t.start() #启动线程,即让线程开始执行
运行结果:
说明:
- 可以明显看出使用了多线程并发的操作,花费时间要短很多
- 当调用start()时,才会真正的创建线程,并且开始执行
继承实现多线程(threading.Thread):继承Thread类,重写run()方法,使用start()启动线程
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(3):
time.sleep(1)
msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
print(msg)
if __name__ == '__main__':
t = MyThread()
t.start() #启动线程
注意:
用继承实现多线程要重写run()方法。启动线程是使用对象调用start() 方法;不能使用对象调用run()方法,对象调用run实例方法还是单线程。
小结 :
线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行,当线程的run()方法结束时该线程完成。
二、共享全局变量
- 在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享(这点要比多进程要好)
from threading import Thread
import time
g_num = 100
def work1():
global g_num
for i in range(3):
g_num += 1
print("----in work1, g_num is %d---"%g_num)
def work2():
global g_num
print("----in work2, g_num is %d---"%g_num)
print("---线程创建之前g_num is %d---"%g_num)
t1 = Thread(target=work1)
t1.start()
#延时一会,保证t1线程中的事情做完
time.sleep(1)
t2 = Thread(target=work2)
t2.start()
运行结果:
—线程创建之前g_num is 100—
----in work1, g_num is 103—
----in work2, g_num is 103—
- 共享全局的问题
假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算,t1和t2都各对g_num加1000000次,g_num的最终的结果应该为2000000。
但是由于是多线程同时操作,有可能出现下面情况
- 在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0
- 然后t2对得到的值进行加1并赋给g_num,使得g_num=1
- 然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
- 这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("----in work1, g_num is %d---"%g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is %d---"%g_num)
print("---线程创建之前g_num is %d---"%g_num)
t1 = threading.Thread(target=work1, args=(1000000,))
t2 = threading.Thread(target=work2, args=(1000000,))
t1.start()
t2.start()
# 等待子线程执行完成 threading.enumerate()返回的是列表:当前时刻存活的线程
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
运行结果:
结论:
- 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
三、线程同步,互斥锁
1、解决线程同时修改全局变量的方式
对于上一小节提出的那个计算错误的问题,可以通过线程同步来进行解决,同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
- 系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num
- t1对g_num的值进行+1
- t1解锁,此时g_num的值为1,其他的线程就可以使用g_num了,而且是g_num的值不是0而是1
- 同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性
2、互斥锁
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。使用threading模块中的Lock类
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
注意:
- 如果这个锁之前是没有上锁的,那么acquire不会堵塞
- 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止
解决前面2个线程对同一个全局变量各加100万次的操作
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
# 上锁
mutex.acquire()
g_num += 1
# 释放锁
mutex.release()
print("----in work1, g_num is %d---" % g_num)
def work2(num):
global g_num
for i in range(num):
# 上锁
mutex.acquire()
g_num += 1
# 释放锁
mutex.release()
print("----in work2, g_num is %d---" % g_num)
print("---线程创建之前g_num is %d---" % g_num)
# 创建互斥锁对象
mutex = threading.Lock()
t1 = threading.Thread(target=work1, args=(1000000,))
t2 = threading.Thread(target=work2, args=(1000000,))
t1.start()
t2.start()
# 等待子线程执行完成 threading.enumerate()返回的是当前时刻线程的
# print(type(threading.enumerate()))
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
运行结果:
上锁解锁过程:
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
3、线程死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
import threading
import time
class MyThread1(threading.Thread):
def run(self):
# 对mutexA上锁
mutexA.acquire()
# mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
print(self.name+'----do1---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name+'----do1---down----')
mutexB.release()
# 对mutexA解锁
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
# 对mutexB上锁
mutexB.acquire()
# mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
print(self.name+'----do2---up----')
time.sleep(1)
# 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
mutexA.acquire()
print(self.name+'----do2---down----')
mutexA.release()
# 对mutexB解锁
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
此时已经进入到了死锁状态,可以使用ctrl+c退出
避免死锁:
- 程序设计时要尽量避免(银行家算法)
- 添加超时时间等
总结:
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁