目录
一,CPU中的线程
下面从CPU的角度来解释线程。
1,CPU之霸,线程撕裂者
大家都听说过线程撕裂者,实际上他是一款64核心128线程的CPU,优点在于能够应对大规模的并发处理运算。一般默认的是一个核心对应一个线程,由于超线程技术,可以使一个核心拥有两个或多个线程。
2,CPU核心与进程的关系?
CPU核心与进程的关系?其实两者关系并不大。CPU核心是一个物理逻辑单元,负责逻辑运算,他的频率越高越爽,全核10GHZ。
3,什么是进程?
进程是程序运行资源分配的最小单位 。其中资源包括:CPU、内存空间、 磁盘 IO 等,CPU只是负责数据处理与计算。某个进程用哪几个核心,用核心的哪几个线程来执行都是操作系统说了算。我们在代码中只能决定建立几个进程或建立几个线程,然后由操作系统去帮我们分配CPU资源。简单点理解可以认为,进程就是存在内存里的指令集合,是一块完整的内存空间,他等待着操作系统给他分配CPU资源。不同进程之间相互隔离。
4,CPU核心与CPU线程的关系?
CPU核心的目的就是搭载CPU线程。也就是CPU线程依赖于CPU核心来物理实现。
5,CPU线程与编程里的线程有什么区别?
答案是本质上是同一个东西。线程是指:同一时刻设备能并行执行的程序个数。
假如有一个单核单线程的CPU,那么这个CPU在某一时刻只能执行一个一个程序的指令。假如你开了2个软件,这两个软件产生了两个进程,这两个进程又产生了4个线程,那么CPU就会轮流的执行这四个线程,执行的快慢得看CPU的频率,频率越高处理越快。由此看出,主频越高的CPU处理多线程越是得劲。
假如有一个4核8线程的CPU,那么这个CPU在某一时刻能同时并发执行8个程序的指令。假如你开了2个软件,这两个软件产生了两个进程,这两个进程又产生了4个线程,那么直接拿出两个物理核心来执行就行了,这时候的CPU就没有单核单线程的卡了。当然如果产生了16个线程,那么也只能用CPU的8线程去轮流负责产生的16个进程了。
依据CPU的时间片轮转机制,假如一个线程要轮流执行两个程序,每个程序500ms,如果500ms内一个程序都还在计算,那么操作系统直接暂停计算,转而去执行另一个程序,但是另一个程序10ms就执行完成了,那么操作系统会立即来执行上次还在计算的那个程序,进而节省490ms。
由此可见,线程是CPU真正出力的地方,也是操作系统能够分配的最小单位。
6,线程与协程的关系?
线程里面包含协程。因为线程是CPU真正出力的地方,他负责计算。从内存里拿数据,将计算数据放到内存,这两个操作期间,CPU只能干等着。协程就是充分利用这些空闲时间。这就是一个i9的CPU带一个ddr1333的内存条和一个机械硬盘的机器仍然是垃圾的原因。
协程相当于在线程里实现并发,但是操作系统只能管到线程,那么协程只能靠程序员来管理,比如如何让自己写的功能函数在一个线程里轮流执行,从而实现高效率。一般在一个函数进行io时,跳转到其他函数,当io完成时再回来处理读取的数据。
二,python线程的创建
python线程的一般使用threading模块的Thread类实现。
1,方法包装实现线程创建
使用Thread类生成线程对象。
from threading import Thread
from time import sleep
def func1(name):
print(f"线程{name},start") # format
for i in range(3):
print(f"线程{name},{i}")
sleep(3)
print(f"线程{name},end")
if __name__ == '__main__': # 当前程序运行时,生成一个进程
print("主线程,start") # 程序运行时,生成一个进程,并主动生成一个主线程
# 创建线程
t1 = Thread(target=func1, args=("t1",)) # 使用threading模块中的Thread类创建两个线程对象,target参数是要绑定的函数或者方法
# 绑定函数或者方法的参数,需要把这些参数放进一个元组args里
t2 = Thread(target=func1, args=("t2",))
# 启动线程
t1.start()
t2.start()
print("主线程,end")
# 主线程,start
# 线程t1,start
# 线程t1,0
# 线程t2,start
# 线程t2,0
# 主线程,end
# 线程t1,1
# 线程t2,1
# 线程t1,2
# 线程t2,2
# 线程t1,end
# 线程t2,end
# 主线程在子线程之前就结束了
2,类包装实现创建线程
继承Thread类,并重写run方法,或者在run方法里运行引用进来的函数。
from threading import Thread
from time import sleep
class MyThread(Thread): # 继承Thread类
def __init__(self,name):
Thread.__init__(self) # 调用父类的__init__方法,获取该方法里定义的各种属性
self.name = name
def run(self): # 重写run方法,这个run方法就是创建的线程对象所绑定的函数方法
print(f"线程{self.name},start") # format
for i in range(3):
print(f"线程{self.name},{i}")
sleep(3)
print(f"线程{self.name},end")
if __name__ == '__main__':
print("主线程,start")
#创建线程
t1 = MyThread("t1")
t2 = MyThread("t2")
#启动线程
t1.start()
t2.start()
print("主线程,end")
# 主线程,start
# 线程t1,start
# 线程t1,0
# 线程t2,start
# 线程t2,0
# 主线程,end
# 线程t2,1
# 线程t1,1
# 线程t1,2线程t2,2
#
# 线程t2,end
# 线程t1,end
# 出现打印在一行的原因是因为,在执行打印'\n'时,该线程被切换轮转。等到下次轮到自己时再打印'\n'
3,线程对象的jion()方法
join()方法可以实现主线程等待子线程执行完成后再执行主线程。
from threading import Thread
from time import sleep
def func1(name):
for i in range(3):
print(f"thread:{name} :{i}")
sleep(1)
if __name__ == '__main__':
print("主线程,start")
# 创建线程
t1 = Thread(target=func1, args=("t1",))
t2 = Thread(target=func1, args=("t2",))
# 启动线程
t1.start()
t2.start()
# 主线程会等待t1,t2结束后,再往下执行
t1.join()
t2.join()
print("主线程,end")
# 主线程,start
# thread:t1 :0
# thread:t2 :0
# thread:t1 :1
# thread:t2 :1
# thread:t1 :2
# thread:t2 :2
# 主线程,end
4,守护进程
线程对象有一个setDaemon()方法,可以设置该线程是否为守护进程。所谓守护进程,是指:当主线程结束时,守护进程也随之结束,与主线程共存亡。
from threading import Thread
from time import sleep
class MyThread(Thread):
def __init__(self, name):
Thread.__init__(self)
self.name = name
def run(self):
for i in range(3):
print(f"thread:{self.name} :{i}")
sleep(1)
if __name__ == '__main__':
print("主线程,start")
# 创建线程(类的方式)
t1 = MyThread('t1')
# t1设置为守护线程
#t1.setDaemon(True)
t1.daemon = True # 令线程t1的属性daemon为True,也可以使用t1.setDaemon(True)
# t1.setDaemon(True)
# 启动线程
t1.start()
print("主线程,end")
# 主线程,start
# thread:t1 :0
# 主线程,end
# 可以看到2和3都没有打印出来就结束了
三,全局GIL锁
GIL全称全局解释器锁。是指:python的解释器Cpython默认任何时刻只有一个线程在解释器中运行。这是Cpython解释器的缺陷,而不是python的特性。毕竟20多年前的人怎么知道现在的CPU都128线程了呢。
四,线程同步与互斥锁
假如存在一个列表,A线程对其修改,B进程对其访问,假如A,B线程同时去修改和访问则会出现问题,一个数据明明要被A线程删掉的,结果B线程抢先一步就把这个数据访问到了。那么如何解决这个问题呢?可以使用线程同步。
线程同步是指:多个线程访问一个对象时,请线程们先排好队,一个一个使用。
可以使用互斥锁来实现线程同步。互斥锁的原理就是:先把需要访问的数据上锁,留下一把钥匙。当很多线程来访问时,哪个线程拿到了钥匙,谁就去开锁访问数据。注意钥匙是多个线程抢来的,而不是智能分配的。
threading 模块中定义了 Lock 变量,这个变量本质上是一个函数,通过调用这个函数可以获取一把互斥锁。
# 使用互斥锁的案例
from threading import Thread, Lock
from time import sleep
class Account:
def __init__(self, money, name):
self.money = money
self.name = name
# 模拟提款的操作
class Drawing(Thread):
def __init__(self, drawingNum, account):
Thread.__init__(self)
self.drawingNum = drawingNum
self.account = account
self.expenseTotal = 0
def run(self):
lock1.acquire() # 获得钥匙
if self.account.money < self.drawingNum:
print("账户余额不足!")
return
sleep(1) # 判断完可以取钱,则阻塞。就是为了测试发生冲突问题
self.account.money -= self.drawingNum
self.expenseTotal += self.drawingNum
lock1.release() # 交出钥匙
print(f"账户:{self.account.name},余额是:{self.account.money}")
print(f"账户:{self.account.name},总共取了:{self.expenseTotal}")
if __name__ == '__main__':
a1 = Account(1000, "python")
lock1 = Lock()
draw1 = Drawing(600, a1) # 使用组合,定义一个取钱的线程
draw2 = Drawing(450, a1) # 使用组合,定义一个取钱的线程
draw1.start()
draw2.start()
# 账户:python,余额是:400账户余额不足!
#
# 账户:python,总共取了:600
在锁的acquire()与release()方法之间的代码会在获得钥匙的线程里全部执行完毕,而不会中途被挂起。
五,死锁
死锁是指:当一个线程需要多把钥匙时,而另一个线程也需要相同的钥匙,由于错位产生的程序抱死。如:A线程需要1,2两把钥匙才能运行,B线程也需要1,2两把钥匙才能运行,当A线程拿到钥匙1而B线程拿到了钥匙2,导致A线程永远拿不到钥匙2,B线程永远拿不到钥匙1,两个线程相互挂起,程序死机。解决方法就是:避免一个线程拿多把钥匙。
from threading import Thread, Lock
from time import sleep
def fun1():
lock1.acquire() # 这个线程拿到第一把钥匙
print('fun1拿到菜刀')
sleep(2)
lock2.acquire() # 这个线程打算拿第二把钥匙,但是第二把钥匙已经被另一个线程拿走了,该线程会在这里一直挂起
print('fun1拿到锅')
lock2.release()
print('fun1释放锅')
lock1.release()
print('fun1释放菜刀')
def fun2():
lock2.acquire() # 这个线程拿到第二把钥匙
print('fun2拿到锅')
lock1.acquire()
print('fun2拿到菜刀')
lock1.release()
print('fun2释放菜刀')
lock2.release()
print('fun2释放锅')
if __name__ == '__main__':
lock1 = Lock()
lock2 = Lock()
t1 = Thread(target=fun1)
t2 = Thread(target=fun2)
t1.start()
t2.start()
六,信号量
我们使用互斥锁主要是避免多个线程对同一个对象同时进行修改与访问,但是并不一定限制对对象的访问。使用信号量可以限制同时访问一个对象的线程数,当限制的个数为1时,则类似于互斥锁。
# 一个房子,一次只允许两个人进来
from threading import Semaphore, Thread
from time import sleep
def home(name, se):
se.acquire() # 该方法执行一次,信号量计数加一
print(f"{name}进入房间")
sleep(3)
print(f"****{name}走出房间")
se.release() # 该方法执行依次,信号量计数减一
if __name__ == '__main__':
se = Semaphore(5) # 创建一个信号量对象,限制只能最多5个线程能访问
for i in range(7):
t = Thread(target=home, args=(f"tom{i}", se)) # 创建7个进程并运行
t.start()
七,event事件对象
event事件用于唤醒被挂起的线程,当event对象的标志为True时,线程继续执行。实际上,创建一个Event类的实例,主要使用实例对象里的标志flag来决定是继续挂起还是继续运行。
# 小伙伴a,b,c围着吃火锅,当菜上齐了,请客的主人说:开吃!
# 于是小伙伴一起动筷子,这种场景如何实现
import threading
import time
def chihuoguo(name):
# 等待事件,进入等待阻塞状态
print(f'{name}已经启动')
print(f'小伙伴{name}已经进入就餐状态!')
time.sleep(1)
event.wait() # 一直等待,直到event对象的标志变为True
# 收到事件后进入运行状态
print(f'{name}收到通知了.')
print(f'小伙伴{name}开始吃咯!')
if __name__ == '__main__':
event = threading.Event()
# 创建新线程
thread1 = threading.Thread(target=chihuoguo, args=("tom",))
thread2 = threading.Thread(target=chihuoguo, args=("cherry",))
# 开启线程
thread1.start()
thread2.start()
time.sleep(10) # 请客的10s才到
# 发送事件通知
print('---->>>主线程通知小伙伴开吃咯!')
event.set() # 将event对象的标志位设为True,因为一开始默认为False
八,生产者与消费者模式
在多线程环境中,产生数据的线程叫做生产者;处理这些数据的线程叫做消费者。两者之间的核心问题就是:产生的数据放在哪里及如何管理,才能实现生产者和消费者的协调工作。解决方案是:把产生的数据放在缓冲区,然后消费者直接去拿即可。缓冲区实际上就是一块内存空间,采用特殊的储存结构,可以用标准库queue中的Queue类来实现。
from queue import Queue
from threading import Thread
from time import sleep
def producer(): # 生产者至多产生5个馒头数据放在缓冲区
num = 1
while True:
if queue.qsize()<5:
print(f"生产{num}号,大馒头")
queue.put(f"大馒头:{num}号")
num +=1
else:
print("馒头框满了,等待来人消费啊!")
sleep(1)
def consumer(): # 消费者隔一秒吃掉一个馒头数据
while True:
print(f"获取馒头:{queue.get()}")
sleep(1)
if __name__ == '__main__':
queue = Queue() #创建缓冲区
t1 = Thread(target=producer)
t2 = Thread(target=consumer)
t1.start()
t2.start()