什么是多任务?
简单地说,就是操作系统可以同时运行多个任务。实现多任务有多种方式,线程、进程、协程。
并行和并发的区别?
并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的
真的多任务叫并行,假的多任务叫并发。
什么是线程?
可以简单理解为同一进程中有多个计数器,每个线程的执行时间不确定,而每个进程的时间片相等,线程是操作系统调度执行的最小单位。
python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用。
demo
import time
import threading
def sing():
"""唱歌 5秒钟"""
for i in range(5):
print("----正在唱:菊花茶----")
time.sleep(1)
def dance():
"""跳舞 5秒钟"""
for i in range(5):
print("----正在跳舞----")
time.sleep(1)
def main():
t1 = threading.Thread(target=sing)
t2 = threading.Thread(target=dance)
t1.start()
t2.start()
if __name__ == "__main__":
main()
Import threading
# 创建一个线程对象
t1 = threading.Thread(target=func_name, args=(num,), name=”子线程名字”)
# 创建一个线程并启动
t1.start()
# 等待子线程执行完毕之后再继续向下执行主线程
t1.join()
"""
备注:主线程会等待子线程结束之后才会结束,主线程一死,子线程也会死。线程的调度是随机的,并没有先后顺序。
"""
枚举函数enumerate的用法
enumerate()是python的内置函数,在字典上是枚举、列举的意思。用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。通过threading.enumerate()就可以获取线程列表。
多线程注意事项
- 多线程程序的执行顺序是不确定的。
- 线程的启动顺序、run函数中每次循环的执行顺序都不能确定。
- 每个线程默认都有一个名字。
- 当线程的run()方法结束时该线程完成。
- 无法控制线程调度程序,但可以通过别的方式来影响线程的调度方式。
多线程共享全局变量
import threading
import time
# 定义一个全局变量
num = 100
def test1():
global num
num += 1
print("-----test1 num = %d" % num)
def test2():
print("-----test2 num = %d" % unm)
def main():
t1 = threading.Thread(target=test1)
t2 = threading.Thread(target=test1)
t1.start()
time.sleep(1)
t2.start()
time.sleep(1)
print("------main num = %d ----" % num)
if __name__ == "__main__":
main()
多线程资源竞争
import threading
import time
# 定义一个全局变量
g_num = 0
def test1(num):
global g_num
for i in range(num):
g_num += 1
print("-----in test1 g_num=%d----" % g_num)
def test2(num):
global g_num
for i in range(num):
g_num += 1
print("-----in test2 g_num=%d=----" % g_num)
def main():
t1 = threading.Thread(target=test1, args=(1000000,))
t2 = threading.Thread(target=test2, args=(1000000,))
t1.start()
t2.start()
# 等待上面的2个线程执行完毕....
time.sleep(5)
print("-----in main Thread g_num = %d---" % g_num)
if __name__ == "__main__":
main()
互斥锁
- 当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。
- 线程同步能够保证多个线程安全的访问竞争资源,最简单的同步机制是引入互斥锁。
- 互斥锁为资源引入一个状态:锁定/非锁定
- 当某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到改线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
互斥锁的运用
# 由于多线程之间共享全局变量就会导致出现资源竞争的问题,为了避免这种竞争出现,利用互斥锁可以实现线程同步。
# 创建锁
Mutex = threading.Lock()
# 加锁
Mutex.acquire()
# 释放锁
Mutex.release()
demo
使用互斥锁解决资源竞争
import threading
import time
# 定义一个全局变量
g_num = 0
def test1(num):
global g_num
# 上锁,如果之前没有被上锁,那么此时 上锁成功
# 如果上锁之前 已经被上锁了,那么此时会堵塞在这里,直到 这个锁被解开位置
mutex.acquire()
for i in range(num):
g_num += 1
# 解锁
mutex.release()
print("-----in test1 g_num=%d----" % g_num)
def test2(num):
global g_num
mutex.acquire()
for i in range(num):
g_num += 1
mutex.release()
print("-----in test2 g_num=%d=----" % g_num)
# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()
def main():
t1 = threading.Thread(target=test1, args=(1000000,))
t2 = threading.Thread(target=test2, args=(1000000,))
t1.start()
t2.start()
# 等待上面的2个线程执行完毕....
time.sleep(2)
print("-----in main Thread g_num = %d---" % g_num)
if __name__ == "__main__":
main()
注意
- 如果这个锁之前是没有上锁的,那么acquire不会阻塞
- 如果在调用acquire对这个锁上锁之前,它已经被其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止。
上锁过程
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为"阻塞",直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入"unlocked"状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
互斥锁的运用
# 由于多线程之间共享全局变量就会导致出现资源竞争的问题,为了避免这种竞争出现,利用互斥锁可以实现线程同步。
# 创建锁
Mutex = threading.Lock()
# 加锁
Mutex.acquire()
# 释放锁
Mutex.release()
?
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整的执行。
锁的坏处:
阻止了多线程并发执行,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
什么是死锁?
- 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
- 在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,尽管死锁很少发生,但一旦发生就会造成应用的停止响应。
如何解决死锁?
程序设计时要尽量避免
添加超时时间等
银行家算法
银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。
exit(?)