目录
1 多任务概念
简单地说,就是操作系统可以同时运行多个任务
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
注意:
- 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
- 并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
2 线程
2.1 threading模块
python的thread模块是比较底层的模块
python的threading模块是对thread做了一些包装的,可以更加方便的被使用
# -*- coding: utf-8 -*-
import threading
import time
def saySorry():
print("亲爱的,我错了,我能吃饭了吗?")
time.sleep(1)
def one():
s = time.time()
for i in range(5):
saySorry()
print('单线程执行时间:{}'.format(time.time() - s))
def many():
s = time.time()
for i in range(5):
t = threading.Thread(target=saySorry)
t.start() # 启动线程,即让线程开始执行
print('多线程执行时间:{}'.format(time.time() - s))
if __name__ == "__main__":
one()
many()
"""
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
单线程执行时间:5.053362607955933
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
亲爱的,我错了,我能吃饭了吗?
多线程执行时间:0.003016948699951172
"""
可以明显看出使用了多线程并发的操作,花费时间要短很多
当调用start()时,才会真正的创建线程,并且开始执行
2.2 主线程会等待所有的子线程结束后才结束
# -*- coding: utf-8 -*-
import threading
from time import sleep, ctime
def sing():
for i in range(5):
print("正在唱歌...%d" % i)
sleep(1)
def dance():
for i in range(5):
print("正在跳舞...%d" % i)
sleep(1)
if __name__ == '__main__':
print('---开始---:%s' % ctime())
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
sleep(5) # 屏蔽此行代码,试试看,程序是否会立马结束?
print('---结束---:%s' % ctime())
"""
---开始---:Wed Mar 9 15:23:49 2022
正在唱歌...0
正在跳舞...0
正在唱歌...1正在跳舞...1
正在跳舞...2正在唱歌...2
正在跳舞...3正在唱歌...3
正在唱歌...4
正在跳舞...4
---结束---:Wed Mar 9 15:23:54 2022
"""
2.3 查看线程数量
# -*- coding: utf-8 -*-
import threading
from time import sleep, ctime
def sing():
for i in range(5):
print("正在唱歌...%d" % i)
sleep(1)
def dance():
for i in range(5):
print("正在跳舞...%d" % i)
sleep(1)
if __name__ == '__main__':
print('---开始---:%s' % 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
sleep(0.5)
"""
---开始---:Wed Mar 9 15:26:30 2022
正在唱歌...0
正在跳舞...0当前运行的线程数为:3
当前运行的线程数为:3
正在唱歌...1正在跳舞...1
当前运行的线程数为:3
当前运行的线程数为:3
正在跳舞...2
正在唱歌...2
当前运行的线程数为:3
当前运行的线程数为:3
当前运行的线程数为:3正在跳舞...3
正在唱歌...3
当前运行的线程数为:3
当前运行的线程数为:3正在唱歌...4正在跳舞...4
当前运行的线程数为:3
当前运行的线程数为:1
"""
2.4 线程代码封装
使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以,然后重写run方法
python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。
# -*- coding: utf-8 -*-
import threading
import time
class MyThread(threading.Thread):
def run(self):
for i in range(5):
time.sleep(1)
msg = "I'm " + self.name + '@' + str(i) # name是当前线程名字
print(msg)
if __name__ == '__main__':
t = MyThread()
t.run()
"""
I'm Thread-1@0
I'm Thread-1@1
I'm Thread-1@2
I'm Thread-1@3
I'm Thread-1@4
"""
2.5 线程的执行顺序
多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、run函数中每次循环的执行顺序都不能确定。
# -*- coding: utf-8 -*-
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()
"""
I'm Thread-5 @ 0
I'm Thread-3 @ 0
I'm Thread-4 @ 0
I'm Thread-1 @ 0
I'm Thread-2 @ 0
I'm Thread-1 @ 1
I'm Thread-2 @ 1
I'm Thread-4 @ 1
I'm Thread-5 @ 1
I'm Thread-3 @ 1
I'm Thread-5 @ 2
I'm Thread-3 @ 2
I'm Thread-4 @ 2
I'm Thread-2 @ 2
I'm Thread-1 @ 2
"""
- 每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。
- 当线程的run()方法结束时该线程完成。
- 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。
2.6 多线程
2.6.1 共享全局变量
# -*- coding: utf-8 -*-
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加10次,g_num的最终的结果应该为20。
但是由于是多线程同时操作,有可能出现下面情况:
- 在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
- 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确
2.6.2 列表当作实参传递到线程
# -*- coding: utf-8 -*-
from threading import Thread
import time
def work1(nums):
nums.append(44)
print("----in work1---", nums)
def work2(nums):
# 延时一会,保证t1线程中的事情做完
time.sleep(1)
print("----in work2---", nums)
g_nums = [11, 22, 33]
t1 = Thread(target=work1, args=(g_nums,))
t1.start()
t2 = Thread(target=work2, args=(g_nums,))
t2.start()
"""
----in work1--- [11, 22, 33, 44]
----in work2--- [11, 22, 33, 44]
"""
2.6.3 总结
- 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
- 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)
2.7 互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading模块中定义了Lock类,可以方便的处理锁定:
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire([blocking])
# 释放
mutex.release()
其中,锁定方法acquire可以有一个blocking参数
- 如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为止(如果没有指定,那么默认为True)
- 如果设定blocking为False,则当前线程不会堵塞
例:使用互斥锁完成2个线程对同一个全局变量各加100万次的操作
# -*- coding: utf-8 -*-
import threading
import time
g_num = 0
def test1(num):
global g_num
for i in range(num):
# True表示堵塞 即如果这个锁在上锁之前已经被上锁了,那么这个线程会在这里一直等待到解锁为止
# False表示非堵塞,即不管本次调用能够成功上锁,都不会卡在这,而是继续执行下面的代码
mutexFlag = mutex.acquire(True)
if mutexFlag: # 如果上锁成功
g_num += 1
mutex.release() # 解锁
print("---test1---g_num=%d" % g_num)
def test2(num):
global g_num
for i in range(num):
mutexFlag = mutex.acquire(True) # True表示堵塞
if mutexFlag:
g_num += 1
mutex.release()
print("---test2---g_num=%d" % g_num)
# 创建一个互斥锁
# 这个所默认是未上锁的状态
mutex = threading.Lock()
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()
p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
"""
---test2---g_num=1809728
---test1---g_num=2000000
2个线程对同一个全局变量操作之后的最终结果是:2000000
"""
2.7.1 上锁解锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
2.7.2 总结
锁的好处:
- 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
- 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
- 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
2.8 死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
# -*- coding: utf-8 -*-
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()
2.8.1 避免死锁
- 程序设计是尽量避免(银行家算法)
- 添加超时时间等
2.8.2 银行家算法
背景:
一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产,这就是银行家问题。这个问题同操作系统中资源分配问题十分相似:银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。
问题的描述:
一个银行家拥有一定数量的资金,有若干个客户要贷款。每个客户须在一开始就声明他所需贷款的总额。若该客户贷款总额不超过银行家的资金总数,银行家可以接收客户的要求。客户贷款是以每次一个资金单位(如1万RMB等)的方式进行的,客户在借满所需的全部单位款额之前可能会等待,但银行家须保证这种等待是有限的,可完成的。
例如:有三个客户C1,C2,C3,向银行家借款,该银行家的资金总额为10个资金单位,其中C1客户要借9各资金单位,C2客户要借3个资金单位,C3客户要借8个资金单位,总计20个资金单位。某一时刻的状态如图所示。
对于a图的状态,按照安全序列的要求,我们选的第一个客户应满足该客户所需的贷款小于等于银行家当前所剩余的钱款,可以看出只有C2客户能被满足:C2客户需1个资金单位,小银行家手中的2个资金单位,于是银行家把1个资金单位借给C2客户,使之完成工作并归还所借的3个资金单位的钱,进入b图。同理,银行家把4个资金单位借给C3客户,使其完成工作,在c图中,只剩一个客户C1,它需7个资金单位,这时银行家有8个资金单位,所以C1也能顺利借到钱并完成工作。最后(见图d)银行家收回全部10个资金单位,保证不赔本。那麽客户序列{C1,C2,C3}就是个安全序列,按照这个序列贷款,银行家才是安全的。否则的话,若在图b状态时,银行家把手中的4个资金单位借给了C1,则出现不安全状态:这时C1,C3均不能完成工作,而银行家手中又没有钱了,系统陷入僵持局面,银行家也不能收回投资。
综上所述,银行家算法是从当前状态出发,逐个按安全序列检查各客户谁能完成其工作,然后假定其完成工作且归还全部贷款,再进而检查下一个能完成工作的客户,......。如果所有客户都能完成工作,则找到一个安全序列,银行家才是安全的。