多任务的概念
1. 什么是多任务
同时可以运行多个任务
2. 多任务执行的原理
操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒再切换到任务3…这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。
3. 并行和并发
并发: 指的是任务数多于cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)
并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的。
真正的"并行"只能在多核cpu上实现,现实中由于任务数量远远多于CPU的核心数量,所以基本上都是"并发"。操作系统会自动把很多任务轮流调度到每个核心之上。
线程
1. 什么是线程
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
2.通过线程实现多任务
在Python中如果想使用线程实现多任务,可以使用threading模块,它是对thread做了一些包装的,可以更加方便的使用。
import threading
import time
# 单线程执行
def read():
print("学习中,请安静")
time.sleep(1)
if __name__ == '__main__':
for i in range(5):
read()
# 多线程执行
def eat():
print("好好吃饭, 努力长高")
time.sleep(1)
if __name__ == '__main__':
for i in range(5):
# def __init__(self, group=None, target=None, name=None,args=(), kwargs=None, *, daemon=None)
# 根据源码可以看出,在Thread类中,初始化中有以上参数,重要的分别是target传入函数名,args以元祖的形式传入参数,kwargs以字典的形式传入参数
t = threading.Thread(target=eat)
# 开启线程
t.start()
# 同时执行不同的任务
def study():
print("好好学习, 天天向上")
time.sleep(1)
def eat():
print("好好吃饭, 努力长高")
time.sleep(1)
if __name__ == '__main__':
for i in range(5):
t1 = threading.Thread(target=study)
t2 = threading.Thread(target=eat)
t1.start()
t2.start()
可以明显看出使用多线程并发的操作,花费时间要短很多。
当调用start() 时,才会真正的创建线程,并且开始执行
主线程会等待所有的子线程结束后才结束
3.创建线程时传递参数
为了能够在创建线程时,让这个线程执行不一样的事情,可以在使用threading.Thread创建线程时,传递一些参数给它。
# 创建线程时传递参数
def a(name):
print("我的名字是:", name)
def b(food, exercise):
print("我喜欢吃%s和我喜欢%s" % (food, exercise))
def c(name, food):
print("我的名字是%s, 我喜欢吃%s" % (name, food))
if __name__ == '__main__':
# 创建线程
t1 = threading.Thread(target=a, args=("小明",))
# 创建线程时传递多个参数
t2 = threading.Thread(target=b, args=("鱼", "跑步",))
# 传递命名参数
t3 = threading.Thread(target=c, kwargs={"name": "小红", "food": "猪蹄"})
# 开启线程
t1.start()
t2.start()
t3.start()
运行结果:
总结:
在使用Thread创建线程的时候,args是传递一个元祖,元祖中的数据个数与target指定的函数形参个数、顺序一样,kwargs是一个字典里面的key当做形参中的变量名字,value是给这个形参传递的数值。
4.用面向对象的形式创建线程
- 定义一个新的类,继承Thread类
- 在类中实现run方法
- 在run方法中写入要执行的代码
- 使用这个类创建一个对象,调用这个对象的start方法开启线程
class MyThread(threading.Thread):
def run(self):
for i in range(3):
msg = "我的名字是" + self.name + " @ " + str(i)
print(msg)
time.sleep(1)
if __name__ == '__main__':
t = MyThread()
t.start()
运行结果:
5.多线程共享全局变量
在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
list1 = [11, 22, 33]
def work1():
list1.append(44)
print("----work1----", list1)
def work2():
time.sleep(1)
print("----work2----", list1)
if __name__ == '__main__':
w1 = threading.Thread(target=work1)
w2 = threading.Thread(target=work2)
w1.start()
w2.start()
运行结果:
----work1---- [11, 22, 33, 44]
----work2---- [11, 22, 33, 44]
缺点: 线程对全局变量的随意更改可能会造成多线程之间对全局变量的混乱(即线程是不安全的)
6.多线程共享全局变量问题
g_num = 0
def work1(num):
global g_num
for i in range(num):
g_num += 1
print("---in work1, g_num is %d---" % g_num)
def work2(num):
global g_num
for i in range(num):
g_num += 1
print("----in work2, g_num is %d----" % g_num)
print("---线程创建之前g_num is %d---" % g_num)
t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
# threading.enumerate()能够得到当前这个程序中正在运行的所有任务,是一个列表
while len(threading.enumerate()) != 1:
time.sleep(1)
print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
运行结果:
—线程创建之前g_num is 0—
—in work1, g_num is 1446302—
----in work2, g_num is 1548996----
2个线程对同一个全局变量操作之后的最终结果是:1548996
如果多个线程同时对一个全局变量操作,会出现资源竞争问题,从而导致结果不正确,如果要保证多线程安全访问竞争资源,就是要引入互斥锁.
互斥锁
互斥锁为资源引入一个状态: 锁定/非锁定
某个线程要更改共享数据时,先将其锁定,此时资源的状态为"锁定",其他线程不能更改;直到该线程释放资源后,将资源的状态变为"非锁定",其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进入写入操作,从而保证了多线程情况下数据的正确性。
使用互斥锁
threading模块中定义了Lock类可以方便使用
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
使用互斥锁解决资源竞争的问题
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range(num):
# 锁定
mutex.acquire()
g_num += 1
# 释放
mutex.release()
print("---in work1, g_num is %d---" % g_num)
def work2(num):
global g_num
for i in range(num):
mutex.acquire()
g_num += 1
mutex.release()
print("----in work2, g_num is %d----" % g_num)
print("---线程创建之前g_num is %d---" % g_num)
# 创建锁
mutex = threading.Lock()
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个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
运行结果:
—线程创建之前g_num is 0—
—in work1, g_num is 1828602—
----in work2, g_num is 2000000----
2个线程对同一个全局变量操作之后的最终结果是:2000000
总结
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
防止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
由于可以存着多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