什么是线程
在传统操作系统中,每个进程有一个地址空间,而且每个进程默认有一个控制线程
线程相当于一条流水线工作的过程,一条流水线一定属于一个车间,一个车间的工作过程就相当于一个进程
车间负责把资源整合到一起,是一个资源单位,而车间内至少有一个流水线,流水线的工作需要电源,CPU就相当于电源
进程只是把资源集中到一起,(进程只是一个资源单位,或者说资源集合),而线程才是CPU上的执行单位
多线程
多线程的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,共用一个车间的资源
线程依赖于进程,一个进程可以包含多个线程,但一定有一个主进程,线程才是CPU执行的最小单元
例如:开启QQ
开启一个进程,在内存中开辟空间,加载数据,启动一个线程执行代码
线程和进程的区别
进程
划分空间,加载资源(静态)
开启进程开销大
开启多进程速度慢
进程之间数据不能直接共享(可以通过队列)
开启多个进程,每个进程都有不同的pid
线程
开启线程速度小
开启多线程速度快
同一进程下的线程数据可以共享
在主进程下开启的多个线程,每个线程的pid都和主进程一样
多线程的应用场景
并发:一个CPU来回切换(线程之间的切换)多进程并发 多线程并发
多进程并发:开启多个进程,每个进程里面的主线程执行任务
多线程并发:开启一个进程,此进程里面的多个线程执行任务
开启线程的两种方式


from threading import Thread def task(name): print(f'{name} is running') if __name__ == '__main__': t = Thread(target=task,args=('Leeson',)) t.start() print('主线程')


rom threading import Thread class MyThread(Thread): def run(self): print(f'{self.name} is running') if __name__ == '__main__': t = MyThread() t.start() print('主线程')
线程的一些方法


from threading import Thread import threading import time def task(name): time.sleep(1) print(f'{name} is running') print(threading.current_thread().name) if __name__ == '__main__': for i in range(5): t = Thread(target=task,args=('mcsaoQ',)) t.start() # 线程对象的方法: # time.sleep(1) # print(t.is_alive()) # 判断子线程是否存活 *** # print(t.getName()) # 获取线程名 # t.setName('线程111') # print(t.setName('alex')) # 获取线程名 # threading模块的方法: # print(threading.current_thread().name) # MainThread # print(threading.enumerate()) # 返回一个列表 放置的是所有的线程对象 print(threading.active_count()) # 获取活跃的线程的数量(包括主线程) print('主线程')
守护线程
无论是进程还是线程,都遵循守护XXX会等待主XX运行完毕后被销毁
强调:运行完毕并非终止运行
对于主进程来说
运行完毕指的是主进程代码运行完毕
主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束
对于主线程来说
运行完毕是指主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束
from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.setDaemon(True) #必须在t.start()之前设置 t.start() print('主线程') print(t.is_alive())


from threading import Thread import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") t1=Thread(target=foo) t2=Thread(target=bar) t1.daemon=True t1.start() t2.start() print("main-------")
互斥锁
多线程的同步锁与多进程的同步锁是一个道理,就是多个线程抢占同一个数据(资源)时,我们要保证数据的安全,顺序的合理
要利用串行,多个线程抢占一个资源,如果不串行,就会发生问题
from threading import Thread from threading import Lock import time x = 100 lock = Lock() def task(): global x lock.acquire() temp = x time.sleep(0.1) temp -= 1 x = temp lock.release() if __name__ == '__main__': t_l1 = [] for i in range(100): t = Thread(target=task) t_l1.append(t) t.start() for i in t_l1: i.join() print(f'主{x}')
死锁现象,递归锁
进程也有死锁与递归锁问题,与线程的死锁与递归锁同理
死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去


from threading import Thread from threading import Lock import time lock_A = Lock() lock_B = Lock() class MyThread(Thread): def run(self): self.f1() self.f2() def f1(self): lock_A.acquire() print(f'{self.name}拿到A锁') lock_B.acquire() print(f'{self.name}拿到B锁') lock_B.release() lock_A.release() def f2(self): lock_B.acquire() print(f'{self.name}拿到B锁') time.sleep(0.1) lock_A.acquire() print(f'{self.name}拿到A锁') lock_A.release() lock_B.release() if __name__ == '__main__': for i in range(3): t = MyThread() t.start() print('主....')
死锁现象的解决方法:递归锁。在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。
递归锁就是一把锁,锁上有记录,只要acquire一次,锁上计数器就加一次,release一次,锁上计数就减一次
只要递归锁计数不为0,其它线程就不能争抢


