一、线程和进程
1.什么是进程?
一个程序的执行实例就是一个进程。An executing instance of a program is called a process.
一个进程都有一个虚拟内存地址空间,可执行代码,调用操作系统的接口,安全的上下文,唯一的进程标识符PID,环境变量,优先级类,最大最小的工作的内存空间,和最少一个线程。
每一个进程想要执行,至少需要有一个线程,经常叫做主线程;
进程中的线程可以创建额外的线程,主线程可以创建子线程,子线程又可以创建子线程。
线程之间相互独立,不相互依赖。a线程生成b线程,a线程消失,b线程仍然可以存在。
A thread is an execution context, which is all the information a CPU needs to execute a stream of instructions.
Suppose you're reading a book, and you want to take a break right now, but you want to be able to come back and resume reading from the exact point where you stopped.
One way to achieve that is by jotting down the page number, line number, and word number.
So your execution context for reading a book is these 3 numbers.
If you have a roommate, and she's using the same technique, she can take the book while you're not using it, and resume reading from where she stopped.
Then you can take it back, and resume it from where you were. Threads work in the same way.
A CPU is giving you the illusion that it's doing multiple computations at the same time.
It does that by spending a bit of time on each computation.
It can do that because it has an execution context for each computation.
Just like you can share a book with your friend, many tasks can share a CPU.
On a more technical level, an execution context (therefore a thread) consists of the values of the CPU's registers.
Last: threads are different from processes.
A thread is a context of execution, while a process is a bunch of resources associated with a computation.
A process can have one or many threads.
Clarification:
the resources associated with a process include memory pages (all the threads in a process have the same view of the memory), file descriptors (e.g., open sockets), and security credentials (e.g., the ID of the user who started the process).
比如:
想要执行QQ,那么QQ要以一个整体的形式暴露给操作系统管理,里面包含对各种资源的调用,内存的管理,网络接口的调用...
2.什么是线程?
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是一串指令的集合。
线程是一个执行的上下文,它包含了CPU执行一串指令所需需要的所有信息。
3.进程和线程的区别?
线程共享内存空间,进程间的内存是独立的。
线程可以直接访问自己隶属的进程的数据,线程间访问该进程的数据是访问的同一块数据;子进程从它们的父进程中复制出了数据,独立地占用在其进程中的数据。
同一个进程的线程之间可以直接交流;两个进程之间想要通信,必须通过一个中间代理来实现。
创建新线程很简单;创建新进程需要对其父进程进行一次克隆。
一个线程可以控制和操作同一进程里的其他线程,但是进程只能操作子进程。
主线程的改变可能会影响同一进程内的其他线程;父进程的改变不会影响其子进程。
Threads share the address space of the process that created it; processes have their own address space.
Threads have directly access to the data segment of its process; processes have their own copy of the data segment of the parent process.
Threads can directly communicate with other threads of its process; process must use interprocess communication to communicate with sibling processes.
New threads are easily created; new processes require duplication of the parent processes.
Threads can exercise considerable control over threads of the same process; process can only exercise control over child processes.
Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
注意:线程和进程没有可比性,无法比较哪个比较快。
进程是资源的集合,进程的执行必须要通过线程;线程是真正执行任务。两者无法进行比较速度。
但是,启动一个线程的速度快于启动一个进程的速度。
4.GIL(Global Interpreter Lock)
单核的CPU只能执行单个进程,但是实际使用中却看上去像是可以多任务并发的执行。这个是如何实现的呢? ————是通过线程执行时的在CPU上的上下文的切换。
4核的CPU代表同一时间最多可以启动4个进程,但是在Python中,无论是多少核的CPU,在同一时间都只能通过执行单个线程。多任务的并发执行是通过快速的线程的上下文切换实现的。
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once.
This lock is necessary mainly because CPython's memory management is not thread-safe.
Howerver, since the GIL exists, other features have grown to depend on the guarantees that it enforces.
总而言之,无论启动多少个线程,有多少个CPU,Python在执行的时候会淡定地在同一时刻只允许一个线程运行。
CPython是用C语言实现的解释器,调用的是操作系统(C语言写的)的原生线程,Python在调用C语言的原生线程时,必须把上下文发给C语言的原生线程,由于解释器无法控制C语言的原生线程。
所以CPython中引入了GIL,在线程的上下文都传递给CPU时,确保同一时间内只有一个线程运行,确保数据的安全,准确,不混乱。
首先需要明确的一点是GIL
并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。
就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。
有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。
像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。
所以在很多人的概念里CPython就是Python,也就想当然的把GIL
归结为Python语言的缺陷。
所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
比方说,在JPython解释器中,调用的线程是用Java重新写的,所以不存在GIL。在PyPy中,新增了JIT(即时编译),附带中解决了GIL问题,所以在PyPy中也没有GIL。
二、Python threading模块
1.调用方式
线程有两种调用方式,一种是直接调用,另一种是继承式调用。
第一种:直接调用,使用threading.Thread()
1 import threading,time 2 def sayhi(num): 3 print('running on number:%s'%num) 4 time.sleep(3) 5 if __name__=='__main__': 6 t1=threading.Thread(target=sayhi,args=(1,)) 7 t2=threading.Thread(target=sayhi,args=(2,)) 8 t1.start() 9 t2.start() 10 print(t1.getName()) 11 print(t2.getName())
返回:
上面的返回结果显示的是两个线程t1,t2的运行输出结果和主线程向屏幕打印两个子线程t1,t2的名称。
第二种:继承式调用
1 import threading 2 class MyThread(threading.Thread): 3 def __init__(self,n): 4 super(MyThread,self).__init__() 5 self.n=n 6 def run(self): #定义每个线程要运行的函数 7 print('running task',self.n) 8 if __name__=='__main__': 9 t1=MyThread('t1') 10 t2=MyThread('t2') 11 t1.start() 12 t2.start() 13 print(t1.getName()) 14 print(t2.getName())
返回:
2.调用启动多个线程
上面的两个实例,都是通过创建一个新的线程来创建线程的,想要一次性创建多个线程,可以用循环的方式实现:
1 import threading,time 2 def run(n): 3 print('Task',n) 4 time.sleep(2) 5 print('Task done') 6 7 start_time=time.time() 8 t_obj=[] #初始化一个列表,用来存放线程t 9 for i in range(5): 10 t=threading.Thread(target=run,args=('thread-%s'%i,)) 11 t.start() 12 t_obj.append(t) 13 print('cost:',time.time()-start_time) #检测每个子线程的执行时间 14 15 print('threading type',threading.current_thread()) 16 print('main threading cost:',time.time()-start_time)
返回:
从上面的脚本的运行结果来看,主线程创建了子线程后,是并行的运行的,子线程不会等主线程完成以后再执行,而是独立地运行的。
3.help(threading.Thread)
class Thread(builtins.object)
A class that represents a thread of control. 表示控制线程的类Thread。
This class can be safely subclassed in a limited fashion. Thread可以用有限的方式安全地生成子类。
There are two ways to specify the activity: by passing a callable object to the constructor, or by overriding the run() method in a subclass. 有两种方式可以指定活动:通过将调用调用传入构造函数直接调用;或者通过重写子类的run()方法继承调用。
Methods defined here:
构造函数:
__init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None) #构造函数总是用关键字参数调用。 '''参数: group默认为None,是为了ThreadGroup类扩展时保留的参数。 target默认为None,表示没有调用任何方法。target指定的方法是Thread类中run方法的调用对象 name是线程名称。默认情况下,每个线程的唯一的名称是按照‘Thread-N’的格式构造的,N是一个小的十进制数字。 args是传给target指定方法中的参数容器,是元组的类型。默认为().如果传入的参数是单个参数,传入时写成args=(1,) *kwargs是传给target指定方法的关键字参数的容器,是字典类型。默认为{}. 如果一个子类重写了构造函数,那么它必须确保在执行线程之前调用基类Thread的构造函数(Thread.__init__())。 '''
线程方法:
__repr__(self) getName(self) #返回线程名称 isAlive = is_alive(self) #判断是否运行 isDaemon(self) #判断是否守护线程 is_alive(self) #判断是否运行 join(self, timeout=None) #阻塞线程 run(self) #运行线程 setDaemon(self, daemonic) #将线程设置为守护线程 setName(self, name) #设置线程名称 start(self) #运行线程
关于数据描述符的方法:
__dict__ #定义的线程实例的各种属性的字典集合。 t.__dict__ Out[11]: {'_args': (), '_daemonic': False, '_ident': None, '_initialized': True, '_is_stopped': False, '_kwargs': {}, '_name': 'Thread-6', '_started': <threading.Event at 0x9b6dcc0>, '_stderr': <ipykernel.iostream.OutStream at 0x87106d8>, '_target': <function __main__.f>, '_tstate_lock': None} __weakref__ #定义的线程的弱引用列表 daemon #判断一个线程是否为守护线程,返回值为布尔值。 ident #返回线程的线程标识符。如果没有启动该线程,返回值为None.线程标识符是一个非零整数。标称标识符可以在线程退出时被回收。 name #线程名,它没有语义。多个线程可以使用相同的名称。初始名称由构造函数设置。
4.join阻塞进程
语法:threading.Thread.join(self,[timeout])
Thread调用join方法可以阻塞进程直到该线程执行完毕。
通用的做法是我们启动一批线程,使得所有的子线程并行执行,再通过在下面加上一层循环,给每个子线程加上join方法,使所有的子线程结束后,再执行主线程。
实例:join的常用用法——等多个子线程并行地全部执行完成后执行主线程
1 import threading,time 2 def run(n): 3 print('Task',n,'Thread Type',threading.current_thread()) 4 time.sleep(2) 5 print('Tack done') 6 7 start_time=time.time() 8 9 t_obj=[] #定义一个列表,用来存放子线程t 10 for i in range(5): 11 t=threading.Thread(target=run,args=('thread-%s'%i,)) 12 t.start() 13 t_obj.append(t) 14 print('cost:',time.time()-start_time) 15 16 print(t_obj) #查看所有的子线程 17 for t in t_obj: 18 t.join() #用加一层循环的方法,阻塞所有的线程,等所有子线程执行结束后,执行下面的线程(此处为主线程)。 19 20 print('thread type',threading.current_thread()) #由于上面子线程都加上了join,所以主线程会等子线程全部完成后再执行 21 print('total cost:',time.time()-start_time)
返回:
上面的脚本的执行结果显示join的常用方式:
通过for循环并行启动了所有的子线程,再通过for循环join,让上面一层的for循环中的所有的子线程执行完成后(或超时后)再执行下面的主线程。
这样能够确保在执行主线程之前子线程是以并行的状态运行的,又能确保主线程运行之前所有的join的子线程全部都已经运行结束。
实例:对上面实例的实现的错误用法,将程序变成了串形的join。
阻塞进程,将并行的线程全部加上了join,将线程阻塞,编程了串形的运行方式。
1 import threading,time 2 def run(n): 3 print('Task',n,'Thread Type',threading.current_thread()) #打印任务,打印当前执行的线程类型 4 time.sleep(2) 5 print('Task done') 6 start_time=time.time() 7 for i in range(5): 8 t=threading.Thread(target=run,args=('t-%s'%i,)) 9 t.start() 10 t.join() #等待每个线程执行完成后,再执行下一个,串形的运行方式。 11 print('cost:', time.time() - start_time) #只能检测每个子线程的执行时间 12 print(threading.current_thread()) #打印当前线程类型,确认当前执行的线程为子线程还是主线程。——该阶段为主线程 13 print('cost:',time.time()-start_time)
返回:
通过上面脚本执行返回的结果来看,t.join()加在了每个线程的后面,阻塞了每一个线程,导致每一个线程都要等前面一个线程执行完成后才执行,最后执行主线程。
5.守护进程Daemon
什么是守护进程?
有些线程执行后台任务,比方说发送keepalive包,或者执行周期性垃圾收集等等。
这些后台任务仅有主程序执行时才可以运行,一旦其他的非守护进程线程退出,这些线程就会直接停止。
如果没有守护进程线程,则必须跟踪这些线程,并且告诉这些线程必须在主程序彻底退出之前退出。
通过设置守护线程,可以不用管它们运行程序,当程序退出是,所有的守护进程线程都会被自动关闭。
Some threads do background tasks, like sending keepalive packets, or performing periodic garbage collection, or whatever.
These are only useful when the main program is running, and it's okay to kill them off once the other,non-daemon, threads have exited.
Without daemon threads, you'd have to keep track of them, and tell them to exit, before your program can completely quit.
By setting them as deamon threads, you can let them run and forget about them, and when you program quits, any daemon threads are killed automatically.
注意:守护进程线程会在关闭时自动中止。它们占用的资源(例如打开文件,数据库事务等)可能不能正确发布。
如果想要线程优雅地中止,可以使他们成为非守护进程线程,并使用一个合适的信号机制,如事件。
Note:Daemon threads are abruptly stopped at shutdown. Their resources (such as open files, database transactions, etc.) may not be released properly.
If you want your threads to stop gracefully, make them non-daemonic and use a suitable signalling mechanism such as an Event.
主线程,子线程和守护线程之间的关系?
1 主线程
主线程就是程序本身,可以通过thread.current_thread()打印查看。
如果是主线程则会返回<_MainThread(MainThread,started 3636)>,如果是子线程就会返回<Thread(Thread-1,started 8200)>.后面是started后面跟的是线程号(PID)。
threading.active_count():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
2 主线程和子线程的关系
主线程和子线程的关系是相互独立的,没有依赖关系的。
子线程不加join的情况下,子线程不会等主线程执行完毕才执行。在主线程先执行结束,子线程还未执行结束的情况下,程序还是会等子线程执行完成后才退出。
子线程加join的情况下,主线程会等子线程执行完毕之后再执行,最后才退出。
3 子线程与守护线程
将子线程转化为守护线程的方法是thread.setDaemon(True),把当前线程设置为守护线程。并且setDamon(True)一定是放在thread.start()之前设置。
将子线程转化为守护线程之后,只要主线程执行完毕,程序只会等待非守护线程执行结束,不论守护线程是否执行完毕,程序都会退出。
4 主线程与守护线程
一个主线程可以启动多个守护线程,守护线程依赖主线程。
守护线程执行一些后台任务,如监听服务,接收数据包等,辅助主线程执行任务。
实例:设置守护进程
1 import threading,time 2 def run(n): 3 print('Task',n,'Thread Type',threading.current_thread()) 4 time.sleep(2) 5 print('Task done') 6 start_time=time.time() 7 for i in range(5): 8 t=threading.Thread(target=run,args=('t-%s'%i,)) 9 t.setDaemon(True) 10 t.start() 11 print('cost:', time.time() - start_time) #只能检测每个子线程的执行时间 12 print(threading.current_thread()) #打印当前线程类型,确认当前执行的线程为子线程还是主线程。——该阶段为主线程 13 print('cost:',time.time()-start_time)
返回:
解析:
从上面脚本返回的结果来看,主线程花费了0.0020003318786621094s,程序退出。上面的for循环中的守护线程全部随着程序的中止而中止。全部都只打印了开始,而没有打印结束的结果。
三、GIL,Thread Lock线程锁(互斥锁Mutex)和递归锁
1.GIL和Thread Lock
GIL全局解释锁,保证执行时,同一时间就只有一个线程在运行。
Thread Lock线程锁,又称互斥锁Mutex,保证程序执行时,同一时间就只有一个线程在修改数据。与GIL无关。
2.CPython加入GIL的原因?
为了降低程序开发的复杂度。
比方说:写Python时不用考虑内存回收的问题,Python解释器会自动定期进行内存回收。
Python解释器里有一个独立的线程,每过一段时间这个线程会wake up做一次全局论询来看看哪些内存数据是可以清空的。
此时,我们自己写的Python程序里的线程和Python解释器自己的线程是并发运行的。假设你的线程删除了一个变量,Python解释器的垃圾回收线程在清空这个变量的过程中clearing时刻,可能一个其他线程正好又重新给这个还没来得及清空的内存空间赋值了,结果就有可能新赋值的数据被删除了。
为了解决这种类似的问题,Python解释器简单粗暴的加了一层锁,即当一个线程运行时,其他人都不能动,这样就解决了上述的问题。
这也是Python早期版本的遗留问题。
3.用户程序加互斥锁的原因?
GIL仅保证在统一时间内仅一个线程能够运行,但是在Python2.x中,有可能会出现线程对数据的修改时发生混乱。
混乱的原因:
每个线程对数据进行修改时,不是每次的执行都能修改成功,如下图的示例中,第5步执行时间到了,但是被没有完成数据修改的所有指令,就释放了GIL;
线程2成功修改了count;线程1继续申请GIL锁,继续执行指令,继续修改count。
如果线程多,可能会有不同的线程在统一时间修改同一数据,可能会导致数据混乱,所以需要对数据的加上一层程序的用户锁。
具体可以参考:http://www.cnblogs.com/alex3714/articles/5230609.html
4.GIL的多线程运行实例
图示:
脚本实例:
1.数据修改之前,先生成一个互斥锁的实例;
2.程序中修改数据之前先申请互斥锁,锁住数据,修改数据成功后,释放锁。
3.在实际程序运行过程中,Python3中似乎实现了优化,但是官方未有声明称Python3中不需要加Mutex。
1 import threading 2 def run(n): 3 lock.acquire() 4 global num #在函数中通过global声明全局变量num 5 num +=1 6 # time.sleep(1) #这个time.sleep()时,用户锁还没释放,把程序变成了串形。 7 lock.release() 8 lock = threading.Lock() #先生成一个实例,后面并发运行5000个子线程时,在对数据进行修改时,通过lock.acquire(),lock.release()加上程序锁,将数据修改的运行编程了串形的。 9 num = 0 10 t_objs = [] #存线程实例 11 for i in range(5000): 12 t = threading.Thread(target=run,args=("t-%s" %i ,)) 13 t.start() 14 t_objs.append(t) #为了不阻塞后面线程的启动,不在这里join,先放到一个列表里 15 for t in t_objs: #循环线程实例列表,等待所有线程执行完毕 16 t.join() 17 18 print("num:",num)
返回结果:向屏幕打印"num: 5000".
如果不加上Thread Lock,在Python2中或linux上运行的脚本中可能返回的结果会小于5000.因为多个子线程对同一块内存数据进行操作,未加互斥锁的情况下,出现了数据混淆。
5.递归锁RLock
在一个大锁中还要再包含子锁。
如果还是用lock = threading.Lock()生成一个锁的实例,那么下面的程序在执行的过程中会出现大锁和子锁的混乱,导致无法正确地release对应的锁,会出现死循环。
而lock=threading.RLock()可以用来生成一个递归锁的实例,实现大锁包小锁。
实现的原理主要通过字典的方式实现:
locks = { door1:key1, door2:key2, }
实例:
1 import threading, time 2 def run1(): 3 print("grab the first part data") 4 lock.acquire() #申请加锁 5 global num 6 num += 1 7 lock.release() #释放锁 8 return num 9 10 def run2(): 11 print("grab the second part data") 12 lock.acquire() #申请加锁 13 global num2 14 num2 += 1 15 lock.release() #释放锁 16 return num2 17 18 def run3(): 19 lock.acquire() #申请加锁 20 res = run1() 21 print('--------between run1 and run2-----') 22 res2 = run2() 23 lock.release() #释放锁 24 print(res, res2) 25 26 num, num2 = 0, 0 27 28 lock = threading.RLock() #生成了一个递归锁的实例. 29 30 for i in range(10): 31 t = threading.Thread(target=run3,) 32 t.start() 33 34 while threading.active_count() != 1: 35 print(threading.active_count()) 36 else: 37 print('----all threads done---') 38 print(num, num2)
递归锁在实际的应用中,没有太多的应用场景,但是一定要知道遇到多层锁的情况下要使用递归锁。
6.信号量Semaphore
互斥锁Mutex同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据。
使用方法同互斥锁一样,都是先生成一个实例,再在程序中申请锁住,修改完成后,释放锁。
实例:
1 import threading, time 2 def run(n): 3 semaphore.acquire() 4 time.sleep(1) 5 print("run the thread: %s " % n) 6 semaphore.release() 7 if __name__ == '__main__': 8 semaphore = threading.BoundedSemaphore(5) # 最多允许5个线程同时运行 9 for i in range(22): 10 t = threading.Thread(target=run, args=(i,)) 11 t.start() 12 while threading.active_count() != 1: 13 pass 14 else: 15 print('----all threads done---')
上面的实例的执行结果是:向屏幕输出的打印值,每五个向屏幕输出,22个子线程,最后向屏幕打印了2个。
7.Event事件
Event是一个简单的同步对象(synchronization object)。
Event对象用于线程间的通信,它是由线程设置的信号标志,它提供了设置信号,清除信号,等待等方法用于实现线程间的通信。
信号标志可以被设置、清除、等待。
如果由线程设置的信号标志为假,则线程等待直到信号被其他线程设置为真。
生成一个event对象:
event = threading.Event()
设置信号:设置Event对象内部的信号标志为真。event.is_set()方法可以用来判断其内部信号标志的状态。
event.set()
清除信号:清除Event对象内部的信号标志,将其设为假。
event.clear()
import threading event=threading.Event() #生成一个事件实例 event.is_set() #判断事件是否设置信号标志 Out[3]: False event.set() #将事件的信号标志设置为真 event.is_set() Out[5]: True event.clear() #清除信号 event.is_set() Out[8]: False
等待:Event对象wait的方法只有在内部标志为真的时候才会很快的执行并完成返回。当Event对象的内部信号标志为假时,则wait方法会一直等到其为真时才返回。
event.wait() #等待标志位被设置,如果未设置,则阻塞线程;如果设置了,则执行。
实例:红绿灯
通过Event来实现两个或多个线程间的交互:启动一个线程做交通指挥灯,生成几个线程做车辆,测量行驶按红灯停,绿灯行的规则。
1 import threading, time 2 event = threading.Event() # 生成一个事件实例 3 4 def traffic_signal(): 5 sec = 0 # 初始化一个计时器 6 while True: 7 if sec <= 10: #10s内,设置为红灯 8 event.clear() #清除信号 9 print('