Python之路--并发编程之线程并发
一.线程介绍
1.什么是线程?
如果我们把进程比作一个车间的话,线程就是车间中的一条条流水线。流水线需要电源,电源就相当于CPU。
当我们创建一个进程的时候,进程中就会自动产生一个线程,就是主线程,也可以叫做控制线程
换句话说:进程相是一个资源整合单位,而线程才是CPU上的执行单位。
例如:北京地铁比作一个进程的话,地铁昌平线就是其中的一个进程。
2.线程的开销比进程要小
开启进程的时候,进程要向CPU申请内存空间,而开线程并不需要申请内存空间,只要在进程下直接开启就可以,所以进程的开销比线程大的多。
例如:开启进程就相当于重新开一个车间,这是需要申请空间,而开线程就像在原先的车间内在拉一条流水线就可以了。
3.进程之间是竞争关系,而线程之间是协作关系
不同的进程之间因为要申请内存空间,就会发生竞争关系,而线程之间共用同一个进程中所有的资源,是一种协作关系。
4.为什么要用多线程?
多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:
1. 多线程共享一个进程的地址空间
2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
二.threading模块
创建线程threading和进程的模块multiprocess使用时的接口和模式都是相同的
三.开启线程的两种方式
开启线程有两种方式:一种采用普通方式,一种是采用类的方式(采用类的方式是定义函数名只能是run函数)
'''采用普通方法开启线程''' from threading import Thread def work(name): print('%s eat 泔水'%name) if __name__=='__main__': t=Thread(target=work,args=('alex',)) t.start() print('主进程')
'''采用类的方法开启线程''' from threading import Thread class Mythread(Thread): def __init__(self,name): super().__init__() self.name=name def run(self): #只能命名为run函数 print('%s eat 泔水'%self.name) if __name__=='__main__': t=Mythread('Alex') t.start() print('主')
四.基于线程实现套接字通信
方式一:采用普通的方式
'''采用普通的方式''' from threading import Thread from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8086)) server.listen(5) def comm(conn): while True: try: data = conn.recv(1024) print(data.decode('utf-8')) if not data: break conn.send(data.upper()) except Exception: break if __name__ == '__main__': while True: conn, addr = server.accept() print('ip:%s port:%s'%(addr[0], addr[1])) t = Thread(target=comm, args=(conn,)) t.start()
from socket import * client=socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8086)) while True: msg=input('===>') if not msg:break client.send(msg.encode('utf-8')) data=client.recv(1024) print(data.decode('utf-8'))
方式二:采用类的方式
'''采用类的方式''' from threading import Thread from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1', 8086)) server.listen(5) class Mythread(Thread): def __init__(self,conn): super().__init__() self.conn=conn def run(self):#函数名只能定为run函数 while True: try: data = self.conn.recv(1024) print(data.decode('utf-8')) if not data: break self.conn.send(data.upper()) except Exception: break if __name__ == '__main__': while True: conn, addr = server.accept() print('ip:%s port:%s'%(addr[0], addr[1])) t=Mythread(conn) t.start()
from socket import * client=socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8086)) while True: msg=input('===>') if not msg:break client.send(msg.encode('utf-8')) data=client.recv(1024) print(data.decode('utf-8'))
五.线程中其他的方法
Thread实例对象的方法 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。 threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
方法示例:
from threading import Thread,current_thread,enumerate,active_count import os def work(name): print('%s is work'%current_thread().getName())#获取当前线程的名称 print('%s eat 泔水'%name) if __name__=='__main__': print('%s is work'%os.getpid())#获取当前进程的pid print(os.getppid())#获取父进程的pid t=Thread(target=work,args=('alex',)) t.start() print(enumerate())#查看线程的个数,以列表的形式显示出来 print(active_count())#查看活跃的进程的个数 print('主进程') '''打印结果如下''' '''10060 is work 8928 如果只有一个进程的话,那么父进程显示的就是pycharm的进程 Thread-1 is work [<_MainThread(MainThread, started 8496)>, <Thread(Thread-1, started 8532)>] alex eat 泔水 2 主进程'''
六.守护线程
1.守护线程和守护进程的概念是一样的,都是等主线程(主进程)结束后就会自动的销毁。
但是需要强调的是:运行完毕,不代表终止运行。
进程和线程的区别:
1.在进程中,主进程的代码运行完毕后,主进程就运行完毕了
2.在线程中,主线程代码运行结束后,还要等着其他非守护线程的代码运行完成后,才算运行完毕。
对上述问答题的详细解释
'''1.在进程中,主进程代码运行结束后,主机进程就运行完毕了(守护进程就在这个时候被回收了) 然后主进程会等着其他非守护进程的子进程运行结束后,把子进程进行回收(否则会产生僵尸进程),最后就结束了 ''' '''2.在线程中,主线程运行完毕时指得是进程内所有的非守护线程都结束了才算运行完毕(守护线程就在这个时候被回收), 这是因为在进程中,主线程运行完毕就相当于整个进程也云结束了,然后进程内的资源就会被回收, 如果在主线程代码结束就回收的话,会产生混乱,在进程中必须保证所有的非守护线程运行结束后资源才能被回收'''
示例:
from threading import Thread import time def say(): print('123') time.sleep(2) print('456') def say1(): print('789') time.sleep(3) print('666') if __name__=='__main__': t=Thread(target=say) t1=Thread(target=say1) t.daemon=True #设置为守护进程 t.start() t1.start() print('主') '''打印结果''' '''123 789 主 456 666''' '''这是因为t为守护进程,t1为线程,守护线程要等到所有其他非守护线程都结束后, 并且主线程也结束后才会结束'''
七.GIL锁
1.什么是GIL锁?
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL
那么CPython实现中的GIL又是什么呢?GIL全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:
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.)
好吧,是不是看上去很糟糕?一个防止多线程并发执行机器码的一个Mutex,乍一看就是个BUG般存在的全局锁嘛!别急,我们下面慢慢的分析。
2.为什么会有GIL锁:
由于物理上得限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。即使在CPU内部的Cache也不例外,为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思,也不可避免的带来了一定的性能损失。
Python当然也逃不开,为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。
慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,并且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?
所以简单的说GIL的存在更多的是历史原因。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前GIL这种方式会更优雅。
3.GIL锁的存在是保护python解释器级别的数据,让同一个时刻只有一个线程可以可以执行,这个线程可以对解释器级别的数据进行修改,这样的方法虽然极大的降低了执行效率,但是这样保证了数据的安全。
4.解决效率的问题:1.如果所需开的进程数教少,我们可以通过开进程的方式来代替开线程。
5.python的多线程在只有在io密集型的任务中才会有作用,在计算密集型的情况下,多进程产生的是负面的作用,这是可以考虑开通过开多进程的情况来实现。
6.总结:
Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。从本分的分析中,我们可以做以下一些简单的总结: - 因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能 - 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现 - GIL在较长一段时间内将会继续存在,但是会不断对其进行改进
八.同步锁
1.几个需要注意的问题
三个需要注意的点: #1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,
但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来 #2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,
join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高
2.GIL锁和同步锁的区别
(1).GIL锁是保护python解释器级别的数据,而同步锁lock是保护用户级别的数据
(2).两把锁本质上都是互斥锁,但是保护的数据级别是不一样的。

浙公网安备 33010602011771号