Day 02
为什么要有线程
进程的缺点
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
生产者消费者模型
生产者消费者模式是通过·一个容器来解决生产者和消费者的强耦合问题
生产者和消费者彼此之间不能直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取
阻塞队列就是一个缓冲区,平衡了生产者和消费者的处理能力。
线程理论
线程理论
- 计算机相当于大工厂,工厂里有一个个车间(进程),有很多人(线程)干不同的事
- 真正干活的是线程----------线程是CPU调度的最小单位
- 进程是资源分配的最小单位,线程是CPU调度的最小单位。每一个进程至少有一个线程
- 线程开销更小,更轻量级
开启线程的两种方式
创建线程的两种方式和进程是一样的
函数方法
from threading import Thread
from time import sleep
from random import randint
# 函数
def task(n):
print(f"我是线程{n}---------启动!")
sleep(randint(1, 4))
print(f"我是线程{n}---------结束!")
if __name__ == '__main__':
for i in range(1, 4):
t1 = Thread(target=task, args=(i, ))
t1.start()
print("主线程结束!")
类方法
class MyTask(Thread):
def __init__(self, n):
super().__init__()
self.n = n
def run(self) -> None:
print(f"我是线程{self.n}---------启动!")
sleep(randint(1, 4))
print(f"我是线程{self.n}---------结束!")
if __name__ == '__main__':
for i in range(1, 4):
t1 = MyTask(i)
t1.start()
print("主线程结束!")
TCP服务端实现并发效果
线程对象join方法
该函数为阻塞函数,会阻塞知道等待队列中所有的数据被处理完毕
if __name__ == '__main__':
for i in range(1, 4):
t1 = MyTask(i)
t1.start()
t1.join()
print("主线程结束!")
----------------------------------
我是线程1---------启动!
我是线程2---------启动!
我是线程3---------启动!
我是线程3---------结束!
主线程结束! # 线程3结束开始执行主进行 但是不影响其他线程的正常运行
我是线程1---------结束!
我是线程2---------结束!
同一个线程下的多个线程数据共享
一个线程修改后另外的数据只能在修改后的基础上修改
class MyTask(Thread):
def __init__(self, n):
super().__init__()
self.n = n
def run(self) -> None:
global number
print(f"我是线程{self.n}---------启动!")
sleep(1)
number -= 1
print(f"我是线程{self.n}---------{number}!")
线程对象及其方法
active_count():返回活跃的线程数量,(主+子)
current_thread():
返回当前的Thread对象,该对象对应于调用者的控制线程。如果未通过线程模块创建调用者的控制线程,则将返回功能受限的虚拟线程对象。可以通过 .属性
或方法获取 线程的属性方法
Thread实例对象的方法
- is_alive():返回线程对象是否存活True or False
- getName():返回线程名字
- getName():设置线程名字
- isDaemon():返回是不是守护线程 True or False
守护线程
无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行。
- 对主进程来说,运行完毕指的是主进程代码运行完毕
- 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
if __name__ == '__main__':
t1 = MyTask(1, 2)
t2 = MyTask(2, 3)
# t1.daemon = True # t1只睡了两秒,主线程还要等到睡了三秒的t2完成,所以t1会继续运行
t2.daemon = True # 由于t1睡的时间短,所以t1结束,主线程就只等待t2结束,而t2是守护线程,所以t1结束t2也就结束了
t1.start()
t2.start()
print("主线程结束!")
-----------------------------------
>>> 我是线程1---------启动!
>>> 我是线程2---------启动!
>>> 主线程结束!
>>> 我是线程1---------结束!
线程互斥锁
线程Lock和进程Lock是两个完全不同的对象
Lock互斥锁
原语:即原子语句,这个过程不能被中断。
同上数据共享栗子
不加锁 用文件读取的模式更加明显
from threading import Thread, Lock
from time import sleep
from random import randint
number = 1000000
class MyTask(Thread):
def __init__(self, n):
super().__init__()
self.n = n
def run(self) -> None:
global number
sleep(1)
# mutex.acquire()
for i in range(1, 100001): # 每个数据减去 10w
number -= 1
# mutex.release()
if __name__ == '__main__':
mutex = Lock()
for i in range(1, 3): # 开启两个线程
t = MyTask(i)
t.start()
sleep(10)
print(number)
----------------------------------
>>> 819193
所以为了防止这样的数据错乱我们需要给他加一把锁
def run(self) -> None:
global number
sleep(1)
mutex.acquire()
for i in range(1, 100001):
number -= 1
mutex.release()
if __name__ == '__main__':
mutex = Lock()
for i in range(1, 3):
t = MyTask(i)
t.start()
sleep(20) # 防止线程还没结束 就直接打印结果不准确
print(number)
------------------------------
>>> 800000
GIL全局解释所理论
#1 python的解释器有很多,cpython,jpython,pypy(python写的解释器)
#2 python的库多,库都是基于cpython写起来的,其他解释器没有那么多的库
#3 cpython中有一个全局大锁,每条线程要执行,必须获取到这个锁
#4 为什么会有这个锁呢?python的垃圾回收机制
#5 python的多线程其实就是单线程
#6 某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行
# 7 总结:cpython解释器中有一个全局锁(GIL),线程必须获取到GIL才能执行,我们开的多线程,不管有几个cpu,同一时刻,只有一个线程在执行(python的多线程,不能利用多核优势)
# 8 如果是io密集型操作:开多线程
# 9如果是计算密集型:开多进程
以上两句话,只针对与cpython解释器