线程
一:线程理论
一台计算机相当于一个工厂,工厂里有很多车间(进程),车间里面有很多工人(线程)。
真正干活儿的是工人(线程)。
1.什么是线程
进程是资源分配的最小单位,而线程是进程中的一部分,每个进程中至少有一个线程,是CPU调度的最小单位。
进程是资源分配的最小单位,线程是CPU调度的最小单位
线程开销更小,更轻量级。
2.线程的数据是共享的
在操作系统中,每一个进程的内存空间是独立的,数据不共享。
但是同一个进程中的各个线程,数据是共享的。
二:开启线程的2种方式
1.第一种 - 普通方式
from threading import Thread
import time
def task(t_name, s_time):
print(f'子线程[{t_name}]开始')
time.sleep(s_time)
print(f'子线程[{t_name}]结束')
if __name__ == '__main__':
t1 = Thread(target=task, args=('t1', 1)) # 实例化得到一个线程对象
t1.start() # 对象.start()启动线程
print('主线程')
# 子线程[t1]开始
# 主线程
# 子线程[t1]结束
2.第二种 - 继承类的方式
from threading import Thread
import time
class MyThread(Thread):
def run(self):
print('子线程开始')
time.sleep(1)
print('子线程结束')
if __name__ == '__main__':
t = MyThread()
t.start()
print('主线程')
# 子线程开始
# 主线程
# 子线程结束
三:TCP服务端实现并发效果
To be continue...
四:线程对象join方法
from threading import Thread
import time
def task(t_name, s_time):
print(f'{t_name} 开始')
time.sleep(s_time)
print(f'{t_name} 结束')
if __name__ == '__main__':
t1 = Thread(target=task, args=('t1', 1))
t2 = Thread(target=task, args=('t2', 2))
t1.start()
t2.start()
t1.join() # 等待子线程执行结束
t2.join()
print('主线程')
# t1 开始
# t2 开始
# t1 结束
# t2 结束
# 主线程
五:同一个进程下的多个线程数据共享
from threading import Thread
import time
money = 99
def task(t_name, n):
global money
print(f'{t_name} 开始')
money = n
print(f'{t_name} 结束')
if __name__ == '__main__':
t1 = Thread(target=task, args=('t1', 10))
t2 = Thread(target=task, args=('t2', 100))
t1.start()
t2.start()
t1.join()
t2.join()
print(money)
print('主线程')
# t1 开始
# t1 结束
# t2 开始
# t2 结束
# 100
# 主线程
六:线程对象 及 其他方法
from threading import Thread, current_thread, active_count
import time
def task():
print('子线程开始')
print(current_thread().name) # 打印出当前线程名字
time.sleep(1)
print('子线程结束')
if __name__ == '__main__':
t1 = Thread(target=task, name='I\'m Thread1')
t2 = Thread(target=task, name='I\'m Thread2')
t1.start()
t2.start()
print(f'存活线程数:{active_count()}') # 打印出当前有多少存活的线程
# 子线程开始
# I'm Thread1
# 子线程开始
# I'm Thread2
# 存活线程数:3
# 子线程结束
# 子线程结束
from threading import Thread, current_thread, active_count
import time
import os
def task(t_name, n):
print(f'{t_name}开始')
print(f'当前线程:{current_thread().name}') # 线程名字
print(f'当前线程所在的进程ID:{os.getpid()}')
time.sleep(n)
print(f'{t_name}结束')
if __name__ == '__main__':
t1 = Thread(target=task, name='Thread1', args=('t1', 2))
t2 = Thread(target=task, name='Thread2', args=('t2', 5))
t1.start()
t2.start()
t1.join()
print(f'线程t1是否存活:{t1.is_alive()}')
print(f'线程t2是否存活:{t2.is_alive()}')
# 当作线程id号
print(f'线程t1的ID:{t1.ident}')
print(f'线程t2的ID:{t2.ident}')
print(f'当前线程所在的进程ID:{os.getpid()}')
print(f'存活线程数:{active_count()}') # 打印出2 ,存活的是t2和主线程
# t1开始
# 当前线程:Thread1
# 当前线程所在的进程ID:37420
# t2开始
# 当前线程:Thread2
# 当前线程所在的进程ID:37420
# t1结束
# 线程t1是否存活:False
# 线程t2是否存活:True
# 线程t1的ID:45360
# 线程t2的ID:73396
# 当前线程所在的进程ID:37420
# 存活线程数:2
# t2结束
知识点:
1.获取线程名:t.name
、t.getName()
2.查看当前进程下有几个线程存活:active_count()
3.查看当前线程是否存活:t1.is_alive()
4.查看线程ID:t1.ident
5.查看当前线程所在的进程ID:os.getpid()
七:守护线程
from threading import Thread, current_thread, active_count
import time
def task(t_name, n):
print(f'{t_name}开始')
time.sleep(n)
print(f'{t_name}结束')
if __name__ == '__main__':
t1 = Thread(target=task, name='Thread1', args=('Thread1', 10))
t2 = Thread(target=task, name='Thread2', args=('Thread2', 4))
t1.setDaemon(True)
t1.start()
t2.start()
print('主线程')
# Thread1开始
# Thread2开始
# 主线程
# Thread2结束
八:线程互斥锁
from threading import Thread, Lock
import time
money = 99
def task(n, mutex):
global money
mutex.acquire() # 在修改数据的时候,加锁
temp = money
# time.sleep(random.randint(1, 2))
time.sleep(0.1)
money = temp - 1
mutex.release() # 修改完之后,释放锁,其他线程就可以抢到锁
if __name__ == '__main__':
ll = []
mutex = Lock()
for i in range(10):
t = Thread(target=task, args=(i, mutex))
t.start()
# t.join() # 不能在这里join,会变成串行
ll.append(t)
for i in ll:
i.join()
print(money)
# 89
九:GIL 全局解释器锁
1.什么是GIL?
官网解释:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
在CPython中,全局解释器锁(GIL)是一个互斥体,它阻止多个本机线程同时执行Python字节码。cpyth的线程管理不是必需的。(然而,由于GIL的存在,其他特性也越来越依赖于它所执行的保证。)
GIL(Global Interpreter Lock)全局解释器锁
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。
同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,然而因为CPython是大部分环境下默认的Python执行环境。
所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷,所有GIL并不是python的特性,仅仅是因为历史原因在Cpython解释器中难以移除。
2.GIL存在的原因
①CPython在执行多线程的时候并不是线程安全的,所以为了程序的稳定性,加一把全局解释锁,能够确保任何时候都只有一个Python线程执行。
②因为垃圾回收机制的存在,如果一个定义了一个x=2
,如果在一种较为极端的情况下,x
已经生成,但是2
还没赋值给x
,此时的x
就会被垃圾回收机制当成垃圾 被回收,为了避免这种情况出现,加了一把全局解释器锁。
3.线程释放GIL锁的2种情况
①.遇到I/O操作
例如:发出了HTTP请求,需要等待响应、读写文件...
如果是
②Time Tick
到期
Time Tick规定了线程的最长执行时间,超过时间后自动释放GIL锁
3.单核与多核情况下的多线程任务调度
单核:
由此可以发现:由于GIL机制的存在,单核CPU在同一时刻,只有一个线程在运行。
当线程遇到了
I/O操作
或者Time Tick
到期的时候,就会释放GIL锁release GIL
。此时,其他2个线程去抢这把锁,抢到之后,线程才能运行。
虽然出现I/O操作
和Time Tick
到期都会释放GIL锁,但是这2种情况是不一样的:
出现I/O:
如果Thread1出现了IO操作,释放了GIL锁,Thread1会主动放弃抢锁,不再参与这场竞争。
Time Tick到期:
如果Thread1出现了TimeTick到期,释放了GIL锁,Thread1会和其他线程竞争,同时抢这把GIL锁
- 所以,在单核的情况下,CPU的利用率是很高的。
多核:
由此可以发现:Thread1在CPU1上运行,Thread2在CPU2上运行。
GIL是全局的,CPU2上的Thread2需要等待CPU1上的Thread1让出GIL锁,才有可能执行。
如果在多次竞争中,Thread2都胜出,Thread1没有得到GIL锁,意味着CPU1一直是闲置的,无法发挥多核的优势。
所以,有了GIL的存在,CPython的的多线程 其实就是 单线程。
4.针对与cpython解释器,有哪些注意点?
①针对IO密集型:开多线程
②针对计算密集型:开多进程
5.既然GIL有缺陷,为什么还有这么多人用呢?
①.python的库多,库都是基于cpython写起来的,其他解释器没有那么多的第三方库(海纳百川,有容乃大
)。
②.虽然有这个历史诟病,但是丝毫不影响我们敲代码(金无足赤,人无完人
)。
③.代码是一成不变的,但是我们可以利用它的优势,优化它的劣势(取其精华,去其糟粕
)
④.Python牛逼!(人生苦短,我用Python
)
6.总结
cpython解释器中只有一个全局锁(GIL),线程必须获取到GIL才能执行,我们开的多线程,不管有几个CPU,同一时刻,只能有一个线程在执行(python的多线程,不能利用多核优势)