1、线程和进程
计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
一个车间里,可以有很多工人。他们协同完成一个任务。
线程就好比车间里的工人。一个进程可以包括多个线程。
车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征所有在同一个进程里的线程是共享同一块内存空间的(线程和进程的一个重要的区别)。
可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
什么是进程(process)? 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。 线程是操作系统最小的调度单位,是一串指令的集合。 进程要操作CPU,必须要先创建一个线程。
问题:线程快还是进程快?两者没有可比性,进程是资源的集合,线程是去执行任务。不过启动一个线程比启动一个进程快,当启动起来之后速度一样,因为进程也是通过线程来运行的。
线程与进程的区别:
- 线程共享内存空间,进程的内存是独立的
- 同一个进程的线程之间可以直接交流,两个进程想通信,必须通过一个中间代理来实现
- 创建新线程很简单,创建新进程需要对其父进程进行一次克隆
- 一个线程可以控制和操作同一进程里的其他线程,但是进程只能操作子进程
- 对主线程的更改(取消、优先级更改等)可能会影响进程的其他线程的行为;对父进程的更改不会影响子进程。
- 线程可以直接访问其进程的数据段;进程有自己的父进程数据段的副本。
- 一个程序至少有一个进程,一个进程至少有一个线程。
2、多线程与多进程
从上面关于线程和进程的的通俗解释来看,多线程和多进程的含义如下:
多进程:允许多个任务同时进行
多线程:允许单个任务分成不同的部分运行
3、Python多线程编程
3.1 单线程
在好些年前的MS-DOS时代,操作系统处理问题都是单任务的,我想做听音乐和看电影两件事儿,那么一定要先排一下顺序。
from time import ctime,sleep def music(): for i in range(2): print "I was listening to music. %s" %ctime() sleep(1) def move(): for i in range(2): print "I was at the movies! %s" %ctime() sleep(5) if __name__ == '__main__': music() move() print "all over %s" %ctime()
每一场电影需要5秒钟,因为太好看了,所以我也通过for循环看两遍。在整个休闲娱乐活动结束后,我通过
print "all over %s" %ctime()
看了一下当前时间,差不多该睡觉了。
运行结果:
I was listening to music. Tue Jul 10 12:51:39 2018 I was listening to music. Tue Jul 10 12:51:40 2018 I was at the movies! Tue Jul 10 12:51:41 2018 I was at the movies! Tue Jul 10 12:51:46 2018 all over Tue Jul 10 12:51:51 2018
其实,music()和move()更应该被看作是音乐和视频播放器,至于要播放什么歌曲和视频应该由我们使用时决定。所以,我们对上面代码做了改造:
# -*- coding:utf-8 -*- from time import ctime,sleep def music(func): for i in range(2): print ("I was listening to %s. %s" %(func,ctime())) sleep(1) def move(func): for i in range(2): print ("I was at the %s! %s" %(func,ctime())) sleep(5) if __name__ == '__main__': music(u'爱情买卖') move(u'阿凡达') print ("all over %s" %ctime())
运行结果:
C:Python27python.exe C:/Users/Administrator/Desktop/py_work/expreicise.py I was listening to 爱情买卖. Tue Jul 10 12:56:14 2018 I was listening to 爱情买卖. Tue Jul 10 12:56:15 2018 I was at the 阿凡达! Tue Jul 10 12:56:16 2018 I was at the 阿凡达! Tue Jul 10 12:56:21 2018 all over Tue Jul 10 12:56:26 2018
3.2 多线程
Python3 通过两个标准库 _thread (python2中是thread模块)和 threading 提供对线程的支持。
_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。
3.2.1使用_thread模块
调用_thread模块中的start_new_thread()函数来产生新线程。
先用一个实例感受一下:
import _thread import time # 为线程定义一个函数 def print_time(threadName, delay): count = 0 while count < 5: time.sleep(delay) count += 1 print("%s: %s" % (threadName, time.ctime(time.time()))) # 创建两个线程 try: _thread.start_new_thread(print_time, ("Thread-1", 2,)) _thread.start_new_thread(print_time, ("Thread-2", 4,)) except: print("Error: unable to start thread") while 1: pass print("Main Finished")
代码输出为:
D:Anaconda3python.exe C:/Users/Administrator/Desktop/py_work/expreicise.py Thread-1: Tue Jul 10 13:07:56 2018 Thread-1: Tue Jul 10 13:07:58 2018 Thread-2: Tue Jul 10 13:07:58 2018 Thread-1: Tue Jul 10 13:08:00 2018 Thread-2: Tue Jul 10 13:08:02 2018 Thread-1: Tue Jul 10 13:08:02 2018 Thread-1: Tue Jul 10 13:08:04 2018 Thread-2: Tue Jul 10 13:08:06 2018 Thread-2: Tue Jul 10 13:08:10 2018 Thread-2: Tue Jul 10 13:08:14 2018
注意到,在主线程写了:
while 1: pass
这是让主线程一直在等待.
如果去掉上面两行,那就直接输出并结束程序执行:
"Main Finished"
3.2.2使用threading模块
threading 模块除了包含 _thread 模块中的所有方法外,还提供的其他方法:
threading.currentThread(): 返回当前的线程变量。
threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
run(): 用以表示线程活动的方法。
start():启动线程活动。
join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
isAlive(): 返回线程是否活动的。
getName(): 返回线程名。
setName(): 设置线程名。
直接创建线程
#最简单的多线程 import threading import time def run(n): print('task',n) time.sleep(2) t1= threading.Thread(target=run,args=('t1',))#'t1'是一个参数,后边加逗号,不加逗号的话,直接将元组当参数了 t2= threading.Thread(target=run,args=('t2',)) print(time.ctime()) t1.start() t2.start() print(time.ctime())#体现出多线程的作用 #结果: Thu Jul 12 22:56:54 2018 task t1 task t2 Thu Jul 12 22:56:54 2018
另一种启动线程的方式:构造线程类
我们也可以通过直接从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动新线程,即它调用了线程的 run() 方法:
#最简单的多线程 import threading import time class MyThread(threading.Thread): def __init__(self,n): super(MyThread,self).__init__() self.n=n def run(self):#类里边用线程必须名字叫run print('running task',self.n) time.sleep(2) t1=MyThread('t1') t2=MyThread('t2') print(time.ctime()) t1.start() t2.start() print(time.ctime())#两个线程同时执行到本步,执行的时间特别特别快。但是到此步程序并没有执行完成,执行完本步,过会儿才执行完成。
结果:
Thu Jul 12 23:31:25 2018
running task t1
running task t2
Thu Jul 12 23:31:25 2018
接上面的听音乐和看电影的例子,我们可以直接使用threading.Thread 创建线程,并指定执行的方法以及传递的参数:
import threading from time import ctime,sleep def music(func): for i in range(2): print ("I was listening to %s. %s" %(func,ctime())) sleep(1) def move(func): for i in range(2): print ("I was at the %s! %s" %(func,ctime())) sleep(5) threads = [] t1 = threading.Thread(target=music,args=(u'爱情买卖',)) threads.append(t1) t2 = threading.Thread(target=move,args=(u'阿凡达',)) threads.append(t2) if __name__ == '__main__': for t in threads: t.start() print ("all over %s" %ctime())
结果输出为:
I was listening to 爱情买卖. Tue Jul 10 13:14:43 2018 I was at the 阿凡达! Tue Jul 10 13:14:43 2018 all over Tue Jul 10 13:14:43 2018 I was listening to 爱情买卖. Tue Jul 10 13:14:44 2018 I was at the 阿凡达! Tue Jul 10 13:14:48 2018
import threading import time def run(n): print('task',n) time.sleep(2) start_time=time.time() for i in range(50): t= threading.Thread(target=run,args=('t-%s'%i,)) t.start() #每执行t.start()一次,就会产生一个线程,每个线程独立运行,和其他线程没关系, # 不会等着一个线程执行完之后,再跟着执行另一个线程。 # 在主线程里想测试所有的子线程是测不了的。 #默认情况下,主线程不会等子线程执行完毕的,因为两者没关系,完全独立。
结果:
D:Anaconda3python.exe C:/Users/Administrator/Desktop/py_work/expreicise.py task t-0 task t-1 task t-2 task t-3 task t-4 task t-5 task t-6 task t-7 task t-8 task t-9 task t-10 task t-11 task t-12 task t-13 task t-14 task t-15 task t-16 task t-17 task t-18 task t-19 task t-20 task t-21 task t-22 task t-23 task t-24 task t-25 task t-26 task t-27 task t-28 task t-29 task t-30 task t-31 task t-32 task t-33 task t-34 task t-35 task t-36 task t-37 task t-38 task t-39 task t-40 task t-41 task t-42 task t-43 task t-44 task t-45 task t-46 task t-47 task t-48 task t-49 Process finished with exit code 0
#一次性执行50个线程,统计时间 #这个程序计算的不正确 import threading import time def run(n): print('task',n) time.sleep(2) print('task done',n) start_time=time.time() for i in range(50): t= threading.Thread(target=run,args=('t-%s'%i,)) t.start() print('所有线程结束了吗?') print('cost:',time.time()-start_time)#cost: 0.0070002079010009766
结果:
D:Anaconda3python.exe C:/Users/Administrator/Desktop/py_work/expreicise.py task t-0 task t-1 task t-2 task t-3 task t-4 task t-5 task t-6 task t-7 task t-8 task t-9 task t-10 task t-11 task t-12 task t-13 task t-14 task t-15 task t-16 task t-17 task t-18 task t-19 task t-20 task t-21 task t-22 task t-23 task t-24 task t-25 task t-26 task t-27 task t-28 task t-29 task t-30 task t-31 task t-32 task t-33 task t-34 task t-35 task t-36 task t-37 task t-38 task t-39 task t-40 task t-41 task t-42 task t-43 task t-44 task t-45 task t-46 task t-47 task t-48 task t-49 所有线程结束了吗? cost: 0.0070002079010009766 task done t-1 task done t-0 task done t-2 task done t-6 task done t-3 task done t-5 task done t-7 task done t-8 task done t-4 task done t-11 task done t-10 task done t-13 task done t-9 task done t-12 task done t-14 task done t-16 task done t-19 task done t-17 task done t-24 task done t-21 task done t-23 task done t-15 task done t-18 task done t-22 task done t-20 task done t-34 task done t-31 task done t-33 task done t-32 task done t-29 task done t-27 task done t-25 task done t-28 task done t-30 task done t-26 task done t-41 task done t-38 task done t-37 task done t-42 task done t-40 task done t-36 task done t-39 task done t-35 task done t-49 task done t-48 task done t-46 task done t-47 task done t-43 task done t-44 task done t-45 Process finished with exit code 0
从结果可以看到,为什么我们开启了其他线程之后,主线程立即退出了?因为我们没有使用join方法,对于主线程来说,thread1和thread2....是子线程,使用join方法,会让主线程等待子线程执行解说再继续执行。默认情况下,主线程不会等子线程执行完毕的,因为两者没关系,完全独立。
join()方法
我们修改一下代码:
import threading import time class MyThread(threading.Thread): def __init__(self,n): super(MyThread,self).__init__() self.n=n def run(self):#类里边用线程必须名字叫run print('running task',self.n) time.sleep(2) t1=MyThread('t1') t2=MyThread('t2') print(time.ctime()) t1.start() t1.join()#t1的结果没返回前,程序不往下走 t2.start() t2.join() print(time.ctime()) #结果: Thu Jul 12 23:34:44 2018 running task t1 running task t2 Thu Jul 12 23:34:48 2018
#要求:所有子线程依然多线程执行,执行结束后,主线程再往下走 import threading import time class MyThread(threading.Thread): def __init__(self,n,sleep_time): super(MyThread,self).__init__() self.n=n self.sleep_time=sleep_time def run(self): print('running task',self.n) time.sleep(self.sleep_time) print('task done',self.n) t1=MyThread('t1',2) t2=MyThread('t2',4)#两个线程执行时间不同 print(time.ctime()) t1.start() t2.start() t1.join() t2.join() print(time.ctime())#两个线程,执行完花了4秒,不是6秒
结果:
Sat Jul 21 18:11:04 2018
running task t1
running task t2
task done t1
task done t2
Sat Jul 21 18:11:08 2018
#一次性执行50个线程,统计时间 import threading import time def run(n): print('task',n) time.sleep(2) print('task done',n) start_time=time.time() t_objs = []#存线程实例 for i in range(50): t= threading.Thread(target=run,args=('t-%s'%i,)) t.start() t_objs.append(t)#为了不阻塞后面线程的启动,不在这里join,先放到一个列表里。 for t in t_objs:#循环线程实例列表,等待所有线程执行完毕。 t.join() print('所有线程结束') print('cost:',time.time()-start_time)#cost: 2.00811505317688
结果:
D:Anaconda3python.exe C:/Users/Administrator/Desktop/py_work/expreicise.py task t-0 task t-1 task t-2 task t-3 task t-4 task t-5 task t-6 task t-7 task t-8 task t-9 task t-10 task t-11 task t-12 task t-13 task t-14 task t-15 task t-16 task t-17 task t-18 task t-19 task t-20 task t-21 task t-22 task t-23 task t-24 task t-25 task t-26 task t-27 task t-28 task t-29 task t-30 task t-31 task t-32 task t-33 task t-34 task t-35 task t-36 task t-37 task t-38 task t-39 task t-40 task t-41 task t-42 task t-43 task t-44 task t-45 task t-46 task t-47 task t-48 task t-49 task done t-1 task done t-0 task done t-5 task done t-4 task done t-2 task done t-3 task done t-8 task done t-10 task done t-6 task done t-7 task done t-12 task done t-9 task done t-11 task done t-14 task done t-17 task done t-15 task done t-13 task done t-16 task done t-18 task done t-22 task done t-19 task done t-23 task done t-20 task done t-24 task done t-21 task done t-26 task done t-25 task done t-35 task done t-32 task done t-27 task done t-31 task done t-28 task done t-33 task done t-29 task done t-34 task done t-30 task done t-36 task done t-44 task done t-40 task done t-43 task done t-39 task done t-42 task done t-41 task done t-37 task done t-38 task done t-48 task done t-47 task done t-49 task done t-45 task done t-46 所有线程结束 cost: 2.00811505317688 Process finished with exit code 0
可以看到 退出主线程 在最后才被打印出来。
两个疑问
我们刚才介绍了两种使用多线程的方式,一种是直接调用threading.Thread 创建线程,另一种是从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动进程。学到这里,我就抛出了两个疑问,为什么第一种方法中我们可以为不同的线程指定运行的方法,而第二种我们都运行的是同一个方法,那么它内部的实现机制是什么呢?第二个疑问是,第二种方法中,我们没有实例化start()方法,那么run和start这两个方法的联系是什么呢?
首先,start方法和run方法的关系如下:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。
而run()方法的源码如下,可以看到,如果我们指定了target即线程执行的函数的话,run方法可以转而调用那个函数,如果没有的话,将不执行,而我们在自定义的Thread类里面重写了这个run 方法,所以程序会执行这一段。
def run(self): """Method representing the thread's activity. You may override this method in a subclass. The standard run() method invokes the callable object passed to the object's constructor as the target argument, if any, with sequential and keyword arguments taken from the args and kwargs arguments, respectively. """ try: if self._target: self._target(*self._args, **self._kwargs) finally: # Avoid a refcycle if the thread is running a function with # an argument that has a member that points to the thread. del self._target, self._args, self._kwargs
线程同步
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release 方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。如下:
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。
考虑这样一种情况:一个列表里所有元素都是0,线程"set"从后向前把所有元素改成1,而线程"print"负责从前往后读取列表并打印。
那么,可能线程"set"开始改的时候,线程"print"便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
实例:
import threading import time class myThread (threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print ("开启线程: " + self.name) # 获取锁,用于线程同步 threadLock.acquire() print_time(self.name, self.counter, 3) # 释放锁,开启下一个线程 threadLock.release() def print_time(threadName, delay, counter): while counter: time.sleep(delay) print ("%s: %s" % (threadName, time.ctime(time.time()))) counter -= 1 threadLock = threading.Lock() threads = [] # 创建新线程 thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # 开启新线程 thread1.start() thread2.start() # 添加线程到线程列表 threads.append(thread1) threads.append(thread2) # 等待所有线程完成 for t in threads: t.join() print ("退出主线程")
输出为:
1 D:Anaconda3python.exe C:/Users/Administrator/Desktop/py_work/expreicise.py 2 开启线程: Thread-1 3 开启线程: Thread-2 4 Thread-1: Tue Jul 10 13:26:23 2018 5 Thread-1: Tue Jul 10 13:26:24 2018 6 Thread-1: Tue Jul 10 13:26:25 2018 7 Thread-2: Tue Jul 10 13:26:27 2018 8 Thread-2: Tue Jul 10 13:26:29 2018 9 Thread-2: Tue Jul 10 13:26:31 2018 10 退出主线程
线程优先级队列( Queue)
Python 的 Queue 模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列 PriorityQueue。
这些队列都实现了锁原语,能够在多线程中直接使用,可以使用队列来实现线程间的同步。
Queue 模块中的常用方法:
Queue.qsize() 返回队列的大小
Queue.empty() 如果队列为空,返回True,反之False
Queue.full() 如果队列满了,返回True,反之False
Queue.full 与 maxsize 大小对应
Queue.get([block[, timeout]])获取队列,timeout等待时间
Queue.get_nowait() 相当Queue.get(False)
Queue.put(item) 写入队列,timeout等待时间
Queue.put_nowait(item) 相当Queue.put(item, False)
Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
Queue.join() 实际上意味着等到队列为空,再执行别的操作
import queue import threading import time exitFlag = 0 class myThread (threading.Thread): def __init__(self, threadID, name, q): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.q = q def run(self): print ("开启线程:" + self.name) process_data(self.name, self.q) print ("退出线程:" + self.name) def process_data(threadName, q): while not exitFlag: queueLock.acquire() if not workQueue.empty(): data = q.get() queueLock.release() print ("%s processing %s" % (threadName, data)) else: queueLock.release() time.sleep(1) threadList = ["Thread-1", "Thread-2", "Thread-3"] nameList = ["One", "Two", "Three", "Four", "Five"] queueLock = threading.Lock() workQueue = queue.Queue(10) threads = [] threadID = 1 # 创建新线程 for tName in threadList: thread = myThread(threadID, tName, workQueue) thread.start() threads.append(thread) threadID += 1 # 填充队列 queueLock.acquire() for word in nameList: workQueue.put(word) queueLock.release() # 等待队列清空 while not workQueue.empty(): pass # 通知线程是时候退出 exitFlag = 1 # 等待所有线程完成 for t in threads: t.join() print ("退出主线程")
上面的代码每次执行的结果是不一样的,取决于哪个进程先获得锁,一次运行的输出如下:
1 开启线程:Thread-1 2 开启线程:Thread-2 3 开启线程:Thread-3 4 Thread-2 processing One 5 Thread-3 processing Two 6 Thread-1 processing Three 7 Thread-3 processing Four 8 Thread-1 processing Five 9 退出线程:Thread-3 10 退出线程:Thread-2 11 退出线程:Thread-1 12 退出主线程
操作系统的设计,因此可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。