python使用多线程实例讲解
1 进程和线程
1.1 进程和线程的概念
进程(process)和线程(thread)是操作系统的基本概念。
进程是资源分配的最小单位,线程是CPU调度的最小单位。
线程是程序中一个单一的顺序控制流程,进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位。
一、什么是进程
计算机程序只不过是磁盘中可执行的二进制(或其他类型)的数据,它们只有在被读取到内存中,被操作系统调用时才开始它们的生命周期。
进程是程序的一次执行。每个进程都有自己的地址空间、内存、数据栈及其他记录其运行轨迹的辅助数据。操作系统管理在其上运行所有的进程,并为这些进程公平分配时间、进程也可以通过fork和spawn操作来完成其他的任务。
不过进程有自己的内存空间,数据栈等,所以只能使用进程间通讯(Inter Process communication, IPC),而不能直接共享信息。
二、什么是线程
线程跟进程有些相似,不同的是:所有的线程运行在同一个进程中,共享相同的运行环境。
线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被抢占(中断)或暂时的被挂起(睡眠),让其他线程运行,这叫做让步。
一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更方便地共享数据以及相互通讯。线程一般都是并发执行的,正是由于这种并发和数据共享的机制使得多个任务的合作变成可能。
实际上,在单CPU的系统中,真正的并行是不可能的,每个线程会被安排成每次只运行一小会,然后就把CPU让出来,让其他的线程去运行。在进程的整个运行过程中,每个线程都只做自己的事,在需要的时候跟其他的线程共享运行的结果。
当然,这样的共享并不是完全没有危险的。如果多个线程共同访问同一片数据,则由于数据访问的顺序不同,有可能导致数据结果的不一致的问题,即竞态条件(race condition)。同样,大多数线程库都带有一些列的同步原语,来控制线程的执行和数据的访问。
另一个需要注意的是由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情况下,这种“贪婪”的函数会让CPU的时间分配有所倾斜,导致各个线程分配到的运行时间可能不尽相同,不尽公平。
1.2 进程与线程的区别
(1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
(2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
(3)调度和切换:线程上下文切换比进程上下文切换要快得多。
(4)在多线程OS中,进程不是一个可执行的实体。
总结,进程和线程可以类比为火车和车厢。
(1)线程在进程下行进(单纯的车厢无法运行)。
(2)一个进程可以包含多个线程(一辆火车可以有多个车厢)。
(3)不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)。
(4)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)。
(5)进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)。
(6)进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到该趟火车的所有车厢)
(7)进程可以拓展到多机,进程最适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
(8)进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存(比如火车上的洗手间【互斥锁mutex】)。
(9)进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去【信号量semaphore】)。
1.3 多进程与多线程的概念与区别
(1)一个进程相当于一个要执行的程序,它会开启一个主线程,多线程会开启多个子线程;
(2)python设计之初没有多核CPU,所以它的多线程是一种并发操作(伪并行),它相当于把CPU的时间片分成一段一段很小的片段,然后分给各个线程交替进行,由于每个片段都很短,所以看上去像平行操作。
举个例子:现在有一个16核的CPU,一个要执行的数据读取任务A,我们将A分成多个进程并行操作,每个进程放到一个核上。但是如果将这个任务A用一个进程(开多个线程)完成的话,虽然一个核心同一时间处理一个线程,按理说16核可以同时处理16个线程(未考虑超线程技术),但由于python的缺陷,这里面的多线程依然是并发(伪并行)的,所以效率低。
1.4 Python的全局解释器锁GIL
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设置之初就考虑到要在解释器主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。
虽然Python解释器可以运行多个线程,但任意时刻,只有一个线程在解释器中运行。对Python虚拟机的访问由全局解释器锁(global interpreter lock,GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python虚拟机按以下方式执行:
(1) 设置GIL
(2) 切换到一个线程去运行
(3) 运行:
a. 指定数量的字节码的指令,或者
b. 线程主动让出控制(可以调用time.sleep(0))
(4) 把线程设置为睡眠状态
(5) 解锁GIL
(6) 再次重复以上所有步骤
在调用外部代码(如C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于这期间没有Python的字节码被运行,所以不会做线程切换)。编写扩展的程序员可以主动解锁GIL。不过Python开发人员则不用担心在这些情况下你的Python代码会被锁住。
2 python多线程实例
2.1 普通的单线程
# -*- coding: utf-8 -*-
from time import ctime,sleep
def music(name):
for i in range(2):
print("I was listening to music.----{}:{}".format(name, ctime()))
sleep(1)
def coding(code):
for i in range(2):
print("I was coding codes!----{}:{}".format(code, ctime()))
sleep(5)
if __name__ == '__main__':
music("my love music")
coding("python code")
print("all over.----{}".format(ctime()))
我们先听了一首音乐,通过for循环来控制音乐的播放了两次,每首音乐播放需要1秒钟,sleep()来控制音乐播放的时长。接着我又敲了会代码,,每段代码需要5秒钟,通过for循环敲了两遍。
2.2 多线程
python提供了两个模块来实现多线程thread 和threading ,thread有一些缺点,在threading得到了弥补,我们直接学习threading 就可以了。继续对上面的例子进行改造,引入threadring来同时播放音乐和写代码:
2.2.1 设置守护线程(不等待)
在Python中,守护线程是指在程序运行时在后台运行的线程,当主线程结束时,守护线程也会随之结束。守护线程通常用于执行一些不需要阻塞主线程或长时间运行的任务。
# -*- coding: utf-8 -*-
from time import ctime,sleep
import threading
def music(name):
for i in range(2):
print("I was listening to music.----{}:{}".format(name, ctime()))
sleep(1)
def coding(code):
for i in range(2):
print("I was coding codes!----{}:{}".format(code, ctime()))
sleep(5)
if __name__ == '__main__':
threads = []
t1 = threading.Thread(target=music, args=('my love music',))
threads.append(t1)
t2 = threading.Thread(target=coding, args=('python code',))
threads.append(t2)
for t in threads:
# setDaemon(True)将线程声明为守护线程,必须在start()方法调用之前设置
t.setDaemon(True)
t.start()
# 子线程启动后,主线程也继续执行下去
print("all over.----{}".format(ctime()))
因为是守护线程,当主线程执行完最后一条语句print后,没有等待子线程,直接就退出了,同时子线程也一同结束。
从执行结果来看,子线程(muisc 、coding)和主线程(print all over)都是同一时间启动,但由于主线程执行完结束,所以导致子线程也终止。
若让主线程多等待8秒,则会正常输出。
if __name__ == '__main__':
threads = []
t1 = threading.Thread(target=music, args=('my love music',))
threads.append(t1)
t2 = threading.Thread(target=coding, args=('python code',))
threads.append(t2)
for t in threads:
# setDaemon(True)将线程声明为守护线程,必须在start()方法调用之前设置
t.setDaemon(True)
t.start()
# 子线程启动后,主线程也继续执行下去
sleep(8)
print("all over.----{}".format(ctime()))
2.2.2 不设置守护线程(等待)
if __name__ == '__main__':
threads = []
t1 = threading.Thread(target=music, args=('my love music',))
threads.append(t1)
t2 = threading.Thread(target=coding, args=('python code',))
threads.append(t2)
for t in threads:
t.start()
# 子线程启动后,主线程也继续执行下去
print("all over.----{}".format(ctime()))
主线程执行结束以后,进程会等待子线程运行结束后,进程才会退出。
2.2.3 线程阻塞join方法
对上面的程序加了个join()方法,用于等待线程终止。join()的作用是,在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
if __name__ == '__main__':
threads = []
t1 = threading.Thread(target=music, args=('my love music',))
t2 = threading.Thread(target=coding, args=('python code',))
threads.append(t2)
threads.append(t1)
for t in threads:
t.setDaemon(True)
t.start()
t.join()
# 子线程t1运行结束后,继续执行主线程
print("all over.----{}".format(ctime()))
使用子线程t1阻塞,2秒后运行结束,线程t2还没运行结束。