from threading import Thread from threading import RLock import time lock_A = lock_B = RLock() class MyThread(Thread): def run(self): self.f1() self.f2() def f1(self): lock_A.acquire() print(f'{self.name}拿到A锁') lock_B.acquire() print(f'{self.name}拿到B锁') lock_B.release() lock_A.release() def f2(self): lock_B.acquire() print(f'{self.name}拿到B锁') time.sleep(0.1) lock_A.acquire() print(f'{self.name}拿到A锁') lock_A.release() lock_B.release() if __name__ == '__main__': for i in range(10): t = MyThread() t.start() print('主....')
信号量
之前的锁都只允许一个线程或进程进入,信号量允许多个线程或进程同时进入
信号量管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):
from threading import Thread from threading import Semaphore from threading import current_thread import time import random sem = Semaphore(5) def go_public_wc(): sem.acquire() print(f'{current_thread().getName()} 上厕所ing') time.sleep(random.randint(1,3)) sem.release() if __name__ == '__main__': for i in range(20): t = Thread(target=go_public_wc) t.start()
GIL锁
全局解释器锁
本身也是一把互斥锁(将并发变成串行,同一时刻只能有一个线程使用共享资源,牺牲效率,保证数据安全)
GIL锁详解
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 虚拟机按以下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上所有步骤。
在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。
为什么有人说python处理并发不好
设置GIL锁保证了解释器里面的数据安全
在开发python语言时,计算机只有单核,没有考虑到多个CPU处理进程的问题
强行加GIL锁减轻了开发人员的负担,但也带来了几个问题:
单进程的多线程不能利用多核
多进程的多线程可以利用多核
多核前提下python处理任务的方式


开启四个进程,开启四个线程 from multiprocessing import Process from threading import Thread import time import os # print(os.cpu_count()) def task1(): res = 1 for i in range(1, 100000000): res += i def task2(): res = 1 for i in range(1, 100000000): res += i def task3(): res = 1 for i in range(1, 100000000): res += i def task4(): res = 1 for i in range(1, 100000000): res += i if __name__ == '__main__': # 四个进程 四个cpu 并行 效率 start_time = time.time() p1 = Process(target=task1) p2 = Process(target=task2) p3 = Process(target=task3) p4 = Process(target=task4) p1.start() p2.start() p3.start() p4.start() p1.join() p2.join() p3.join() p4.join() print(f'主: {time.time() - start_time}') # 7.53943133354187 # 一个进程 四个线程 1 cpu 并发 25.775474071502686 # start_time = time.time() # p1 = Thread(target=task1) # p2 = Thread(target=task2) # p3 = Thread(target=task3) # p4 = Thread(target=task4) # # p1.start() # p2.start() # p3.start() # p4.start() # # p1.join() # p2.join() # p3.join() # p4.join() # print(f'主: {time.time() - start_time}') # 25.775474071502686
多进程的并行比单进程多线程并发的效率高很多


讨论IO密集型: 通过大量的任务去验证. from multiprocessing import Process from threading import Thread import time import os # print(os.cpu_count()) def task1(): res = 1 time.sleep(3) if __name__ == '__main__': 开启150个进程(开销大,速度慢),执行IO任务, 耗时 9.293531656265259 start_time = time.time() l1 = [] for i in range(150): p = Process(target=task1) l1.append(p) p.start() for i in l1: i.join() print(f'主: {time.time() - start_time}') 开启150个线程(开销小,速度快),执行IO任务, 耗时 3.0261728763580322 start_time = time.time() l1 = [] for i in range(150): p = Thread(target=task1) l1.append(p) p.start() for i in l1: i.join() print(f'主: {time.time() - start_time}') # 3.0261728763580322
任务数量大,单进程下的多线程效率高
GIL锁与互斥锁的关系
GIL 自动上锁解锁,保护解释器的数据安全
互斥锁 手动上锁解锁,保护文件数据的安全


from threading import Thread from threading import Lock import time lock = Lock() x = 100 def task(): global x lock.acquire() temp = x # time.sleep(1) temp -= 1 x = temp lock.release() if __name__ == '__main__': t_l = [] for i in range(100): t = Thread(target=task) t_l.append(t) t.start() for i in t_l: i.join() print(f'主线程{x}')
线程全部是计算密集型:
当程序执行,开启多个线程,第一个线程要先拿到GIL锁,然后拿到LOCK锁,释放LOCK锁,最后释放GIL锁


from threading import Thread from threading import Lock import time lock = Lock() x = 100 def task(): global x lock.acquire() temp = x time.sleep(1) temp -= 1 x = temp lock.release() if __name__ == '__main__': t_l = [] for i in range(100): t = Thread(target=task) t_l.append(t) t.start() for i in t_l: i.join() print(f'主线程{x}')
线程全部是IO密集型:
当程序执行,开启多个线程,第一个线程要先拿到GIL锁,然后拿到LOCK锁,开始运行,遇到阻塞,CPU窃走,GIL释放,第一个线程挂起,第二个线程抢到GIL锁,接下来想抢LOCK锁,但LOCK锁还没被释放,阻塞,挂起,第三个线程进入......
进程池线程池
进程池:放置进程的一个容器
线程池:放置线程的一个容器
线程即使开销小,电脑也不可以无限的开进程,所以应该对线程(进程)做数量的限制,在计算机能满足的最大情况下,更多的创建线程(进程)


from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ThreadPoolExecutor import time import os import random def task(name): print(name) print(f'{os.getpid()} 准备接客') time.sleep(random.randint(1,3)) if __name__ == '__main__': p = ProcessPoolExecutor() # 设置进程数量默认为cpu个数 for i in range(23): p.submit(task,1) # 给进程池放任务,传参


from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ThreadPoolExecutor import time import os import random def task(name): print(name) print(f'{os.getpid()} 准备接客') time.sleep(random.randint(1,3)) if __name__ == '__main__': p = ThreadPoolExecutor() # ,默认cpu数量*5 for i in range(23): p.submit(task,1) # 给进程池放任务,传参