介绍
多件事情同时运行,即多任务。
在我们的任务管理器中所看到的多个进程同时运行就是多任务情形。
有顺序的进行任务不是多任务,如先唱歌在跳舞。
from time import sleep
def sing():
for i in range(3):
print(f'正在唱歌。。。{i}')
sleep(1)
def dance():
for i in range(3):
print(f'正在跳舞。。。{i}')
sleep(1)
if __name__ == '__main__':
sing()
dance()
让唱歌跳舞同时进行,所用的方法就是多任务。
import threading
from time import sleep
def sing():
for i in range(3):
print(f'正在唱歌。。。{i}')
sleep(1)
def dance():
for i in range(3):
print(f'正在跳舞。。。{i}')
sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
在计算机中,操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。
我们平时看到的多任务实际上是cpu在不停地切换执行程序。
注意:
- 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
- 并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的。
多线程
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
为了提高程序的执行效率,多线程就成了必要。
python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用
以下是顺序运行的代码,
import time
def saySorry():
print('亲爱的我错了我可以吃饭了吗?')
time.sleep(1)
if __name__ == '__main__':
start = time.time()
saySorry()
end = time.time()
print(f'代码执行耗时{end-start}')
执行结果:
亲爱的我错了我可以吃饭了吗?
代码执行耗时1.0002381801605225
以下是多线程代码:
import threading
import time
def saySorry():
print('亲爱的我错了我可以吃饭了吗?')
time.sleep(1)
if __name__ == '__main__':
start = time.time()
for i in range(5):
t = threading.Thread(target=saySorry)
t.start()
end = time.time()
print(f'代码执行耗时{end-start}')
执行结果:
亲爱的我错了我可以吃饭了吗?
亲爱的我错了我可以吃饭了吗?
亲爱的我错了我可以吃饭了吗?
亲爱的我错了我可以吃饭了吗?
亲爱的我错了我可以吃饭了吗?
代码执行耗时0.0010030269622802734
可以明显看出使用了多线程并发的操作,花费时间要短很多
当调用start()
时,才会真正的创建线程,并且开始执行
关于线程执行顺序
import threading
from time import sleep,ctime
def sing():
print(f'开始唱歌。。。{ctime()}')
sleep(3)
print(f'结束唱歌。。。{ctime()}')
def dance():
print(f'开始跳舞。。。{ctime()}')
sleep(3)
print(f'结束跳舞。。。{ctime()}')
if __name__ == '__main__':
print(f'程序开始。。。{ctime()}')
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
while True:
print(threading.enumerate())
print(ctime())
if len(threading.enumerate())<=1:
break
sleep(0.5)
t1.join()
t2.join()
print(f'程序结束。。。{ctime()}')
当我们执行多次多线程程序时,可以看出,多线程程序的执行顺序是不确定的。当我们执行到sleep()
的时候,线程进入阻塞状态,sleep结束后,线程进入就绪状态,等待调度。而线程调度会自主选择一个线程执行,直到所有线程全部执行完成。但线程的执行顺序我们是无法控制的。
线程之间共享全局变量
线程无参数:
g_num = 100
def work1():
global g_num
for i in range(10):
g_num += 1
print(f'-----------------in work1 g_num={g_num}')
def work2():
print(f'-----------------in work1 g_num={g_num}')
if __name__ == '__main__':
print(f'程序开始时,g_num的初始值是{g_num}')
t1 = threading.Thread(target=work1)
t1.start()
t1.join()
t2 = threading.Thread(target=work2)
t2.start()
t2.join()
线程有参数:
def work1(lis):
lis.append(33)
lis.append(44)
print(f'-----------------in work1 lis={lis}')
def work2(lis):
print(f'-----------------in work1 lis={lis}')
if __name__ == '__main__':
lis = [11,22]
print(f'程序开始时,lis的初始值是{lis}')
# 如果线程任务需要接收参数
# 创建线程时,以元组的方式传给args即可
t1 = threading.Thread(target=work1,args=(lis,))
t1.start()
t1.join()
t2 = threading.Thread(target=work2,args=(lis,))
t2.start()
# 线程之间共享全局变量,包括可变和不可变类型
注意
线程之间共享全局变量会出现资源紧张的问题。
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print(f'-----------------in work2 g_num={g_num}')
def work2(num):
global g_num
for i in range(num):
g_num += 1
print(f'-----------------in work1 g_num={g_num}')
if __name__ == '__main__':
print(f'程序开始时,g_num的初始值是{g_num}')
t1 = threading.Thread(target=work1,args=(1000000,))
t2 = threading.Thread(target=work2,args=(1000000,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f'程序结束时,g_num的最终值是{g_num}')
结果按照正常来说应该是200 0000,但实际执行结果却是:
程序开始时,g_num的初始值是0
-----------------in work2 g_num=999886
-----------------in work1 g_num=1145184
程序结束时,g_num的最终值是1145184
原因:前面说过,线程执行是无序的,这导致了资源竞争。当线程一拿到数据时本应往下传递,但在传递的时候,线程二还没拿到线程一的数据,由于线程一不再占用资源,于是线程二就开始执行了,导致线程一执行结果即正在传递的数据失效作废,但所有线程还在往下执行。
这种现象是概率型的,但事件越多概率越大,也就导致了以上执行结果与预想结果偏差过大。
所以说,线程间是不安全的。
互斥锁
为了解决多个线程间共享数据的资源竞争问题,python引进互斥锁。
g_num = 0
mutex = threading.Lock() # 创建锁对象
def work1(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print(f'-----------------in work2 g_num={g_num}')
def work2(num):
global g_num
for i in range(num):
mutex.acquire() # 上锁
g_num += 1
mutex.release() # 解锁
print(f'-----------------in work1 g_num={g_num}')
if __name__ == '__main__':
print(f'程序开始时,g_num的初始值是{g_num}')
t1 = threading.Thread(target=work1,args=(1000000,))
t2 = threading.Thread(target=work2,args=(1000000,))
t1.start()
t2.start()
t1.join()
t2.join()
print(f'程序结束时,g_num的最终值是{g_num}')
缺陷:效率变低。可能产生死锁。
死锁代码:
m1 = threading.Lock()
m2 = threading.Lock()
def run1():
# 对m1加锁
m1.acquire()
print('run------------------1')
time.sleep(1)
m2.acquire()
print('run------------------2')
time.sleep(1)
m2.release()
# 对m1解锁
m1.release()
def run2():
# 对m2加锁
m2.acquire()
print('run------------------2')
time.sleep(1)
m1.acquire()
print('run------------------1')
time.sleep(1)
m1.release()
# 对m2解锁
m2.release()
解决死锁办法:
1、银行家算法
注意事项
- join() —— 主线程会等待该县城结束后才会结束。
- 并行、并发、同步、异步、互斥、阻塞是多线程必须了解的概念。