线程与进程
操作系统原理相关的书,基本都会提到一句很经典的话: "进程是资源分配的最小单位,线程则是CPU调度的最小单位"。
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
进程和线程的区别主要有:
1、进程之间是相互独立的,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,但互不影响;而同一个进程的多个线程是内存共享的,所有变量都由所有线程共享。
2、由于进程间是独立的,因此一个进程的崩溃不会影响到其他进程;而线程是包含在进程之内的,线程的崩溃就会引发进程的崩溃,继而导致同一进程内的其他线程也奔溃。
线程的类型:
主线程:当一个程序启动时,就有一个进程被操作系统(OS)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(Main Thread)。每个进程至少都有一个主线程,主线程通常最后关闭。
子线程:在程序中创建的其他线程,相对于主线程来说就是这个主线程的子线程。
守护线程:daemon thread,对线程的一种标识。守护线程为其他线程提供服务,如JVM的垃圾回收线程。当剩下的全是守护线程时,进程退出。
前台线程:相对于守护线程的其他线程称为前台线程。
GIL(Global Interpreter Lock)全局解释器锁
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 虚拟机按以下方式执行:
1、设置GIL
2、切换到一个线程去运行
3、运行:
a. 指定数量的字节码指令,或者
b. 线程主动让出控制(可以调用time.sleep(0))
4、把线程设置为睡眠状态
5、解锁GIL
6、再次重复以上所有步骤
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。Python同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL
还有,就是在做I/O操作时,GIL总是会被释放。对所有面向I/O 的(会调用内建的操作系统C 代码的)程序来说,GIL 会在这个I/O 调用之前被释放,以允许其它的线程在这个线程等待I/O 的时候运行。如果是纯计算的程序,没有 I/O 操作,解释器会每隔 100 次操作就释放这把锁,让别的线程有机会执行(这个次数可以通过 sys.setcheckinterval 来调整)如果某线程并未使用很多I/O 操作,它会在自己的时间片内一直占用处理器(和GIL)。也就是说,I/O 密集型的Python 程序比计算密集型的程序更能充分利用多线程环境的好处。
单线程
下面我们以一个单线程的例子来正式进入线程的讲解,如果是单线程的,那么我们在听音乐和看电影就得分开做,代码如下所示:
from time import ctime,sleep
def music(func):
for i in range(2):
print("listening music {},{}".format(func,ctime()))
sleep(1)
def move(func):
for i in range(2):
print("watching move {},{}".format(func,ctime()))
sleep(5)
if __name__ == "__main__":
music("We Don't Talk Anymore")
move('DEADPOOL')
print("all over %s" %ctime())
#output
listening music We Don't Talk Anymore,Fri May 10 18:07:59 2019
listening music We Don't Talk Anymore,Fri May 10 18:08:00 2019
watching move DEADPOOL,Fri May 10 18:08:01 2019
watching move DEADPOOL,Fri May 10 18:08:06 2019
all over Fri May 10 18:08:11 2019
可以看到整个程序执行完,花的时间是12秒,也就是听音乐看电影各做两遍。
对于我们的CPU来说,特别是现在的都是多核的了,这样做实在是浪费CPU的性能,这个时候,我们来到了多线程。
多线程
在 Python 中,进行多线程编程的模块有两个:thread 和 threading。其中,thread 是低级模块,threading 是高级模块,对 thread 进行了封装,一般来说,我们只需使用 threading 这个模块。
我们利用多线程的方法实现同时听音乐和看电影!
from time import ctime,sleep
import threading
def music(func):
for i in range(2):
print("listening music {} .{}".format(func,ctime()))
sleep(1)
def move(func):
for i in range(2):
print("watching move {} .{}".format(func,ctime()))
sleep(5)
threads = []
t1 = threading.Thread(target=music,args=("We Don't Talk Anymore",))
threads.append(t1)
t2 = threading.Thread(target=move,args=('DEADPOOL',))
threads.append(t2)
if __name__ == "__main__":
for t in threads:
t.setDaemon(True)
t.start()
print("all over %s" %ctime())
#output
listening music We Don't Talk Anymore .Fri May 10 18:16:50 2019
watching move DEADPOOL .Fri May 10 18:16:50 2019
all over Fri May 10 18:16:50 2019
listening music We Don't Talk Anymore .Fri May 10 18:16:51 2019
watching move DEADPOOL .Fri May 10 18:16:55 2019
我们注意到前面3句是同时输出的,这也就是它们同时开始执行的,也就是实现了多线程并发的效果。
注意我们这句t.setDaemon(True),这是将线程声明为守护线程,那么设置该线程为守护线程,表示该线程是不重要的,进程退出时不需要等待这个线程执行完成。这样做的意义在于:避免子线程无限死循环,导致退不出程序,也就是避免楼上说的孤儿进程。下面贴一个比较好的回答!(如果是这样,那么后面两句话应该不会输出才对啊,我在pycharm上面运行,3.7版本是有输出的,这个地方我现在还比较混乱,后面弄懂了来更新博文。)
官方文档上面说‘A boolean value indicating whether this thread is a daemon thread (True) or not (False). This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread; the main thread is not a daemon thread and therefore all threads created in the main thread default to daemon = False. The entire Python program exits when no alive non-daemon threads are left.’ 意思是 所有的子线程是否是守护进程都继承自主线程,因为主线程默认是非守护进程,因此,所有的由该主线程创建的子线程都不是守护进程。 当所有的非守护进程结束的时候,python程序也就结束了 换句话说: 给子线程设置为守护进程的时候,说明该线程不重要,当主线程结束的时候,程序直接结束了。
如果将程序更改为下面这样,输出结果又有了变化!
if __name__ == "__main__":
for t in threads:
t.setDaemon(True)
t.start()
t.join()
print("all over %s" %ctime())
#output
listening music We Don't Talk Anymore .Fri May 10 18:36:43 2019
watching move DEADPOOL .Fri May 10 18:36:43 2019
listening music We Don't Talk Anymore .Fri May 10 18:36:44 2019
watching move DEADPOOL .Fri May 10 18:36:48 2019
all over Fri May 10 18:36:53 2019
我们加入的join()函数,这个函数用于线程同步,即主线程任务结束之后,进入阻塞状态,一直等待其他的子线程执行结束后,才会终止。
我们通过拓展,可以对代码进行重构,让它能够播放不同的歌曲,而且在播放时不用去更改太多的地方!
from time import ctime,sleep
import threading
def play(file,time):
for i in range(2):
print("start playing:{}!{}".format(file,ctime()))
sleep(time)
list = {"We Don't Talk Anymore.mp3":3,'DEADPOOL.mp4':4,'Try.mp3':5}
threads = []
files = range(len(list))
for file,time in list.items():
t = threading.Thread(target=play,args=(file,time))
threads.append(t)
if __name__ == "__main__":
for i in files:
threads[i].start()
for i in files:
threads[i].join()
print('end {}'.format(ctime()))
#output
start playing:We Don't Talk Anymore.mp3!Fri May 10 18:54:31 2019
start playing:DEADPOOL.mp4!Fri May 10 18:54:31 2019
start playing:Try.mp3!Fri May 10 18:54:31 2019
start playing:We Don't Talk Anymore.mp3!Fri May 10 18:54:34 2019
start playing:DEADPOOL.mp4!Fri May 10 18:54:35 2019
start playing:Try.mp3!Fri May 10 18:54:36 2019
end Fri May 10 18:54:41 2019
好了,多线程最基本的东西基本就说完了,留下的坑后面再慢慢补!
参考文章
https://segmentfault.com/a/1190000014306740
http://funhacks.net/explore-python/Process-Thread-Coroutine/thread.html