多线程编程
多线程编程对于具有如下特点的编程任务而言是非常理想的:本质上是异步;需要多个并发活动;每个活动处理顺序可能是不确定的,或者说是随机的、不可预测的。这种编程任务可以被组织或划分成多个执行流,其中每个执行流都有一个指定要完成的任务。根据应用的不同,这些子任务可能需要计算出中间结果,然后合并为最终的输出结果。
本节目录
1、线程与进程
2、线程与Python
3、thread模块
4、单线程与多线程执行对比
5、多线程实践
6、生产者消费者模型
7、线程的替代方案
1、线程与进程
程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
进程:计算机程序只是存储在磁盘上的可执行二进制(或其他类型)文件。只有把它加载到内存中并被操作系统调用,才有其生命周期。进程(有时称为重量级进程)则是一个执行中的程序。
为什么有了进程还要线程
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:
-
进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
-
进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
线程是什么?
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
简单来说,线程是操作系统最小的调度单位,是一串指令的集合
一个进程中的各个线程与主线程共享一片数据空间,因此相比于独立的进程而言,线程间的信息共享和通信更加容易,线程一般以并发的方式执行的,正是由于这种并行和数据共享机制,使得多任务间的协作称为可能。当然,在单核的CPU系统中,因为真正的并发是不可能的,所以线程的执行是这样规划的:每个线程执行一小会,然后让步给其他线程(再次排队等待更多的CPU时间)。在整个进程的执行过程中,每个线程执行它自己特定的任务,在必要时和其他线程进行结果通信。
当然,这种共享并不是没有风险的,如果两个或多个线程访问同一片数据,由于1数据访问顺序不同,可能导致结果不一致,这种情况通常称为静态条件(race condition)。幸运的是大多数线程库都有一些同步原语,以允许线程管理器控制执行和访问。
进程与线程的区别:
-
进程要操作CPU必须先创建一个线程,进程本身并不可以执行,进程操作CPU是通过线程来实现的
-
线程共享内存,进程的内存是相互独立的。同一个进程之间的线程之间可以直接交流,两个进程想要通信必须通过一个中间代理来实现
- 同一个进程之间的线程可以直接交流,两个进程想要通信必须通过一个中间代理来实现。
- 创建一个新线程很简单,创建一个新进程需要对其父进程进行一次克隆。
- 一个线程可以控制与操作同一进程里的其他线程,但是进程只能操作子进程。
- 对于主线程的修改有可能会影响其他线程的运行,但是对于父进程的修改不会影响子进程。
2.线程与Python
线程的使用场景
#Python的多线程实际上是假多线程,因为Python的GIL(全局解释器锁)的存在,Python是不能过实现真正的多线程并发的,我们所看到的多线程并行的效果只是因为CPU在不断的进行上下切换,实际上还是串行,并不会同一时间多个线程分布在多个CPU上运行 #IO操作不占用CPU #计算占用CPU #故Python多线程不适合CPU密集型的任务,适合IO密集型的任务
全局解释器锁
Python代码的执行是由Python虚拟机(又名解释器主循环)进行控制的。Python在设计时是这样考虑的,在主循环中同时只能有一个控制线程在执行,就像单核CPU系统中的多进程一样。内存中可以有许多程序,但是在任意给定时刻只能有一个程序在运行。同理,尽管Python解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行
对于Python虚拟机的访问是由全局解释器锁(GIL)控制。这个锁就是用来保证同时只能有一个线程运行的。在多线程环境中,Python虚拟机将按下面所述的方式执行。
(1)设置GIL.
(2)切换进一个线程去运行。
(3)执行下面操作之一
a.指定数量的字节码指令
b.线程主动让出控制权(可以调用time.sleep(0)来完成)。
(4)把线程设置回睡眠状态(切换出线程)。
(5)解锁GIL
(6)重复上述步骤
线程的两种调用方式:
#直接调用 import time import threading def run(n): #定义每个线程要运行的函数 print('task',n) time.sleep(1) # r1=run('t1') # r2=run('t2') t1=threading.Thread(target=run,args=('t1',))#生成一个线程实例 t2=threading.Thread(target=run,args=('t2',)) t1.start() #启动线程 t2.start() #继承调用 class MyThread(threading.Thread): def __init__(self,name): super(MyThread,self).__init__() self.name=name def run(self): #定义每个线程要运行的函数 print("Hello %s"%self.name) time.sleep(1) h1=MyThread('h1') h2=MyThread('h2') h1.start() h2.start()
import time import threading def run(n): print('task',n) time.sleep(1) #主线程和它启动的子线程是并行的 start_time=time.time() for i in range(50): t=threading.Thread(target=run,args=('t%s'%i,)) #子线程 而整个程序下来本身是一个主线程 t.start() print('cost:',time.time()-start_time)# 无法测出整个程序运行的时间 因为主线程和它启动的子线程是并行的 主线程启动后不会等它的子线程执行完毕再往下走 #想要测出整个程序的运行时间 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) for i in t_objs: #循环线程实例列表,等待所有线程执行完毕 i.join()#t1等待t1线程执行完后才往下走 #t1.join(5)#等待子线程执行5s后再执行主线程,主线程结束后继续执行子线程 print('all the threads has finish!',threading.current_thread()) print('cost:',time.time()-start_time)
join与Daemon(守护线程)
- join: 等待该线程终止 。当在主线程当中执行到t1.join()方法时,就认为主线程应该把执行权让给t1,直到t1执行结束,主线程才能在执行。
2.Daemon: threading模块支持守护线程,其工作方式是:守护线程一般是一个等待客户端请求服务的服务器。如果没有客户端请求,守护线程就是空的。如果把一个线程设置为守护线程,就表示这个线程不重要,进程退出时不需要等待这个线程之执行完成。
如果主线程准备退出时,不需要等待某些子线程完成,就可以为这些子线程设置为守护线程标记。该标记为真时,表示该线程不重要,或者说该线程只是用来等待客户端请求而不做任何其他事器情。
import threading def run(n): print('task',n) time.sleep(1) t1=threading.Thread(target=run,args=('t1',)) t2=threading.Thread(target=run,args=('t2',)) t1.start() #t1.join() #t1等待t1线程执行完后才往下走 #t1.join(5)#等待子线程执行5s后再执行主线程,主线程结束后继续执行子线程 t2.start() print('done') join
import time import threading def run(n): print('task',n) time.sleep(1) #主线程和它启动的子线程是并行的 start_time=time.time() for i in range(50): t=threading.Thread(target=run,args=('t%s'%i,)) #子线程 而整个程序下来本身是一个主线程 t.setDaemon(True)#将当前线程设置为Daemon线程 在线程启动前设置 t.start() print('all the threads has finish!',threading.current_thread())
event事件
# python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法wait、clear、set #使用threading.Event可以使一个线程等待其他线程的通知,我们把这个Event传递到线程对象中,Event默认内置了一个标志,初始值为False。一旦该线程通过wait()方法进入等待状态,直到另一个线程调用该Event的set()方法将内置标志设置为True时,该Event会通知所有等待状态的线程恢复运行。 #event.set() 设置标志 #event.clear() 清除标志 #event.wait() 如果标志被设置, wait方法不做任何事情。如果标志被清除,等待将阻塞,直到它再次被设置。任何数量的线程都可以等待相同的事件 #event.isset()判断标志位是否被设定
import threading import time event=threading.Event()#创建一个事件 def lighter(): count=0 event.set()#设置标志位 while True: if count>=5 and count<10:#改成红灯 event.clear()#把标志位清空 print("