并发编程之线程
一、进程创建的机制
进程是计算机中最小的资源分配单位
进程对于操作系统来说还是有一定的负担
创建一个进程,操作系统要分配的资源大致:代码、数据、文件
存放代码到内存,存储数据到内存空间,文件,系统分配需要时间,占用的空间也比较大
二、线程
随着对并发的要求越来越高,无限开启进程是不现实的,解决高并发下进程的问题,使用线程。
线程是轻量级的概念,它没有属于自己的进程资源
一条线程只负责执行代码,没有自己独立的代码、变量、文件资源,线程都是共享资源
线程是计算机中被CPU调度的最小单位,计算机当中的CPU都是执行线程中的代码
三、进程和线程之间的关系
每一个进程中都有至少一条线程在工作,一个进程可以有多个线程;
线程是不能独立存在的
四、线程的特点
① 同一个进程中的所有线程的内存空间、资源都是共享的
② 轻量级,没有自己的资源
③ 线程之间的切换很近,也很快,进程之间切换比较慢,进程占用资源多
④ 轻型实体
⑤ 独立调度和分配的基本单位
⑥ 共享进程资源
⑦ 并发执行
五、线程的分类
用户级线程:操作线程,操作系统的内核只能通过进程去执行线程
内核级线程:操作系统能够直接调度线程
混合实现:用户能查看调度线程,内核也能调度线程
六、进程和线程之间的区别
① 占用的资源
② 调度的效率
③ 资源是否共享
七、python中的线程
一个线程中的多个线程不能并线,由于python是解释型语言,所有的解释型语言都不能并行多个进程。
GIL锁:全局解释器锁,为了线程的安全
由于CPython解释器的原因,内部有一个全局解释器锁,所以线程不能充分的利用多核,只能同一时刻同一个进程中的线程只有一个能被CPU执行
GIL锁目前是能够提高线程切换的效率,但也确实限制了程序的效率
锁是保证数据安全的,GIL保证只有一个线程能使用CPU,每个线程拥有GIL锁的时间是有限的,当一个线程已经到了GIL锁轮转临界点的时候,操作系统调度线程。
CPython控制GIL锁,只有一个线程能执行,其他线程处于阻塞状态,线程处于就绪状态由CPython控制,本质上出现数据不安全的问题,一个python代码对应了多条CPU指令
八、线程的创建
Thread模块比较老,Threading模块比较新,而且更高级,两个模块不能一起使用
import os
from threading import Thread
def func():
print('子线程',os.getpid())
print('主线程',os.getpid())
for i in range(2):
t = Thread(target=func) # 创建子线程
t.start()
'''
主线程 8076
子线程 8076
子线程 8076
'''
轻量级
线程比进程的运行效率高
import os,time
from multiprocessing import Process
from threading import Thread
def func():
print('进程号:',os.getpid())
if __name__ == '__main__':
# 创建线程
start = time.time()
t_lis = []
for i in range(100):
t = Thread(target=func)
t.start()
t_lis.append(t)
for t in t_lis:t.join() # 保障线程全部执行完毕
tt = time.time() - start
# 创建进程
start = time.time()
p_lis = []
for i in range(100):
p = Process(target=func)
p.start()
p_lis.append(p)
for p in p_lis:p.join()
pt = time.time() - start
print(tt,pt) # 统计线程和进程所运行的时间
# 0.02799248695373535 2.422297239303589
数据共享
from threading import Thread
count = 100
def func():
global count
count -= 1
t_lis = []
for i in range(100):
t = Thread(target=func)
t.start()
t_lis.append(t)
for t in t_lis:t.join()
print(count) # 0
对象的方法:
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。
from threading import Thread
import time
def func():
for i in range(3):
time.sleep(0.5)
print('子线程')
t = Thread(target=func)
print(t.is_alive()) # 判断线程是否活动
print(t.getName()) # 查看线程名
t.setName('th') # 修改线程名
print(t.getName()) # 查看线程名
time.sleep(2)
print(t.is_alive()) # 判断线程是否活动
'''
False
Thread-1
th
False
'''
currentThread().ident # 查看线程号
from threading import currentThread
from threading import Thread
def func():
print('子线程',currentThread().ident) # 查看线程号
print('主线程',currentThread().ident) # 查看线程号
t = Thread(target=func)
t.start()
enumerate() # 正在运行的线程列表
len(enumerate()) # 返回正在运行的线程数量
activeCount() # 返回正在运行的线程数量
from threading import enumerate
from threading import currentThread
from threading import Thread
from threading import activeCount
def func():
print('子线程:',currentThread().ident) # 查看线程号
print('父进程:',currentThread().ident) # 查看线程号
for i in range(3):
t = Thread(target=func)
t.start()
print(enumerate()) # 查看正在运行的线程列表
print(len(enumerate())) # 返回正在运行的线程数量
print(activeCount()) # 返回正在运行的线程数量
九、守护线程
守护进程是在主进程结束后,还等待所有子线程执行结束才结束
主线程结束意味着主进程结束
主线程等待所有的线程结束
主线程结束之后,守护线程随着主进程的结束而结束
from threading import Thread
import time
def func1():
while 1:
print(123)
time.sleep(1)
def func2():
print('func2 start')
time.sleep(3)
print('func3 end')
t1 = Thread(target=func1)
t2 = Thread(target=func2)
t1.setDaemon(True) # 设置为守护进程
t1.start()
t2.start()
print('主进程代码end')
无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁
需要强调的是:运行完毕并非终止运行
1.对主进程来说,运行完毕指的是主进程代码运行完毕
2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详细解释:
1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。