40---创建线程对象
糖果时间
代码开启进程和线程的方式基本是一致的,学会了开启进程就学会了开启线程
一 通过代码开启线程
- 开启线程不需要在main下面执行代码,直接书写即可,但是还是习惯性的将启动命令写在main下
1.0 不在main下执行线程
from threading import Thread
import time
def task(name):
print(f'{name} is run')
time.sleep(2)
print(f'{name} is over')
t = Thread(target=task,args=('tank',))
t.start()
print('主')
# 运行结果
tank is run
主
tank is over
# 分析:发现是先运行子线程,因为线程不需要重新开辟内存空,在开启线程的瞬间,线程就运行了。
1.1 第一种方式---类实例化产生线程对象
from threading import Thread
import time
def task(name):
print(f'{name} is run')
time.sleep(2)
print(f'{name} is over')
if __name__ == '__main__':
t = Thread(target=task,args=('tank',))
t.start()
print('主')
1.2 第二种方式---类的继承产生线程对象
from threading import Thread
import time
class MyThread(Thread):
def __init__(self,name):
# 针对双下划线开头双下划线结尾的方法,童话一读成 双下。。。
# 重写了父类的方法,但是不知道别人的方法具体数据,就调用父类的方法,添加自己的
super().__init__()
self.name = name
def run(self):
print(f'{self.name} is run')
time.sleep(1)
print(f'{self.name} is over')
if __name__ == '__main__':
t = MyThread('egon')
t.start()
print('主')
二 线程对象方法
- join()方法
主线程等待子线程结束再运行
from threading import Thread
import time
class MyThread(Thread):
def __init__(self,name):
super().__init__()
self.name = name
def run(self):
print(f'{self.name} is run')
time.sleep(2)
print(f'{self.name} is over')
if __name__ == '__main__':
t = MyThread('tank')
t.start()
t.join()
print('主')
- current_thread()
from threading import Thread,current_thread
import time
import os
class MyThread(Thread):
def __init__(self,name):
super().__init__()
self.name = name
def run(self):
print(f'{self.name} is run')
time.sleep(2)
print(f'{self.name} is over',current_thread().name,os.getpid())
if __name__ == '__main__':
t = MyThread('tank')
t.start()
t.join()
print('主',os.getpid())
# current_thread().name---当前进程的名称
# os.getpid()---当前线程所在的进程
- active_count()---判断存活的进程数
from threading import Thread,current_thread,active_count
import time
import os
class MyThread(Thread):
def __init__(self,):
super().__init__()
def run(self):
print('is run')
time.sleep(2)
print(f'is over',current_thread().name,os.getpid())
if __name__ == '__main__':
t = MyThread()
t.start()
print(active_count()) # 2
t.join()
print('主',os.getpid(),active_count()) # 1
三 同一个进程下的数据多个线程共享
from threading import Thread
money = 100
def foo():
global money
money = 666
print('子',money)
if __name__ == '__main__':
t = Thread(target=foo)
t.start()
print('主',money)
# 程序运行结果
子 666
主 666
四 守护线程
- 注意点:
主线程运行结束后不会立刻结束,会等待其他子线程全部运行完毕才会结束---因为主线程结束就意味着所在的进程结束,进程的内存空间会被回收。
4.1 通俗理解
皇上没死我就能或者,一旦皇上在我之前死了,那我也得被死亡...
4.2 官方理解
主线程结束了,子线程无论是否运行完毕也会被结束
4.3 代码案例---简单版
from threading import Thread
import time
def foo():
print('太监逍遥自在!')
time.sleep(3)
print('老子寿终正寝')
if __name__ == '__main__':
t = Thread(target=foo)
t.daemon = True
t.start()
print('吾主驾崩')
# 运行结果
太监逍遥自在!
吾主驾崩
4.4 代码案例---稍微复杂版
from threading import Thread
import time
def foo():
print(123)
time.sleep(1)
print(1234)
def func():
print(456)
time.sleep(3)
print(4567)
if __name__ == '__main__':
t1 = Thread(target=foo)
t2 = Thread(target=func)
t1.daemon = True
t1.start()
t2.start()
print('主')
# 运行结果
123
456
主
1234
4657
# 分析:t1是守护线程,但是主线程运行完毕后要等其他非守护线程执行完毕后才能结束主线程。
五 线程互斥锁
- 为了防止多个线程同时操作同一个数据造成数据错乱
from threading import Thread,Lock
import time
money = 100
mutex = Lock()
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.01)
money = tmp - 1
mutex.release()
if __name__ == '__main__':
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
六 GIL全局解释器锁
6.1 GIL介绍
python解释器有多个版本:
CPython
Jpython
Pypypython
在Cpython解释器中GIL是一把互斥锁,用来阻止同一进程下的多个线程的同时执行
同一个进程下的多个线程无法利用多核优势
产生的问题:是否由于GIL的存在,python的多线程是否一点用都没有呢?
GIL存在的原因
因为Cpython中的内存管理不是线程安全的。
内存管理---垃圾回收机制
1 应用技术
2 标记清除
3 分代回收
重点:
1 GIL不是python的特点,而是Cpython的特点
2 GIL遇到IO操作就会自动释放
2 GIL是保证解释器级别的数据的安全
3 GIL会导致同一个进程下的多个线程无法同时执行,即无法利用多核优势。
4 针对不同的数据还是要加不同的锁来处理
5 解释型语言的通病:同一个进程下多个线程无法利用多核优势
- 代码验证---GIL的验证
from threading import Thread,Lock
import time
money = 100
def task():
global money
tmp = money
# time.sleep(0.01)
money = tmp - 1
if __name__ == '__main__':
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
# 运行结果
0
分析:在没有IO操作的时候,不加任何互斥锁,多个线程同时操作一份数据,不会产生数据错乱,但是一旦模拟了延迟,GIL遇到IO操作就会立即释放GIL,其他线程就可争抢GIL,就会产生数据错乱,此时就需要加入线程互斥锁
- GIL+锁
from threading import Thread,Lock
import time
money = 100
mutex = Lock()
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.01)
money = tmp - 1
mutex.release()
if __name__ == '__main__':
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
# 运行结果
0
分析:
#1.100个线程去抢GIL锁,即抢执行权限
#2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
#3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
#4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
6.2 Cpython无法利用多核优势,是否意味着python就没有用了?
- 解决该问题的思路
#1. cpu到底是用来做计算的,还是用来做I/O的?
#2. 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能
#3. 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处
- 举例说明
一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。
如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,
反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高
- 结论
对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地。
#分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
#单核情况下,分析结果:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
#多核情况下,分析结果:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
#结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
6.3 多线程和多进程不同场景下对比
-
计算密集型
1 多进程计算
from multiprocessing import Process
from threading import Thread
import time
import os
def cal():
res = 1
for i in range(1,1000):
res *= i
print(res)
if __name__ == '__main__':
print(os.cpu_count())
p_list = []
start_time = time.time()
for i in range(8):
p = Process(target=cal)
p.start()
p_list.append(p)
for p in p_list:
p.join()
print(time.time()-start_time)
# 多进程下:0.1874539852142334s
2 多线程计算
from threading import Thread
import time
def cal():
res = 1
for i in range(1,1000):
res*=i
print(res)
if __name__ == '__main__':
t_list = []
start_time = time.time()
for i in range(8):
t = Thread(target=cal)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(time.time()-start_time)
# 0.015620231628417969
-
IO密集型
1 多线程
from threading import Thread import time def cal(): time.sleep(3) if __name__ == '__main__': t_list = [] start_time = time.time() for i in range(8): t = Thread(target=cal) t.start() t_list.append(t) for t in t_list: t.join() print(time.time()-start_time) # 运行结果 3.01137638092041
2 多进程
from multiprocessing import Process import time def cal(): time.sleep(3) if __name__ == '__main__': t_list = [] start_time = time.time() for i in range(8): t = Process(target=cal) t.start() t_list.append(t) for t in t_list: t.join() print(time.time()-start_time) # 运行结果 3.1901254653930664
6.4 总结
多进程和多线程都有各自的优势
并且我们后面在写项目的时候通常可以
多进程下面再开设多线程
这样的话既可以利用多核也可以减少资源消耗
6.5 额外重点总结---join与锁的区别
#有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.