【百尺竿头,更进一步学Python】Python进阶课程——Python线程
今天我们在学习线程之前,我们先对我们以前学过的Python进阶课程进行一次回顾.
俗话说得好:“温故而知新,可以为师矣”!
Python进阶课程回顾
什么是任务
-
一个程序的每一个模块都视为一个任务,一个程序至少是一个任务,计算机执行程序或代码都是按照任务去分配的。
-
可以理解为每一个程序的运行对于计算机而言都是他的任务工作。
-
目前的系统都是多任务系统,每个系统可以同时运行多个程序
多任务的实现
- 从程序的运行层次而言,多任务分为:并发和并行
并发
- 同时运行的程序任务他们在同一个运行时间线上交替快速的轮流执行,会产生一个假的同时运行效果.
并行
-
以多进程的方式更高效率的使用计算机的所有内核去完成,将程序的进程分配给更多的内核去单独处理每个任务中的每个进程。
-
只有多核CPU才能实现真正的多任务处理
从编程层次而言,多任务的实现可以通过:
- 多进程、多线程、协程来实现.
进程
-
一个正在运行的程序就是一个进程
-
进程是操作系统进行资源分配的最基本单位
我们在上篇博客中举了一个栗子,说的是:现实生活中的工厂就是我们的计算机,工厂中的车间就是一个进程,而车间中的员工就是我们这篇博客要讲的线程
线程
什么是线程
-
python中的线程(thread)模块是比较底层的模块.
-
线程(thread)是操作系统进行运算调度的最小单位.
-
线程在进程之中,是进程的实际运作单位.
-
一条线程其实就是进程中的一个单一顺序的控制流.
-
一个进程中可以并发多个线程,并且每个线程都并行执行不同的任务
-
线程就是车间中的工人,实际干活的也是工人,真正执行程序的还是线程
单线程
-
程序中的代码按照计算机的线程去逐步执行
-
在单线程中程序的执行是按照连续顺序执行下来,前面的代码执行完毕以后,后面的才会执行.
实例:
import time
def upload():
print("开始上传!")
for i in range(1, 5):
print(f"上传了{'{:.2%}'.format(i / 4)}")
time.sleep(1)
print("上传完毕!")
def download():
print("开始下载!")
for i in range(1, 5):
print(f"下载了{'{:.2%}'.format(i / 4)}")
time.sleep(1)
print("下载完毕!")
upload()
download()
# 最后的执行结果是:
# 开始上传!
# 上传了25.00%
# 上传了50.00%
# 上传了75.00%
# 上传了100.00%
# 上传完毕!
# 开始下载!
# 下载了25.00%
# 下载了50.00%
# 下载了75.00%
# 下载了100.00%
# 下载完毕!
多线程实例
import time
import threading
def upload():
print("开始上传!")
for i in range(1, 5):
print(f"上传了{'{:.2%}'.format(i / 4)}")
time.sleep(1)
print("上传结束!")
def download():
print("开始下载!")
for i in range(1, 5):
print(f"下载了{'{:.2%}'.format(i / 4)}")
time.sleep(1)
print("下载完毕!")
def main():
upload_thread = threading.Thread(target=upload)
down_thread = threading.Thread(target=download)
upload_thread.start()
down_thread.start()
if __name__ == '__main__':
main()
# 最后的执行结果是:
# 开始上传!
# 上传了25.00%
# 开始下载!
# 下载了25.00%
# 上传了50.00%
# 下载了50.00%
# 上传了75.00%
# 下载了75.00%
# 上传了100.00%
# 下载了100.00%
# 上传结束!
# 下载完毕!
查看线程数量
- 一个正在执行的程序必然有一个线程,这个线程是主线程
threading.enumerate()
print(threading.enumerate())
# 出来以后是列表的形式显示
# [<_MainThread(MainThread, started 139821703014208)>, <Thread(Thread-1, started 139821686482688)>, <Thread(Thread-2, started 139821678089984)>]
print(len(threading.enumerate()))
# 可以显示出来线程的的数量
# 3
# 注意:如果在主线程还没有调用之前查询的话只有子线程
线程的特点
线程执行代码的封装
- 通过使用threading模块能够完成多任务的程序开发,为了让每个线程的封装性更完美所以使用threading模块时,往往会自定义一个新的子类class,只要继承threading.Thread即可实现
import time
import threading
class MyThread(threading.Thread):
# 重写Thread类里面的run方法
# 一般情况下一个线程中的入口函数是run
def run(self):
print("开始上传!")
for i in range(1, 5):
print(f"上传了{'{:.2%}'.format(i / 4)}")
time.sleep(1)
print("上传结束!")
self.download()
def download(self):
print("开始下载!")
for i in range(1, 5):
print(f"下载了{'{:.2%}'.format(i / 4)}")
time.sleep(1)
print("下载完毕!")
if __name__ == '__main__':
t = MyThread()
t.start()
# 最后执行的结果是:
# 开始上传!
# 上传了25.00%
# 上传了50.00%
# 上传了75.00%
# 上传了100.00%
# 上传结束!
# 开始下载!
# 下载了25.00%
# 下载了50.00%
# 下载了75.00%
# 下载了100.00%
# 下载完毕!
多线程共享全局变量
- 在一个函数中对全局变量进行修改的时候,到底是否需要看是否对全局变量的执行进行了引用修改,如果修改了引用,也就是说让全局变量指向了一个新的地方,如果仅修改了引用的数据,此时不用担心变量被分化
import threading
num = 0
def work1():
global num
for i in range(10):
num += i
print(f"在work1最终的数据是:{num}")
def work2():
global num
print(f"在work2最终的数据是:{num}")
if __name__ == '__main__':
t1 = threading.Thread(target=work1)
t2 = threading.Thread(target=work2)
t1.start()
t2.start()
print(f"最终的数据是:{num}")
# 最后执行的结果是:
# 在work1最终的数据是:45
# 最终的数据是:45
# 在work2最终的数据是:45
共享全局变量的问题
-
假设两个线程都要对全局变量进行修改.
-
如果数据量小基本上没有任何影响.
-
但是如果数据量大了,会发现新的问题:数学老师的棺材板压不住了!
实例:
import threading
num = 0
def add1():
global num
for _ in range(10000000):
num += 1
print(f"add1运算的最终结果是num={num}")
def add2():
global num
for _ in range(10000000):
num += 1
print(f"add2运算的最终结果是num={num}")
if __name__ == '__main__':
t1 = threading.Thread(target=add1)
t2 = threading.Thread(target=add2)
t1.start()
t2.start()
print(f"最终结果是num={num}")
# 最后执行的结果是:
# 最终结果是num=329538
# add2运算的最终结果是num=12933729
# add1运算的最终结果是num=13159958
注意:
- 通过上面的案例,我们发现了新的问题,就是资源抢占的问题,这个问题我们需要引用同步的概念来解决.
同步的概念
什么是同步
-
同步指的是协同步调,按预定的先后次序进行运行
-
线程同步或者进程同步,可以列几位进程或线程A和B一块配合,A执行到一定程度需要依靠B的某个结果,如果B还没有执行完得出结果,那么A就需要停下来等到B运行,反之也是如此
那么解决线程同时修改全局变量的问题如何实现呢?
互斥锁
-
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
-
线程的同步能够保证多个线程安全访问竞争资源,最简单的同步机制就是引入互斥锁
-
互斥锁为资源引入了一个状态:锁定/非锁定
-
某个线程需要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能修改,直到该线程释放资源,将资源的状态变成“非锁定”,其他线程才能够再次锁定该资源,互斥锁保证了每次只有一个线程进行写入工作,从而保证了多线程情况下的数据正确
格式:
# threading模块中定义了Lock锁,可以方便的处理锁定
1.先创建互斥锁
mutex=threading.Lock()
2.锁定资源
mutex.acquire()
3.解锁资源
mutex.release()
注意:
- 在使用互斥锁的同时,锁定的资源越少越好
实例:
import threading
mutex = threading.Lock()
num = 0
def add1():
global num
mutex.acquire()
for _ in range(10000000):
num += 1
mutex.release()
print(f"add1运算的最终结果是num={num}")
def add2():
global num
mutex.acquire()
for _ in range(10000000):
num += 1
mutex.release()
print(f"add2运算的最终结果是num={num}")
if __name__ == '__main__':
t1 = threading.Thread(target=add1)
t2 = threading.Thread(target=add2)
t1.start()
t2.start()
print(f"最终结果是num={num}")
# 加入了互斥锁以后执行的结果是:
# 最终结果是num=448363
# add1运算的最终结果是num=10000000
# add2运算的最终结果是num=20000000
死锁
- 在线程间共享多个资源的时候,如果每个线程分别占有一部分资源,并且同时等待对方的资源,就会造成死锁
如何避免死锁:
- 程序设计时要尽量避免(可以采用银行家算法)
- 添加超时时间,例如五秒以后如果还没有使用完的话,那么就强制释放资源,给另一个线程使用,反之如此.
多线程的优点
使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 这样的话,就会腾出更多的运行资源去处理用户界面这样的话就更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
程序的运行速度可能加快
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。