一、线程
1、概念:一条流水线的工作过程
2、和进程的区别和关系
(1)关系
》进程是资源单位,线程是执行单位,cpu真正执行的是线程
》一个进程至少有一个线程
》多线程针对的是一个进程的概念
》从执行的角度说:执行一个进程就相当于开启一个控制(主)线程
》从资源的角度说:开启一个进程就是开辟一个内存空间
(2)区别
》核心1:创建线程开销很小,不用申请空间(比进程快10-100倍);而进程开销大需要申请空间
》核心2:同一进程的多个线程共享该进程的资源和地址空间;但是不同进程的地址空间是隔离的
》线程可以直接访问进程的数据;而子进程是会拷贝父进程的所有数据
》同一进程的多个线程之间可以直接通信;而不同的进程通信则要依靠IPC机制
》线程可以控制同一进程的其它线程;进程只能控制它的子进程
》改变主线程可能贵影响其它线程的行为;改变父进程不会影响子进程
3、和进程的对比
》线程没有“父子”概念
》同一进程的不同线程的进程ID都是一样的
》由于线程共享进程的资源,所以当线程对数据修改时修改的就是该进程内存空间中的数据,但要注意线程和控制线程(该进程)对数据处理的时间先后顺序
二、创建线程
1、代码实现:Thread类(threading模块)
t = Thread(target=函数名,args=(参数,)/kwargs={字典}) 实例化
t.start() 发送创建线程请求(由于创建线程开销很小,所以发送请求的同时基本就开启了线程)
t .join() 主线程等待线程结束
from threading import Threadfrom multiprocessing import Processdef work(n): print('%s is running' %n)if __name__ == '__main__': t=Thread(target=work,args=(1,)) t.start() print('主线程')
2、其它方法:
(1)对象方法
》isAlive() 线程是否存活
》getName() 获取当前线程的名字,默认Thread-1,Thread-2,...这样
》setName() 设置当前线程的名字
(2)模块提供的方法
》currentThread() 获取当前线程对象
》enumerate() 显示当前活跃线程,输出列表
》activeCount() 当前活跃线程的数目
三、守护线程
1、代码实现:
t.daemon=True 必须在start()之前设置
2、概念
》当控制线程(从执行角度上看是该进程)结束守护线程也随之结束
》控制线程只有当所有非守护线程都结束时才算结束
from threading import Threadimport timedef foo(): print(123) time.sleep(5) print("end123")def bar(): print(456) time.sleep(3) print("end456")if __name__ == '__main__': t1=Thread(target=foo) t2=Thread(target=bar) t1.daemon=True t1.start() t2.start() print("main-------")
四、GIL:全局解释器互斥锁(不是python的特性)
1、产生背景:在Cpython解释器中特有的,因为python解释器带有数据回收机制,这样就会带来回收和利用的冲突性,所以就要求一个进程的多个线程同一时间只能有一个被执行
2、本质:就是一把互斥锁,实现将并发变为串行,保证数据安全
》》》#1 所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
》》》例如:test.py定义一个函数work,在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。》》》#2 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。
在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问
3、实现方式:
with lock:
python解释器的代码()
这个lock就是GIL
4、GIL和lock:
》要根据不同的数据来加不同的锁,因为GIL保证不了用户数据的安全,GIL只能保护python解释器级别的数据
》线程要想被执行就要去抢GIL锁(也就是争执行权限)
5、GIL和多线程
》对于多个计算密集型的程序
(1)单核情况下:因为始终只有一个cpu去执行计算,所以就要求开销要小,故选择多线程
(2)多核情况下:因为有多个cpu去执行计算,能够实现真正的并行,效率会很高,而线程每次只有一个在执行没有利用多核的优势,故选择多进程
》对于多个IO密集型程序
(1)单核情况下:因为只有一个cpu执行,需要不断的切换来实现并发,所以要求开销尽量要小,故选择多线程
(2)多核情况下:多个cpu执行的话,遇到IO仍然要阻塞,若是多进程的话就会常常是阻塞状态,运行时间就会是时间最长的哪个程序,而如果是多线程,首先开销很小,其次每次执行一个线程遇到IO阻塞就会立即去处理别的线程,这样效率会提高,故选择多线程
6、为什么要有GIL和代码运行的整个过程
》首先,python解释器带有的数据回收机制,本身是有计数器的,通过计数来判断是否为垃圾数据,而这个计数就是python解释器中的代码提供的功能(其实也是一段程序来实现)也就是python解释器级别的数据,而肯定有可能会出现当python解释器要回收某一个数据而某一个程序正好要使用这个数据的情况,这就带来了冲突,为了解决这种冲突保护数据安全就必须保证同一时间内只能有一个程序来使用python解释器的代码,这就变成了串行,所以就引入了GIL锁的应用
》我们写一段python代码,首先肯定要打开python解释器,这就相当于开启了一个代码解释器的进程,我们在解释器中写代码其实就是用了解释器的功能(也就是通过一段程序实现的功能),而代码本身是不会自己运行的,运行我们所写的代码其实就是开启了另一个进程(即是python解释器进程的子进程)也就是将我们所写的代码作为参数传给python解释器来执行(这其实就是相当于子进程创建时会把父进程(python解释器的代码)的所有数据都拷贝,这样子进程(我们所写的代码)才能运行),而由于GIL锁的存在,每个程序要想运行就要去抢这把锁也就是争取运行权限,争到了就执行若遇到阻塞操作系统就会强制释放掉该程序的运行权限(也就是释放掉GIL锁),这样别的程序再去抢然后运行,等之前的程序处于就绪态时会再去抢GIL锁接着之前的运行状态继续执行,这就是一段代码要想运行起来必须经历的过程
补充一下:
1、开启python解释器操作系统其实是将解释器的代码从硬盘中取出放到内存中去运行,我们所写的代码也是这样的
2、其实进程和线程的创建和开启(个数快慢等,当然线程的开销肯定要比进程小很多),包括程序运行时对GIL锁的争抢和释放以及运行状态的保留等,这些都是由操作系统及电脑性能来控制和决定的,无法由程序本身来操控
》上面说到每个程序运行前要抢GIL锁(运行权限),运行起来时就是按照自己的代码来运行,会产生自己的数据,而这个数据GIL是保护不了的,因为GIL只是锁住了解释器而没有锁住每个进程对同一块数据的处理功能,所以这就是为什么不同的数据要加不同的锁来保护,GIL只是保护python解释器代码级别的数据而已