多任务可以由多进程完成,也可以由一个进程内的多线程完成。
我们前面提到了进程是由若干线程组成的,一个进程至少有一个线程。由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行
以下是单线程的例子:
import time
import threading
def saySorry():
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
#打印当前线程的名字
print(threading.current_thread().name)
saySorry()
结果如下:
这全部都是main线程执行的,运行时间比较慢。接下来采用多线程进行上面的操作。
import threading
import time
def saySorry():
print(threading.current_thread().name)
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=saySorry)
t.start() # 启动线程,即让线程开始执行
结果如下:
可以明显看出使用了多线程并发的操作,花费时间要短很多
创建好的线程,需要调用start()方法来启动。主线程会等待所有的子线程运行结束才会结束。
import threading
import time
def sing():
for i in range(3):
print("正在唱歌...%d"%i)
time.sleep(1)
def dance():
for i in range(3):
print("正在跳舞...%d"%i)
time.sleep(1)
if __name__ == '__main__':
print('---开始---:%s'%time.ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
#time.sleep(5)
print('---结束---:%s'%time.ctime())
结果如下:
使用threading.enumerate可以查看线程数量。
import threading
import time
def sing():
for i in range(3):
print("正在唱歌...%d"%i)
time.sleep(1)
def dance():
for i in range(3):
print("正在跳舞...%d"%i)
time.sleep(1)
if __name__ == '__main__':
print('---开始---:%s'%time.ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
while True:
length = len(threading.enumerate())
print('当前运行的线程数为:%d'%length)
if length<=1:
break
time.sleep(0.5)
结果如下:
从这个例子也可以看出主线程会等待所有的子线程运行结束。
根据我们对进程的学习,线程同样可以通过继承的方法创建。
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()
结果如下:
跟进程一样,可以在创建线程的时候进行参数的传递。比如修改线程的名字,代码如下:
import threading
import time
class MyThread(threading.Thread):
def __init__(self,name):
super().__init__()
self.name = name
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('xx')
t.start()
结果如下:
python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。
关于线程的执行顺序,看以下代码:
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)
print(msg)
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
结果如下:
从代码和执行结果我们可以看出,多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、run函数中每次循环的执行顺序都不能确定。
从以上例子我们可以总结出:每个线程一定会有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。当线程的run()方法结束时该线程完成。无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
线程的几种状态
线程在运行时具有三种基本状态:
1、执行状态,表示线程已获得处理机而正在运行;
2、就绪状态,指线程已具备了各种执行条件,只须再获得CPU便可立即执行;
3、阻塞状态:指线程在执行中因某事件受阻而处于暂停状态,例如,当一个线程执行从键盘读入数据的系统调用时,该线程就被阻塞。
在学习进程的时候,我们知道,进程的全局变量是不共享的,每个进程各运行各的,互不影响,也就是说进程无法修改全局变量。那线程全局变量是否可以共享、修改呢?看如下代码:
import threading
import time
# 全局变量
g_num = 100
# 修改全局变量
def work1():
# 如果想修改全局变量,必须加global
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)
# 将功能包装成函数方法
def main():
print("---线程创建之前g_num is %d---" % g_num)
# 创建一个线程
t1 = threading.Thread(target=work1)
# 就绪
t1.start()
# 延时一会,保证t1线程中的事情做完
time.sleep(1)
t2 = threading.Thread(target=work2)
t2.start()
if __name__ == '__main__':
# 调用方法
main()
结果如下:
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。来看看多个线程同时操作一个变量怎么把内容给改乱了:
import threading
import time
# 全局变量
g_num = 0
# 函数1
def test1():
global g_num
for i in range(1000000):
g_num = g_num+1
print("---test1---g_num=%d" % g_num)
# 函数2
def test2():
global g_num
for i in range(1000000):
g_num = g_num + 1
print("---test2---g_num=%d" % g_num)
def main():
#创建线程
t1 = threading.Thread(target=test1)
#就绪
t1.start()
# 创建线程
t2 = threading.Thread(target=test2)
# 就绪
t2.start()
time.sleep(5)
print("---main---g_num=%d" % g_num)
if __name__ == '__main__':
main()
结果如下:
多线程公用全局变量可能存在的问题假设两个线程t1和t2都要对g_num=0进行增1运算,t1和t2都各对g_num修改1000000次,g_num的最终的结果应该为2000000。
为什么会出现这样的结果,原因在于高级语言的一条语句在CPU执行时是若干条语句,即使是这么简单的一个计算:g_num = g_num+1也会分成两部:
计算g_num+1,存入临时变量中;
将临时变量的值赋给g_num。
可以看成是:
temp = g_num+1
g_num = temp
由于temp是局部变量,两个线程各自都有自己的temp。按理想状态下,代码是这么运行的:
初始值 g_num = 0
t1: temp = g_num + 1 # temp = 0 + 1 = 1
t1: g_num = temp # g_num = 1
t2: temp = g_num + 1 # temp = 1 + 1 = 2
t2: g_num = temp # g_num = 2
就这样一直循环,最后将得到2000000的结果。
但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:
初始值 g_num = 0
t1: temp = g_num + 1 # temp = 0 + 1 = 1
t2: temp = g_num + 1 # temp = 0 + 1 = 1
t2: g_num = temp # g_num = 1
t1: g_num = temp # g_num = 1
结果就是我们刚才看到的,这就是由于多线程访问,有可能出现下面情况:在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都完成了1次加1工作,但结果仍然是g_num=1。
问题产生的原因就是没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期。这种现象称为“线程不安全”。
究其原因,是因为修改g_num需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。所以,我们必须确保一个线程在修改g_num的时候,别的线程一定不能改。系统调用t1,然后获取到num的值为0,此时上一把锁,即不允许其他线程操作num,对num的值进行+1,解锁。此时num的值为1,其他的线程就可以使用num了,而且是num的值不是0而是1。同理其他线程在对num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性。
import threading
# 全局变量
g_num = 0
myLock = threading.Lock()
# 函数1
def test1():
global g_num
# 上锁
myLock.acquire()
print('test1...上锁...')
for i in range(1000000):
g_num = g_num+1
# 开锁
myLock.release()
print('test1...开锁...')
print("---test1---g_num=%d" % g_num)
# 函数2
def test2():
print('test2...')
'''
print('g_num:%s'%g_num)
如使用这句话将报错SyntaxWarning: name 'g_num' is used
prior to global declaration global g_num
'''
global g_num
myLock.acquire()
print('test2...上锁...')
print('g_num:%s'%g_num)
for i in range(1000000):
g_num = g_num + 1
# 开锁
myLock.release()
print('test2...开锁...')
print("---test2---g_num=%d" % g_num)
def main():
#创建线程
t1 = threading.Thread(target=test1)
#就绪
t1.start()
# 创建线程
t2 = threading.Thread(target=test2)
# 就绪
t2.start()
print("---main---g_num=%d" % g_num)
if __name__ == '__main__':
main()
结果如下:
上锁是为了保护线程访问数据的唯一性,并不妨碍上锁后,cpu对线程的切换,当上锁的代码段执行过程中,cpu切换走,其他线程如果是上锁等待,则cpu继续切换,如果cpu切换的其他线程不是上锁等待,则其他线程依然可执行。cpu的线程切换是不固定的。并不是一定要等待上锁的程序走完。这也就解释了为什么打印test1...上锁...后又打印了test2...。
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
从刚才的例子中可以看出对于全局变量,在多线程中要格外小心,否则容易造成数据错乱的情况发生。那对于非全局变量是否需要加锁呢?看如下代码:
import threading
import time
class MyThread(threading.Thread):
# 重写 构造方法
def __init__(self, num, sleepTime):
# 重写父类的__init__()方法
threading.Thread.__init__(self)
self.num = num
self.sleepTime = sleepTime
def run(self):
self.num += 1
time.sleep(self.sleepTime)
print('线程(%s),num=%d' % (self.name, self.num))
if __name__ == '__main__':
mutex = threading.Lock()
t1 = MyThread(100, 5)
t1.start()
t2 = MyThread(200, 1)
t2.start()
结果如下:
再观察如下代码:
import threading
import time
def test(sleepTime):
num = 1
time.sleep(sleepTime)
num += 1
print('---(%s)--num=%d' % (threading.current_thread(), num))
t1 = threading.Thread(target=test, args=(5,))
t2 = threading.Thread(target=test, args=(1,))
t1.start()
t2.start()
结果如下:
也就是说在多线程开发中,全局变量是多个线程都共享的数据,而局部变量等是各自线程的,是非共享的。
在刚才讲锁的时候说到了死锁,那么什么是死锁呢?在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子:
import threading
import time
class MyThread1(threading.Thread):
def run(self):
if mutexA.acquire():
print(self.name + '----do1---up----')
time.sleep(1)
if mutexB.acquire():
print(self.name + '----do1---down----')
mutexB.release()
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
if mutexB.acquire():
print(self.name + '----do2---up----')
time.sleep(1)
if mutexA.acquire():
print(self.name + '----do2---down----')
mutexA.release()
mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
t1 = MyThread1()
t2 = MyThread2()
t1.start()
t2.start()
结果如下:
那我们如何避免死锁呢?常用的是在程序设计时要尽量避免(比如采用银行家算法),要么添加超时时间等。
有了以上的知识,就可以让线程按我们的需求的顺序执行:
import threading
import time
class Task1(threading.Thread):
def run(self):
while True:
if lock1.acquire():
print("------Task 1 -----")
time.sleep(0.5)
lock2.release()
class Task2(threading.Thread):
def run(self):
while True:
if lock2.acquire():
print("------Task 2 -----")
time.sleep(0.5)
lock3.release()
class Task3(threading.Thread):
def run(self):
while True:
if lock3.acquire():
print("------Task 3 -----")
time.sleep(0.5)
lock1.release()
# 使用Lock创建出的锁默认没有“锁上”
lock1 = threading.Lock()
# 创建另外一把锁,并且“锁上”
lock2 = threading.Lock()
lock2.acquire()
# 创建另外一把锁,并且“锁上”
lock3 = threading.Lock()
lock3.acquire()
t1 = Task1()
t2 = Task2()
t3 = Task3()
t1.start()
t2.start()
t3.start()
结果如下:
也就是说我们可以可以使用互斥锁完成多个任务,有序的进程工作,这就是线程的同步。接下来,我们模拟一下车站的卖票:
import threading
import time
import os
def doChore(): # 作为间隔 每次调用间隔0.5s
time.sleep(0.5)
def booth(tid):
global i
global lock
while True:
lock.acquire() # 得到一个锁,锁定
if i != 0:
i = i - 1 # 售票 售出一张减少一张
print(tid, ':now left:', i) # 剩下的票数
doChore()
else:
print("Thread_id", tid, " No more tickets")
os._exit(0) # 票售完 退出程序
lock.release() # 释放锁
doChore()
# 全局变量
i = 15 # 初始化票数
lock = threading.Lock() # 创建锁
def main():
# 总共设置了3个线程
for k in range(3):
# 创建线程; Python使用threading.Thread对象来代表线程
new_thread = threading.Thread(target=booth, args=(k,))
# 调用start()方法启动线程
new_thread.start()
if __name__ == '__main__':
main()
结果如下:
以上就是关于线程同步的总结。
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。
用FIFO队列实现上述生产者与消费者问题的代码如下:
import threading
import time
import queue
class Producer(threading.Thread):
def run(self):
global queue
count = 0
while True:
if queue.qsize() < 1000:
for i in range(100):
count = count + 1
msg = '生成产品' + str(count)
queue.put(msg)
print(msg)
time.sleep(0.5)
class Consumer(threading.Thread):
def run(self):
global queue
while True:
if queue.qsize() > 100:
for i in range(3):
msg = self.name + '消费了 ' + queue.get()
print(msg)
time.sleep(1)
if __name__ == '__main__':
queue = queue.Queue()
for i in range(500):
queue.put('初始产品' + str(i))
for i in range(2):
p = Producer()
p.start()
for i in range(5):
c = Consumer()
c.start()
结果如下:
为什么要使用生产者和消费者模式?
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式?
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦。
- Queue的说明
对于Queue,在多线程通信之间扮演重要的角色
1、添加数据到队列中,使用put()方法
2、从队列中取数据,使用get()方法
3、判断队列中是否还有数据,使用qsize()方法
在多线程中,如果使用全局变量,会出问题,一旦一个线程修改了,会影响其它线程。如果使用局部变量,一旦想让这个局部变量,一直使用下去,一直传参,比较麻烦。既不想使用全局变量,又一直想用这个局部变量。可以将这个局部变量绑定到对应的线程中,以后此线程一直用个。与其它线程中的无关。
尝试用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象:
import threading
# 创建字典对象
myDict = {}
def process_student():
# 获取当前线程关联的student
std = myDict[threading.current_thread()]
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student
myDict[threading.current_thread()] = name
process_student()
t1 = threading.Thread(target=process_thread, args=('yongGe',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()
结果如下:
这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点low。看下面的代码:
import threading
# 创建全局ThreadLocal对象
local_school = threading.local()
def process_student():
# 获取当前线程关联的student
std = local_school.student
print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
# 绑定ThreadLocal的student
local_school.student = name
process_student()
t1 = threading.Thread(target=process_thread, args=('yongGe',), name='Thread-A')
t2 = threading.Thread(target=process_thread, args=('老王',), name='Thread-B')
t1.start()
t2.start()
结果如下:
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。
异步
from multiprocessing import Pool
import time
import os
def test():
print("---进程池中的进程---pid=%d,ppid=%d--" % (os.getpid(), os.getppid()))
for i in range(3):
print("----%d---" % i)
return "老王"
def test2(args):
print('1...')
print("---callback func--pid=%d" % os.getpid())
print("---callback func--args=%s" % args)
print('2...')
if __name__ == '__main__':
pool = Pool(3)
# callback表示前面的func方法执行完,再执行callback,并且可以获取func的返回值作为callback的参数
pool.apply_async(func=test, callback=test2)
# pool.apply_async(func=test)
# 模拟主进程在做任务
time.sleep(5)
print("----主进程-pid=%d.....----" % os.getpid())
结果如下:
可以看到执行callback的是主进程。也就是说当主进程在做任务时,进程又分出了一个线程去做其他任务。
多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。