目录
⭐时光不止,奋斗不息,才是年轻人该有的样子。一个领域的专家一开始都是小白。感谢女朋友的支持♥一切都会慢慢好起来的🌈
活动地址:CSDN21天学习挑战赛
1.多任务概述
对于系统什么是 “多任务”呢?简单地说就是系统可以同时运行多个任务。比如说:烧水的同时,你可以洗衣服、听歌等。
单核 CPU 在执行任务时,操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2执行0.01秒...然后反复执行下去。在程序上看,每个任务是轮流交替的,但是由于CPU运行速度很快,在视觉上看,我们就感觉所有任务是在同时进行。真正的并行执行多任务只能在多核 CPU 上实现,任务的数量会远大于 CPU 的核数量,所以操作系统会将任务轮流调度到每个核心上执行。
注意:
- 并发:是指任务数多于 CPU 核数,通过操作系统的各种任务调度算法,实现多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度非常快,看上去是一起执行)
- 并行:指的是任务数小于 CPU 核数,多个任务真的是一起执行
2. threading 模块
Python 的 thread 模块是比较底层的模块,Python 的 threading模块 是对 thread 做了一些包装,可以更加方便地被使用。
方法名 | 说明 |
---|---|
threading.active_count() | 返回当前处于 active 状态的 Thread对象 |
threading.current_thread() | 返回当前 Thread对象 |
threading.get_ident() | 返回当前线程的线程标识符。线程标识符是一个非负整数,并无特殊含义,只是用来标识线程,该整数可能会被循环利用。Python3.3 及以后版本支持该方法。 |
threading.enumerate() | 返回当前处于 active 状态的所有 Thread对象列表 |
threading.mainthread() | 返回主线程对象,即启动 Python 解释器的线程对象。Python3.4 及以后版本支持该方法 |
threading.stack_size() | 返回创建线程时使用的栈的大小,如果指定 size 参数,则用来指定后续创建的线程使用的栈大小,size 必须是0(表示使用系统默认值)或大于32K的正整数 |
2.1 Thread类
threading 模块提供了 Thread、Lock、Condition、Event、Timer 和 Semaphore等类来支持多线程,Thread 是其中最重要的也是最基本的一个类,可以通过该类创建线程并控制线程的运行。
函数语法:threading.Thread(group=None, target=None, name=None, args=(), kwarg={}, *, daemon=None)
参数说明:
- group:通常默认即可,作为日后扩展 ThreadGroup 类实现而保留。
- target:用于 run() 方法调用的可调用对象,默认为 None。
- name:线程名称,默认是 Thread-N 格式构成的唯一名称,其中 N 是十进制数。
- args:用于调用目标函数的参数元组,默认为()。
- kwargs:用于调用目标函数的关键参数字典,默认为{}。
- daemon:设置线程是否为守护模式,默认为None。
方法名 | 说明 |
---|---|
start() | 启动线程 |
run() | 线程代码,用来实现线程的功能与业务逻辑,可以在子类中重写该方法来自定义线程的行为 |
init(self, group=None, target=None, name=None, args=(), kwargs=None, daemon=None) | 构造函数 |
is_alive() | 判断线程是否存活 |
getName() | 返回线程名 |
setName() | 设置线程名 |
isDaemon() | 判断线程是否为守护线程 |
setDaemon() | 设置线程是否为守护线程 |
name | 用来读取或设置线程的名字 |
ident | 线程标识,用非0数字或None(线程未被启动) |
daemon | 表示线程是否为守护线程,默认为False |
join(timeout=None) | 当 timeout=None 时,会等待至线程结束;当 timeout 不为 None 时,会等待至 timeout 时间结束,单位为秒 |
使用 Thread 创建线程的方法:
- 为构造函数传递一个可调用对象;
- 继承 Thread 类并在子类中重写 __Int__() 和 run() 方法。2.
2.2 threading.Thread 用法举例:
2.2.1 单线程举例
import time
def sayHello():
print("Hello word!")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
sayHello()
运行结果:
2.2.2 使用 threading 模块多线程举例
#coding=utf-8
import threading
import time
def sayHello():
print("Hello word!")
time.sleep(1)
if __name__ == "__main__":
for i in range(5):
t = threading.Thread(target=sayHello)
t.start()
运行结果:
分析:可以明显看出使用了多线程并发的操作,程序运行时间短;当调用 start() 时,才会真正的创建线程,并且开始执行。在传入 target 参数时,应该只是函数名,不加括号。
2.2.3 查看线程数量
import threading
from time import sleep,ctime
def sing():
for i in range(3):
print("正在唱歌...%d"%i)
sleep(1)
def dance():
for i in range(3):
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)
print('---结束---:%s' % ctime())
程序结果:
2.3 继承 threading.Thread
2.3.1 线程执行代码的封装
为了让每个线程的封装更加完美,所以使用 threading 模块时,往往会定义一个新的子类 class,只要继承 threading.Thread 就可以了,然后重写 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)
if __name__ == '__main__':
t = MyThread()
t.start()
运行结果:
分析: Python 的 threading.Thread 类有一个 run 方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法,从而创建自己的线程实例后,通过 Thread 类的 start 方法,可以启动该线程,交给 Python 虚拟机进行调度,当该线程获得执行的机会时,就会调用 run 方法执行线程。
2.3.2 线程执行顺序
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(3):
t = MyThread()
t.start()
if __name__ == '__main__':
test()
# 输出:
# I'mThread-3@0I'mThread-2@0
#
# I'mThread-1@0
# I'mThread-3@1
# I'mThread-1@1
# I'mThread-2@1
# I'mThread-2@2I'mThread-1@2
#
# I'mThread-3@2
分析:运行的结果可能不太一样,但是大体上是一致的。从上面代码和执行结果看出,多线程的执行顺序是不确定的。当执行到 sleep 语句时,线程被阻塞(blocked),到 sleep 结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个 run 函数,但是线程的启动顺序,run 函数中国每次循环的执行顺序都不能确定。
2.3.3 总结
- 每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的 name,但是 Python 会自动为线程指定一个名字;
- 当线程的 run() 方法结束时,该线程结束;
- 无法控制线程调度程序,但可以通过别的方法来影响线程调度的方式。
2.4 多线程-共享全局变量
2.4.1 整型全局变量
from threading import Thread
import time
test_num = 100
def work1():
global test_num
for i in range(3):
test_num += 1
print('----in work1, test_num = %d----'%test_num)
def work2():
global test_num
print('----in work2, test_num = %d----' % test_num)
print("----线程创建之前test_num = %d----"%test_num)
t1= Thread(target=work1)
t1.start()
#延时一会儿,保证t1线程中的事情做完
time.sleep(1)
t2 = Thread(target=work2)
t2.start()
# 输出:
# ----线程创建之前test_num = 100----
# ----in work1, test_num = 103----
# ----in work2, test_num = 103----
2.4.2 列表全局变量
from threading import Thread
import time
def work1(nums):
nums.append(44)
print('----in work1----',nums)
def work2(nums):
time.sleep(1) #延时一会,保证线程1中的事情做完
print('----in work2----',nums)
test_list = [11,22,33]
t1= Thread(target=work1, args=(test_list,))
t1.start()
t2 = Thread(target=work2, args=(test_list,))
t2.start()
# 输出:
# ----in work1---- [11, 22, 33, 44]
# ----in work2---- [11, 22, 33, 44]
2.4.3 总结
- 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据;
- 缺点是,线程对全局变量随意改变,可能会造成多线程之间对全局变量的混乱(线程并非安全)。
2.4.4 多线程-共享全局变量问题
多线程开发可能遇到的问题:假设两个线程 t1 和 t2 都要对全局变量 test_num(默认是0)进行加1运算,t1 和 t2 都各对 test_num 加10次,test_num 的最终结果应该为20。
但是由于多线程同时操作,有可能会出现下面情况:
- 在 test_num=0 时,t1 取得 test_num=0。此时系统把 t1 调度为“sleeping”状态,把 t2 转换为“running”状态,t2 也获得 test_num=0;
- 然后 t2 对得到的值进行加1并赋值给 test_num,使得 test_num=1;
- 然后系统又把 t2 调度为“sleeping”,把 t1 转为“running”。线程 t1 又把它之前得到的0加1后赋值给 test_num;
- 这样导致 t1 和 t2 都对 test_num加1,但结果 test_num值仍为1。
import threading
import time
test_num =0
def work1(nums):
global test_num
for i in range(nums):
test_num += 1
print('----in work1, test_num is %d----'%test_num)
def work2(nums):
global test_num
for i in range(nums):
test_num += 1
print('----in work2, test_num is %d----'%test_num)
print('----线程创建之前test_num is %d----'%test_num)
t1= threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
while len(threading.enumerate())!=1:
time.sleep(1)
print('2个线程对同一全局变量操作之后的最终结果是:%d'%test_num)
# 输出:
# ----线程创建之前test_num is 0----
# ----in work1, test_num is 1200528----
# ----in work2, test_num is 1246231----
# 2个线程对同一全局变量操作之后的最终结果是:1246231
分析:以上代码中,test_num 应该被加两百万次,而实际一百多万次。如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确。
2.5 线程同步
2.5.1 线程同步概述
同步就是协同步调,按预定的先后次序进行运行。如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。
对于上一小节中提出的问题,可以通过线程同步来进行解决,思路如下:
- 系统调用 t1,然后获取 test_num 的值为0,此时上一把锁,即不允许其他线程操作 test_num;
- t1 对 test_num 的值进行加1;
- t1 解锁,此时 test_num 的值为1,其他的线程就可以使用 test_num 了,而且 test_num 的值为1;
- 同理其他线程对 tset_num 进行修改时,都要先上锁,处理完再解锁,在上锁的过程中不允许其他线程访问,就保证了数据的正确性。
2.5.2 互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。互斥锁为资源引入一个状态:锁定/非锁定。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能进行修改;直到该线程释放资源,将资源的状态变为“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只要一个线程进入写操作,从而保证了多线程情况下数据的正确性。
threading 模块中定义了 Lock 类,可以方便地处理锁定。注意:如果这个锁之前是没有上锁地,那么 acquire 不会堵塞;如果在调用 acquire 对这个锁进行上锁之前,它已经被其他线程上了锁,那么此时 acquire 会堵塞,知道这个锁被解除为止。
应用举例:
import threading
import time
test_num =0
def work1(nums):
global test_num
for i in range(nums):
mutex.acquire() #上锁
test_num += 1
mutex.release() #解锁
print('----in work1, test_num is %d----'%test_num)
def work2(nums):
global test_num
for i in range(nums):
mutex.acquire() #上锁
test_num += 1
mutex.release() #解锁
print('----in work2, test_num is %d----'%test_num)
#1.创建一个互斥锁,默认是未上锁的状态
mutex = threading.Lock()
print('----线程创建之前test_num is %d----'%test_num)
#2.创建两个线程,让它们各自为test_num加1000000次
t1= threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
#等待计算完成
while len(threading.enumerate())!=1:
time.sleep(1)
print('2个线程对同一全局变量操作之后的最终结果是:%d'%test_num)
# 输出:
# ----线程创建之前test_num is 0----
# ----in work2, test_num is 1997033--------in work1, test_num is 2000000----
#
# 2个线程对同一全局变量操作之后的最终结果是:2000000
分析:上段代码加入互斥锁后,其结果与预期一致。
上锁解锁过程:
- 当1个线程调用锁的 acquire() 方法获得锁时,锁就进入“locked”状态;
- 每次只有一个线程可以获得锁。如果另一个线程试图获得这个锁,该线程会变成“blocked”状态,称为“阻塞”,直到拥有锁的线程调用的 release() 方法释放锁之后,锁进入“unlocked”状态;
- 线程调度从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
总结:
锁的好处:确保了某段关节代码只能由一个线程从头到尾完整地执行。
锁地坏处:阻止了多线程并发执行,包含锁的某段代码实际上只能单线程模式执行,效率就很大程度上下降了;由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
2.5.3 死锁
在线程共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽量避免发生死锁,一旦发送就会造成应用的停止响应。
应用举例:
import threading
import time
class MyThread1(threading.Thread):
def run(self):
#对mutexA上锁
mutexA.acquire()
#mutexA上锁后,延时1秒,等待另外那个线程把muteB上锁
print(self.name+'---do1---up---')
time.sleep(1)
#此时会堵塞,因为这个muteB已经被另外的线程抢先上锁了
mutexB.acquire()
print(self.name+'---do1---down---')
mutexB.release()
#对mutexA解锁
mutexA.release()
class MyThread2(threading.Thread):
def run(self):
#对mutexB上锁
mutexB.acquire()
#mutexB上锁后,延时1秒,等待另外那个线程把muteA上锁
print(self.name+'---do2---up---')
time.sleep(1)
#此时会堵塞,因为这个muteA已经被另外的线程抢先上锁了
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()
运行结果:
分析:此时已进入死锁,可以直接停止程序运行。Pycharm 停止运行快捷键是 Ctrl+F2。
如何避免死锁?
- 程序设计时要尽量避免(银行家算法)
- 添加超时时间
2.6 附录——银行家算法
2.6.1 背景知识
一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产,这就是银行家问题。这个问题同操作系统中资源分配问题十分相似,银行家就像是一个操作系统,客户就像运行的线程,银行家的资金是系统的资源。
2.6.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)图中,这时银行家有8个资金单位,所以 C1 客户也能顺利借到钱并完成工作。最后见(d)图,银行家收回全部10个资金单位,保证不赔本。那么客户序列{C1,C2,C3}就是个安全序列,按照这个序列进行贷款,银行家猜死安全的。否则,若在图(b)状态时,银行家将手中的4个资金单位借给了 C1 ,则出现不安全状态:这时 C1 和 C3 都不能完成工作,而银行家手中又没有钱了,系统陷入僵持局面,银行家也不能收回贷款。
综上所述:银行家算法是从当前状态出发,逐个按安全序列检查各客户谁能完成其工作,然后假定其完成工作且归还全部贷款,再进行综合检查下一个能完成的客户,以此类推。如果所有客户都能完成工作,则找到一个安全序列,银行卡才是安全的。