day 40 线程
1 线程概念的引入背景
1.1进程
之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。
程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
1.2 有了进程为什么要有线程
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
- 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
- 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
如果这两个缺点理解比较困难的话,举个现实的例子也许你就清楚了:如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;如果老师在黑板上写演算过程,我们开始记笔记,而老师突然有一步推不下去了,阻塞住了,他在那边思考着,而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。
现在你应该明白了进程的缺陷了,而解决的办法很简单,我们完全可以让听、写、思三个独立的过程,并行起来,这样很明显可以提高听课的效率。而实际的操作系统中,也同样引入了这种类似的机制——线程。
1.3 线程的出现
60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
因此在80年代,出现了能独立运行的基本单位——线程(Threads)。
注意:
进程是资源分配的最小单位,
线程是CPU调度的最小单位.
每一个进程中至少有一个线程。
1.4 进程和线程的关系

线程与进程的区别可以归纳为以下4点:
1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程操作系统中,进程不是一个可执行的实体。
2 开启线程的两种方式
方式一
from multiprocessing import Process
from threading import Thread
import time
def task(name):
print('%s is running'%name)
time.sleep(1)
print('%s is over'%name)
# 开启线程不需要在main下面执行代码,直接书写就可以,但是我们还是习惯性的将启动命令写在main下面
t = Thread(target=task,args=('egon',))
p = Process(target=task,args=('jason',))
p.start()
t.start() # 创建线程的开销非常小 几乎是代码一执行线程就已经创建了
print('主')
方式二
from threading import Thread
import time
class MyThead(Thread):
def __init__(self, name):
"""针对刷个下划线开头双下滑线结尾(__init__)的方法,统一读成:双下init"""
# 重写了别人的方法 又不知道别人的方法里有啥 你就调用父类的方法
super().__init__()
self.name = name
def run(self):
print('%s is running'%self.name)
time.sleep(1)
print('egon DSB')
if __name__ == '__main__':
t = MyThead('egon')
t.start()
print('主')
3 TCP服务端实现并发的效果
服务端
服务端
1 要有固定的IP和PORT
2 24小时不间断提供服务
3 能够支持并发
import socket
from threading import Thread
from multiprocessing import Process
server =socket.socket() # 括号内不加参数默认就是TCP协议
server.bind(('127.0.0.1',8080))
server.listen(5)
# 将服务的代码单独封装成一个函数
def talk(conn):
# 通信循环
while True:
try:
data = conn.recv(1024)
# 针对mac linux 客户端断开链接后
if len(data) == 0: break
print(data.decode('utf-8'))
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
# 链接循环
while True:
conn, addr = server.accept() # 接客
# 叫其他人来服务客户
# t = Thread(target=talk,args=(conn,))
t = Process(target=talk,args=(conn,))
t.start()
客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
client.send(b'hello world')
data = client.recv(1024)
print(data.decode('utf-8'))
4 线程对象的join方法
from threading import Thread
import time
def task(name):
print('%s is running'%name)
time.sleep(3)
print('%s is over'%name)
if __name__ == '__main__':
t = Thread(target=task,args=('egon',))
t.start()
t.join() # 主线程等待子线程运行结束再执行
print('主')
5 同一个进程下的多个线程数据是共享的
from threading import Thread
import time
money = 100
def task():
global money
money = 666
print(money)
if __name__ == '__main__':
t = Thread(target=task)
t.start()
t.join()
print(money)
6 线程对象属性及其他方法
from threading import Thread, active_count, current_thread
import os,time
def task(n):
# print('hello world',os.getpid())
print('hello world',current_thread().name)
time.sleep(n)
if __name__ == '__main__':
t = Thread(target=task,args=(1,))
t1 = Thread(target=task,args=(2,))
t.start()
t1.start()
t.join()
print('主',active_count()) # 统计当前正在活跃的线程数
print('主',os.getpid())
print('主',current_thread().name) # 获取线程名字
7 守护线程
#1.对主进程来说,运行完毕指的是主进程代码运行完毕
#2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
#1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
#2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
点击查看代码
from threading import Thread
import time
def task(name):
print('%s is running' %name)
time.sleep(1)
print('%s is over' %name)
if __name__ == '__main__':
t = Thread(target=task,args=('egon',))
t.daemon = True
t.start()
print('主')
"""
主线程运行结束之后不会立刻结束,会等待所有其他非守护线程结束才会结束
因为主线程的结束意味着所在的进程的结束
"""
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# 稍微有一点迷惑性的例子
from threading import Thread
import time
def foo():
print(123)
time.sleep(1)
print('end123')
def func():
print(456)
time.sleep(3)
print('end456')
if __name__ == '__main__':
t1 = Thread(target=foo)
t2 = Thread(target=func)
t1.daemon = True
t1.start()
t2.start()
print('主.......')
8 锁
8.1 线程互斥锁
点击查看代码
from threading import Thread,Lock
import time
money = 100
mutex = Lock()
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1)
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)
8.2 GIL全局解释器锁
Ps:博客园密码:xiaoyuanqujing@666
"""
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.)
"""
"""
python解释器其实有多个版本
Cpython
Jpython
Pypypython
但是普遍使用的都是CPython解释器
在CPython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时执行
同一个进程下的多个线程无法利用多核优势!!!
疑问:python的多线程是不是一点用都没有???无法利用多核优势
为什么用GIL,因为cpython中的内存管理不是线程安全的
"""
"""
重点:
1.GIL不是python的特点而是CPython解释器的特点
2.GIL是保证解释器级别的数据的安全
3.GIL会导致同一个进程下的多个线程的无法同时执行即无法利用多核优势(******)
4.针对不同的数据还是需要加不同的锁处理
5.解释型语言的通病:同一个进程下多个线程无法利用多核优势
"""
8.3 GIL与普通互斥锁的区别
点击查看代码
from threading import Thread,Lock
import time
mutex = Lock()
money = 100
def task():
global money
mutex.acquire()
tmp = money
time.sleep(0.1) # 只要你进入IO了,GIL会自动释放,但是这时候线程互斥锁并没有释放
money = tmp - 1
mutex.release()
# with mutex: 另一种抢锁写法
# tmp = money
# time.sleep(0.1)
# 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)
"""
100个线程起起来之后,大家都要先去抢GIL,我抢到了GIL,开始运行代码,
我进入io时,GIL自动释放,但是我手上还有一个自己的线程互斥锁,
其他线程虽然抢到了GIL但是抢不到线程互斥锁,
等我的IO操作结束后GIL还是回到你的手上,然后你继续去操作数据
"""
1.GIL是CPython解释器的特点,不是python的特点!!!
2.GIL本质也是一把互斥锁,但是它是解释器级别的锁
3.它的存在是因为CPython解释器内存管理不是线程安全的
垃圾回收机制
引用计数
标记清除
分代回收
4.也就以为着GIL的存在导致了用一个进程下的多个线程无法利用多核优势(不能同时运行)
5.针对不同的数据应该加不同的锁来保证安全
9 多线程
9.1 同一个进程下的多线程无法利用多核优势,是不是就没有用了
"""
多线程是否有用要看具体情况
单核:四个任务(IO密集型\计算密集型)
多核:四个任务(IO密集型\计算密集型)
"""
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# 计算密集型 每个任务都需要10s
单核(不用考虑了)
多进程:额外的消耗资源
多线程:介绍开销
多核
多进程:总耗时 10+
多线程:总耗时 40+
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
# IO密集型
多核
多进程:相对浪费资源
多线程:更加节省资源
9.2 代码验证
计算密集型
点击查看代码
from multiprocessing import Process
from threading import Thread
import os,time
def work():
res = 0
for i in range(10000000):
res *= i
if __name__ == '__main__':
l = []
print(os.cpu_count()) # 获取当前计算机CPU个数
start_time = time.time()
for i in range(12):
p = Process(target=work) # 1.4679949283599854
p.start()
l.append(t)
for p in l:
p.join()
print(time.time()-start_time)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
for i in range(12):
t = Thread(target=work) # 5.698534250259399
t.start()
l.append(t)
for t in l:
t.join()
print(time.time()-start_time)
IO密集型
点击查看代码
from multiprocessing import Process
from threading import Thread
import os,time
def work():
time.sleep(2)
if __name__ == '__main__':
l = []
print(os.cpu_count()) # 获取当前计算机CPU个数
start_time = time.time()
for i in range(4000):
p = Process(target=work) # 21.149890184402466
p.start()
l.append(p)
for p in l:
p.join()
print(time.time()-start_time)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
for i in range(4000):
t = Thread(target=work) # 3.007986068725586
t.start()
l.append(t)
for t in l:
t.join()
print(time.time()-start_time)
9.3 总结
"""
多进程和多线程都有各自的优势
并且我们后面在写项目的时候通常可以在 多进程下面再开设多线程
这样的话既可以利用多核也可以介绍资源消耗
"""
应该结合任务的具体类型再做判断
应该对任务分两种情况讨论
IO密集型
多线程更加节省资源
计算密集型
多进程更加合理
多进程多线程都是有用的,并且后面的操作都是多进程加上多线程从而达到效率的最大化

浙公网安备 33010602011771号