多任务
什么是多任务
生活中,你可能一边听歌,一边写作业;一边抱着孩子,一边打着电话;一边干活,一边聊天。。。这些都是生活中的多任务场景。电脑上的多任务,一边运行音乐程序,一边用Google Chrome写笔记;一边用Google Chrome写着笔记,一边用Google Chrome查资料。。。。这些事实电脑上的多任务。
从上面可以简单归纳一下,多任务就是同一时间内做多件事情 或者 同一时间内运行多个程序。电脑上是由CPU执行多任务的,CPU从单核到现在的多核都能实现多任务,为什么单核CPU也能实现多任务?这就不得不说操作系统了,分时操作系统[1]是使一台计算机采用时间片轮转的方式同时为几个、几十个甚至几百个用户服务的一种操作系统,通过时间片的轮转CPU切换任务,因为CPU运行的足够快,所以感觉是多个任务同时执行。
并行与并发[2]
当CPU的核心数小于要执行的任务时,也就是所谓的并发[3]。
当CPU的核心数大于或等于要执行的任务时,也就是所谓的并行[4]。
简单理解,并行就是一段时间内我可以边看电视,边聊天。并发就像是使用电脑,一会敲敲键盘一会点点鼠标。
线程
CPU执行多任务,实际上是CPU 调度对应的线程去工作,也可以说线程是CPU 调度的基本单位。
什么是线程
可以理解成执行代码的分支,线程是执行对应的代码的。那么线程怎么完成多任务?
单线程
模拟人的使用电脑
import time
# 敲键盘ing
def keyboard():
for i in range(5):
print("敲键盘ing...")
time.sleep(0.5)
# 点鼠标ing
def mouse():
for i in range(5):
print("点鼠标ing...")
time.sleep(1)
if __name__ == '__main__':
keyboard()
mouse()
# 运行结果:
# 敲键盘ing...
# 敲键盘ing...
# 敲键盘ing...
# 敲键盘ing...
# 敲键盘ing...
# 点鼠标ing...
# 点鼠标ing...
# 点鼠标ing...
# 点鼠标ing...
# 点鼠标ing...
这是一个单线程单任务,不能实现我们的需求。有没有其他方式?
多线程
既然一个线程只能运行一个任务,我们有两个线程,所以应该至少有两个线程,才能满足我们的需求。
Python 中开启多线程可以使用threading 模块。
import threading
import time
# 敲键盘ing
def keyboard():
for i in range(5):
print("敲键盘ing...")
time.sleep(0.5)
# 点鼠标ing
def mouse():
for i in range(5):
print("点鼠标ing...")
time.sleep(1)
if __name__ == '__main__':
# 为了方便,我们使用两个线程分别执行 keyboard和 mouse任务
print("开辟前,当前有%s个线程在活动,他们是%s" %
(threading.active_count(), threading.enumerate()))
keyboard_thread = threading.Thread(target=keyboard)
mouse_thread = threading.Thread(target=mouse)
print("开辟后,当前有%s个线程在活动,他们是%s" %
(threading.active_count(), threading.enumerate()))
keyboard_thread.start()
mouse_thread.start()
print("start后,当前有%s个线程在活动,他们是%s" %
(threading.active_count(), threading.enumerate()))
time.sleep(2)
# 使用完毕
print("play over")
# 运行结果:
# 开辟前,当前有1个线程在活动, 他们是[ < _MainThread(MainThread, started
# 140005808121600) >]
# 开辟后,当前有1个线程在活动, 他们是[ < _MainThread(MainThread, started
# 140005808121600) >]
# 敲键盘ing...
# start后,当前有3个线程在活动, 他们是[ < _MainThread(MainThread, started
# 140005808121600) >, < Thread(Thread - 1, started
# 140005782394624) >, < Thread(Thread - 2, started
# 140005774001920) >]
# 点鼠标ing...
# 敲键盘ing...
# 点鼠标ing...
# 敲键盘ing...
# 敲键盘ing...
# 点鼠标ing...
# play
# over
# 敲键盘ing...
# 点鼠标ing...
# 点鼠标ing...
分析结果
- 可以发现开辟前就有一个线程,为什么呢?因为我们写的代码要执行,而线程就是执行代码的,所以开辟线程前就有一条线程符合逻辑,不然谁去执行我们的代码呢?这条默认的线程有另一个称呼叫做主线程。
- threading.Thread() 只是准备创建一条子线程,并没有创建(开辟后,当前有1个线程在活动)
- bathe_thread.start() 的时候,才创建了子线程并运行起来(start后,当前有3个线程在活动)
- 线程之间执行是无序(输出结果无序,当然效果不太明显,你可以多循环几次,或者多运行几次看看结果是不是一样)
- 主线程结束后,子线程并未结束;子线程结束后,主线程结束,说明主线程默认会等待子线程执行结束。(start后,当前有3个线程在活动是主程序的最后一行,主程序运行在主线程上)
有一个问题,我们都是用完电脑了,怎么还有输出?或者说怎么让子线程跟随主线程一起结束?
设置守护线程
import threading
import time
# 敲键盘ing
def keyboard():
for i in range(5):
print("敲键盘ing...")
time.sleep(0.5)
# 点鼠标ing
def mouse():
for i in range(5):
print("点鼠标ing...")
time.sleep(1)
if __name__ == '__main__':
keyboard_thread = threading.Thread(target=keyboard)
mouse_thread = threading.Thread(target=mouse)
# 设置守护,注意必须在start之前执行
keyboard_thread.setDaemon(True)
mouse_thread.daemon = True
keyboard_thread.start()
mouse_thread.start()
time.sleep(2)
# 使用完毕
print("play over")
# 运行结果:
# 敲键盘ing...
# 点鼠标ing...
# 敲键盘ing...
# 点鼠标ing...
# 敲键盘ing...
# 敲键盘ing...
# 点鼠标ing...
# play over
设置守护线程有两种方式,通过thread 对象的setDaemon() 方法,或者thread 对象的daemon 属性设置子线程守护主线程,当主线程结束,子线程被销毁。
小结:
- 获取活动线程的数量
- len(threading.enumerate())
- threading.active_count()
- 获取执行当前代码的线程对象
- threading.current_thread()
- 线程之间的执行是无序的
- 默认情况下,主线程会等待所有子线程都执行完,程序才结束
- 设置守护线程( setDaemon() daemon ),主线程结束,子线程被销毁
- 注意点:主线程里的子线程如果有一个没有设置守护,那么主线程会等待这个子线程执行结束,因为主线程没有结束,所以守护线程不会被销毁。
- 注意点:设置守护线程( setDaemon() daemon ),要在start() 之前设置,否则会报错(RuntimeError: cannot set daemon status of active thread)。
自定义线程
手动设置守护线程稍微有些麻烦,万一忘了设置守护就得不到预期效果,怎么让得到的线程就是一个守护线程?自定义线程,设置守护。
import threading
import time
class DaemonThread(threading.Thread):
"""自定义线程 - 默认守护"""
def __init__(
self, target=None, name=None, args=(), kwargs=None, ):
# 调用父类初始化,设置daemon 为True
super(DaemonThread, self).__init__(
target=target, name=name, args=args, kwargs=kwargs, daemon=True)
# 敲键盘ing
def keyboard():
for i in range(5):
print("敲键盘ing...")
time.sleep(0.5)
# 点鼠标ing
def mouse():
for i in range(5):
print("点鼠标ing...")
time.sleep(1)
if __name__ == '__main__':
keyboard_thread = threading.Thread(target=keyboard)
mouse_thread = threading.Thread(target=mouse)
# 设置守护,注意必须在start之前执行
keyboard_thread.setDaemon(True)
mouse_thread.daemon = True
keyboard_thread.start()
mouse_thread.start()
time.sleep(2)
# 使用完毕
print("play over")
# 运行结果:
# 敲键盘ing...
# 点鼠标ing...
# 敲键盘ing...
# 点鼠标ing...
# 敲键盘ing...
# 敲键盘ing...
# play over
从结果上看通过自定义我们得到了想要的效果。那么有一个问题,什么问题呢?问题就是什么是自定义线程?
有人说自定义线程必须重写父类的run 方法,有人说不需要(就是我),怎么理解呢?
我是这么认为的,首先怎么自定义一个类,或者说怎么定义一个类,class 类名(父类名): 在实现一些逻辑就成为了一个类,一个自己定义的类,那么必须要重写__init__()方法 或者 __new__()方法吗?显然是看需求,类比一下,自定义一个线程必须要重写run() 方法吗?我的run() 方法不需要自己取实现什么逻辑,为什么要去重写呢?
上面是类比,下面反推一下,根据结果可以看出DaemonThread()具有线程的功能(那是必须的DaemonThread 类是Thread类的子类),所以DaemonThread 是一个自定义线程类。
问最简单的自定义线程是什么?根据上面的理解可以得出,继承threading.Tread 然后 pass(空实现)这就是一个最简单的自定义线程。
多线程与全局变量
我想看看娱乐了多少次,做一个记录
import threading
import time
# 娱乐次数
amusement_counter = 0
# 吃饭ing
def eat():
global amusement_counter
for i in range(5):
# print("吃饭ing...")
amusement_counter += 1
time.sleep(0.1)
# 唱歌ing
def sing():
global amusement_counter
for i in range(5):
# print("唱歌ing...")
amusement_counter += 1
time.sleep(0.2)
# 洗澡ing
def bath():
for i in range(5):
global amusement_counter
# print("洗澡ing...")
amusement_counter += 1
time.sleep(0.3)
if __name__ == '__main__':
# 为每个任务开辟一个线程
eat_thread = threading.Thread(target=eat)
sing_thread = threading.Thread(target=sing)
bath_thread = threading.Thread(target=bath)
# 启动线程
eat_thread.start()
sing_thread.start()
bath_thread.start()
time.sleep(2)
print(amusement_counter)
# 结果为
# 15
可以看出线程之间共享全局变量,共享处处有啊,共享单车遍地开花,但是共享单车的问题也不少,那么共享的全局变量会不会有问题呢?往下看
# 使用多线程对全局变量进行累加,为了更方便的体现问题,所以每个任务对全局变量累加1000000次
import threading
counter = 0
def func():
global counter
for i in range(1000000): # 100,0000
counter += 1
print("func ", counter)
def func1():
global counter
for i in range(1000000): # 100,0000
counter += 1
print("func1 ", counter)
if __name__ == '__main__':
func_thread = threading.Thread(target=func)
func1_thread = threading.Thread(target=func1)
# 启动线程
func_thread.start()
func1_thread.start()
# 结果为:
# func 1476347
# func1 1577818
从结果看出和我们想要的结果不一样啊,对全局变量总共累加了200,0000次 ,但是得到的结果却小于这个数,为什么呢?先看一个小游戏,使用python实现一下。
有一辆车,两个人去抢,谁抢到则胜利场数加一,同时抢到则两人的胜利场数不变。
import threading
import random
# 两个人的胜利场数
winA = 0
winB = 0
# 游戏的回合数
counter = 0
if __name__ == '__main__':
for i in range(10):
counter += 1
# 使用随机数,为1则记为胜利,胜利数加1
flagA = random.randint(0, 1)
flagB = random.randint(0, 1)
if flagA == flagB:
continue
if flagA:
winA += 1
if flagB:
winB += 1
print("游戏总场数%d" % counter)
print("A胜利%d场" % winA)
print("B胜利%d场" % winB)
# 结果为:
# 游戏总场数10
# A胜利3场
# B胜利4场
分析结果,游戏总共有10个回合,A的胜场+B的胜场 等于 7个回合,比游戏回合少。是不是感觉有些熟悉?上面我们对全局变量总共累加了200,0000次得到了一个小于200,0000次的数据。
类比一下,全局变量就是我们的车,两个子线程A线程与B线程就相当于A与B,他们同时去取值得到的是同一个值,比如同时取到100,那么A线程操作完全局变量是101,B线程操作完也是101,所以两次累加操作,只有一次是有效的。也就是所谓的资源竞争问题,怎么解决资源竞争?排队,让A线程先去执行,执行完了,B线程再去执行,
import threading
import time
counter = 0
def func():
global counter
for i in range(1000000): # 100,0000
counter += 1
print("func ", counter)
def func1():
global counter
for i in range(1000000): # 100,0000
counter += 1
print("func1 ", counter)
if __name__ == '__main__':
func_thread = threading.Thread(target=func)
func1_thread = threading.Thread(target=func1)
# 启动线程
func_thread.start()
# 获取当前时间
join_start = time.time()
# 主线程等待子线程执行结束,在继续执行
func_thread.join()
# 计算时间差
print(time.time() - join_start)
func1_thread.start()
# 运行结果:
# func 1000000
# 0.153519868850708 # 相差0.15,也就是说程序在join 的时候睡眠的大概0.15秒
# func1 2000000
通过线程协同(join() 方法),虽然完成了我们的需求,但是也让我们的多任务变成了单人任务,有没有其他方式呢?
互斥锁
通过加锁的方式也可以解决资源竞争问题。
import threading
import time
lock = threading.Lock() # 得到一把锁
counter = 0
def func():
lock.acquire() # 加锁
global counter
for i in range(1000000): # 100,0000
counter += 1
print("func ", counter)
lock.release() # 解锁
def func1():
lock.acquire()
global counter
for i in range(1000000): # 100,0000
counter += 1
print("func1 ", counter)
lock.release()
if __name__ == '__main__':
func_thread = threading.Thread(target=func)
func1_thread = threading.Thread(target=func1)
# 启动线程
func_thread.start()
func1_thread.start()
# 运行结果:
# func 1000000
# func1 2000000
互斥锁也可以解决资源竞争的问题,但是也让我们的多任务变为了单任务。
验证同步,互斥锁让多任务变为单任务
验证同步
import threading
counter = 0
def func():
global counter
for i in range(1000000): # 100,0000
counter += 1
if i % 100000 == 0:
print("func ")
print("func ", counter)
def func1():
global counter
for i in range(1000000): # 100,0000
counter += 1
if i % 100000 == 0:
print("func1 ")
print("func1 ", counter)
if __name__ == '__main__':
func_thread = threading.Thread(target=func)
func1_thread = threading.Thread(target=func1)
# 启动线程
func_thread.start()
func_thread.join()
func1_thread.start()
# 运行结果:
# func
# func
# func
# func
# func
# func
# func
# func
# func
# func
# func 1000000
# func1
# func1
# func1
# func1
# func1
# func1
# func1
# func1
# func1
# func1
# func1 2000000
# 通过结果可以看出,先执行了func,执行完了之后,又执行的func1
验证锁
import threading
import time
lock = threading.Lock() # 得到一把锁
counter = 0
def func():
lock.acquire() # 加锁
global counter
for i in range(1000000): # 100,0000
counter += 1
if i % 100000 == 0:
print("func ")
print("func ", counter)
lock.release() # 解锁
def func1():
lock.acquire()
global counter
for i in range(1000000): # 100,0000
counter += 1
if i % 100000 == 0:
print("func1 ")
print("func1 ", counter)
lock.release()
if __name__ == '__main__':
func_thread = threading.Thread(target=func)
func1_thread = threading.Thread(target=func1)
# 启动线程
func_thread.start()
func1_thread.start()
# 运行结果:
# func
# func
# func
# func
# func
# func
# func
# func
# func
# func
# func 1000000
# func1
# func1
# func1
# func1
# func1
# func1
# func1
# func1
# func1
# func1
# func1 2000000
# 通过结果可以看出,先执行了func,执行完了之后,又执行的func1
小结:
- 多线程共享全局变量,但共享全局变量会产生资源竞争问题
- 线程同步(join())和互斥锁都能解决资源竞争问题,但是都是将多任务变成了单任务。
- 使用锁要避免死锁
- 注意点:如果两个子线程共享全局变量,只对一个子线程加锁,可以理解为和没加锁一样。
- 注意点:效率问题,同样是累加200,0000次,直接遍历累加200,0000次所用时间小于线程同步所用时间小于互斥锁所用时间
到此结 DragonFangQy 2018.4.26
欢迎指正
感谢!
-
分时操作系统 ↩
-
并行与并发 ↩
-
并发 ↩
-
并行 ↩