1.线程介绍
1.1什么是线程
这就要重新审视一下进程了 , 当我们每创建一个进程都会在内存中开辟一个内存空间
"""
进程 : 资源单位
线程 : 执行单位 (真正干活的)
将操作系统比喻成一个大的工厂
那么进程就相当于工厂里面的车间
而线程就是车间里面的流水线
每一个进程里面自带一个线程 , 当线程干活的时候所需要的变量等一些资源 , 就向所在的进程去要
再次总结 :
进程 : 资源单位(起一个进程仅仅只是在内存空间中开辟一块独立的空间)
线程 : 执行单位(真正被cpu执行的其实是进程里面的线程,线程指的就是代码的执行过
程,执行代码中所需要使用到的资源都找所在的进程索要
进程和线程都是虚拟单位,只是为了我们更加方便的描述问题
"""
1.2为什么要有线程
开进程
- 申请内存空间 耗资源
- 拷贝代码 耗资源
开线程
-
一个进程内可以开设多个线程,在用一个进程内开设多个线程无需再次申请内存空间及拷贝代码的操作
-
总结 :
开设线程的开销要远远的小于进程的开销
同一个进程下的多个线程之间数据是共享的
我们要开发一款文本编辑器
获取用户输入的功能
实时展示到屏幕的功能
自动保存到硬盘的功能
针对上面这三个功能,开设进程还是线程合适???
开三个线程处理上面的三个功能更加的合理
那是不是我们只要开线程就好了 , 不需要开进程了呢??? 会有额外的情况后面会介绍
1.3线程怎么用
实际上和开启进程的方式差不多 , 下面介绍
2.开启线程的两种方式
方式1
from threading import Thread
def task(name):
print(f"我的名字是 {name}")
if __name__ == '__main__':
t = Thread(target=task, args=('tom',))
t.start() # 创建线程的开销非常小几乎是代码一执行线程就已经创建了
# 开启线程不需要在main下面执行代码直接书写就可以
# 但是我们还是习惯性的将启动命令写在main下面
print('主')
主要方法
Thread实例对象的方法
# join(): 主线程等待子线程运行结束再执行
# name 返回当前线程的名字
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。
threading模块提供的一些方法:
# threading.current_thread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.active_count(): 返回正在活跃(运行)的线程数量,与len(threading.enumerate())有相同的结果。
方式2(类的继承式)
from threading import Thread
import time
class MyThead(Thread):
def __init__(self, name):
"""针对双下滑线开头双下滑线结尾(__init__)的方法 统一读作双下 xxx"""
# 重写了父类的方法 又不知道别人的方法里面有啥 你就对调用父类的方法
super().__init__()
self.name = name
def run(self):
print(f'{self.name} is running!!!')
time.sleep(1)
print(f'{self.name} is ending!!!')
if __name__ == '__main__':
t = MyThead('子线程')
t.run()
print('主')
3.多线程实现tcp服务端并发
学多进程和多线程有什么用呢? 是不是雇佣更多的人帮我们打工
, 干活
, 也就是实现并发的效果
# 服务端
import socket
from threading import Thread
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(3)
def task(conn):
while 1:
try:
msg = conn.recv(1024)
if not msg:break
print(msg.decode())
conn.send(msg.upper())
except Exception:
pass
if __name__ == '__main__':
while 1:
print('等待连接....')
conn, addr = server.accept()
t = Thread(target=task, args=(conn,))
t.start()
4.线程间数据共享
注意 : 是同一进程下的线程间数据是共享的
from threading import Thread
num = 100
def task():
global num
num = 10
if __name__ == '__main__':
t = Thread(target=task)
t.start()
t.join()
print(num)
5.守护线程
一个线程守护另一个线程 , 这个线程结束了 , 他再结束
默认情况下主线程运行束之后不会立刻结束会等待所有其他非守护线程结束才会结束 因为主线程的结束意味着所在的进程的结束
from threading import Thread
import time
def task():
print(1111)
time.sleep(2)
print(2222)
if __name__ == '__main__':
t = Thread(target=task)
t.daemon = True
t.start()
time.sleep(1)
print('主')
"""
当主线程sleep(1) , 结束后 , 即使守护线程还有代码没有执行也要结束 , 所以打印 111 主, 没有打印222
"""
6.线程互斥锁
需求 : 一个自然数100 , 100个线程 , 每个线程减1
没加锁
from threading import Threadimport timenum = 100def task(): global num tmp = num time.sleep(0.1) num = tmp - 1if __name__ == '__main__': t_lst = [] for i in range(100): t = Thread(target=task) t.start() t_lst.append(t) for obj in t_lst: obj.join() print(num) print('主')
加锁
from threading import Thread,Lockimport timenum = 100mutex = Lock()def task(): global num with mutex: tmp = num time.sleep(0.1) num = tmp - 1if __name__ == '__main__': t_lst = [] for i in range(100): t = Thread(target=task) t.start() t_lst.append(t) for obj in t_lst: obj.join() print(num) print('主')
将并发变成了串行 , 牺牲了效率保证了安全
7.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.)python解释器其实有多个版本 Cpython Jpython Pypypython但是普遍使用的都是 CPython解释器在 PYthon解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时执行 同一个进程下的多个线程无法利用多核优势 疑问; python的多线程是不是一点用都没有???无法利用多核优势因为 cpython中的内存管理不是线程安全的内存管理(垃圾回收机制) 1.应用计数 2.标记清楚 3.分代回收''''''重点: 1.GL不是 python的特点而是 CPython解释器的特点 2.G工L是保证解释器级别的数据的安全 3.GI会导致同一个进程下的多个线程的无法同时执行 4.针对不同的数据还是需要加不同的锁处理 5.解释型语言的通病: 同一个进程下多个线程无法利用多核优势'''结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
反推 : 如果同一时刻不同cpu的核心 , 运行了下面四个线程 , 是不是会有一种情况就是 1刚在内存中创建 , 还没有绑定的时候 , 垃圾回收线程就判断没有引用 , 就直回收了 , 这就是出现了数据不安全 , 所以我们在cpython解释器上加了一个锁 , 即GIL锁
8.GIL全局锁和LOCK我们加的锁的区别
from threading import Threadimport timenum = 100def task(): global num tmp = num num = tmp - 1if __name__ == '__main__': t_lst = [] for i in range(100): t = Thread(target=task) t.start() t_lst.append(t) for obj in t_lst: obj.join() print(num) print('主')# 为什么变成0了? 都去抢GIL锁 , 然后操作数据 , 操作完释放
加上sleep(0.1)
from threading import Threadimport timenum = 100def task(): global num tmp = num time.sleep(0.1) num = tmp - 1if __name__ == '__main__': t_lst = [] for i in range(100): t = Thread(target=task) t.start() t_lst.append(t) for obj in t_lst: obj.join() print(num) print('主') # 打印99 , 为什么是99了? 首先依然是抢GIL锁 , 由于遇到sleep(0.1) i/o了 , 就会释放锁 , 其他的线程也就抢进来了 , 所以是99
再加上自己的锁
from threading import Thread,Lockimport timenum = 100mutex = Lock()def task(): global num with mutex: tmp = num time.sleep(0.1) # 只要你进入IO , GIL锁会释放 num = tmp - 1if __name__ == '__main__': t_lst = [] for i in range(100): t = Thread(target=task) t.start() t_lst.append(t) for obj in t_lst: obj.join() print(num) print('主') """100个线程起起来之后 要先去抢GIL我进入io GIL自动释放 但是我手上还有一个自己的互斥锁其他线程虽然抢到了GIL但是抢不到互斥锁最终GIL还是回到你的手上 你去操作数据"""
9.多线程是不是没用了
"""多线程是否有用要看具体情况单核 : 四个任务(io密集型\计算密集型)多核 : 四个任务(io密集型\计算密集型"""# 计算密集型 就是cpu一直在运作,计算, 没有任何的io# 每个任务都需要10秒单核(不用考虑了) 多进程: 额外的消耗资源 多线程: 减小开销多核多进程 : 总耗时10+多线程 : 总耗时40+ # IO密集型多核 多进程 : 相对浪费资源 多线程 : 更加节省资源
代码验证 :
计算密集型
from threading import Threadfrom multiprocessing import Processimport os,timedef work(): res = 0 for i in range(100000000): res *= iif __name__ == '__main__': l = [] print(os.cpu_count()) # 获取计算机核心数 start = time.time() for i in range(4): p = Process(target=work) # 3.3908448219299316 # t = Thread(target=work) # 13.299258470535278 p.start() # t.start() l.append(p) # l.append(t) for obj in l: obj.join() print(time.time()-start)
io密集型
from threading import Threadfrom multiprocessing import Processimport os,timedef work(): # 模拟io time.sleep(2)if __name__ == '__main__': l = [] print(os.cpu_count()) # 获取计算机核心数 start = time.time() for i in range(40): p = Process(target=work) # 2.9211058616638184 p.start() l.append(p) # t = Thread(target=work) # 2.0087316036224365 # t.start() # l.append(t) for obj in l: obj.join() print(time.time()-start)
总结
"""多进程和多线程都有各自的优势并且我们后面在写项目的时候通常可以多进程下面再开设多线程这样的话既可以利用多核也可以介绍资源消耗我们开发的软件90%都是IO密集型的 , 一般会是多进程+多线程 , 你打开一个qq , 不动就是一个静态页面 , 就阻塞在那了 , 有了io"""
10.死锁和递归锁
进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额
所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁
from threading import Thread,Lockimport timemutexA=Lock()mutexB=Lock()class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('%s 拿到A锁' %self.name) mutexB.acquire() print('%s 拿到B锁' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('%s 拿到B锁' %self.name) time.sleep(2) mutexA.acquire() print('%s 拿到A锁' %self.name) mutexA.release() mutexB.release()if __name__ == '__main__': for i in range(10): t=MyThread() t.start()'''Thread-1 拿到A锁Thread-1 拿到B锁Thread-1 拿到B锁Thread-2 拿到A锁然后就卡住,死锁了'''
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
mutexA=mutexB=threading.RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
"""# 递归锁的特点# 可以被连续的 acquire和 release# 但是只能被第一个抢到这把锁执行上述操作# 它的内部有一个计数器每 acquire-次计数加一每rea1se-次计数减1"""
11.信号量Semaphore
信号量在不同的阶段可能对应不同的技术点
在并发编程中信号量指的是锁!!
同进程的一样
Semaphore管理一个内置的计数器, 每当调用acquire()时内置计数器-1; 调用release() 时内置计数器+1; 计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
from threading import Thread,Semaphoreimport threadingimport time# def func():# if sm.acquire():# print (threading.currentThread().getName() + ' get semaphore')# time.sleep(2)# sm.release()def func(): sm.acquire() print('%s get sm' %threading.current_thread().getName()) time.sleep(3) sm.release()if __name__ == '__main__': sm=Semaphore(5) # 括号内写数字写几就表示开设几个坑位 for i in range(23): t=Thread(target=func) t.start()
12.事件Event
些进程/线程需要等待另外一些进程/线程运行完毕之后才能运行,类似于发射信号一样
白话举个例子 : 我开了两个进程 1,2 我想要进程2等待进程1运行结束 , 我再运行 , 比如进程1 是生产原材料 , 进程2是对原材料加工的 , 那么进程2就要等待进程1
from threading import Thread, Eventimport timeevent = Event() # 造一个红绿灯def light(): print('红灯亮着的') time.sleep(3) print('绿灯亮了') event.set()def car(name): print(f"{name}车正在等红灯") event.wait() # 等待别人给你发信号 print(f'{name}车加油门跑了')if __name__ == '__main__': l = Thread(target=light) l.start() for i in range(10): c = Thread(target=car,args=(str(i),)) c.start() print('主')
13.线程q
进程下多个线程数据是共享的 , 为什么先同一个进程下还会去使用队列呢 ? 因为队列 管道+锁 用队列还是为了保证数据的安全
我们现在使思的队列都是只能在本地测诚使用 , 实际生产都是用别人封装好的 , redis , kfuka 等
先进先出队列
import queueq=queue.Queue()q.put('first')q.put('second')q.put('third')print(q.get())print(q.get())print(q.get())'''结果(先进先出):firstsecondthird'''
后进先出
import queueq=queue.LifoQueue()q.put('first')q.put('second')q.put('third')print(q.get())print(q.get())print(q.get())'''结果(后进先出):thirdsecondfirst'''
优先级队列
import queueq=queue.PriorityQueue()#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高q.put((20,'a'))q.put((10,'b'))q.put((30,'c'))print(q.get())print(q.get())print(q.get())'''结果(数字越小优先级越高,优先级高的优先出队):(10, 'b')(20, 'a')(30, 'c')'''