本节内容
- 什么是线程
- 线程与进程的区别
- 开启线程的两种方式
- Thread对象的其他属性或方法
- 守护线程
- GIL全局解释器锁
- 死锁和递归锁
- 信号量 event 计时器
- 线程queue
一 什么是线程
线程相对于进程更为轻量级,当一个进程启动同时也会启动一个主线程,多线程就是指在一个进程下创建多个线程并且这些线程共享地址空间。所以进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
二 线程与进程的区别
1 Threads share the address space of the process that created it; processes have their own address space.
2 Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
3 Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
4 New threads are easily created; new processes require duplication of the parent process.
5 Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
6 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.
总结上述区别,无非两个关键点,这也是我们在特定的场景下需要使用多线程的原因:
同一个进程内的多个线程共享该进程内的地址资源
创建线程的开销要远小于创建进程的开销(创建一个进程,就是创建一个车间,涉及到申请空间,而且在该空间内建至少一条流水线,但创建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小)
三 开启线程的两种方式
开启线程的方式
方式一
1.创建线程的开销比创建进程的开销小,因而创建线程的速度快 from multiprocessing import Process from threading import Thread import os import time def work(): print('<%s> is running'%os.getpid()) time.sleep(2) print('<%s> is done'%os.getpid()) if __name__ == '__main__': t=Thread(target=work,) # t= Process(target=work,) t.start() print('主',os.getpid()) 开启进程的第一种方式
方式二
from threading import Thread import time class Work(Thread): def __init__(self,name): super().__init__() self.name = name def run(self): # time.sleep(2) print('%s say hell'%self.name) if __name__ == '__main__': t = Work('egon') t.start() print('主') 开启线程的第二种方式(用类)
基于多进程多线程实现套接字通信
import socket from multiprocessing import Process from threading import Thread def create_socket(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('127.0.0.3', 8080)) server.listen(5) return server def talk(conn): while True: try: data = conn.recv(1024) if data is None: break conn.send(data.upper()) except ConnectionError: break conn.close() def communication(server): while True: print('wait....') conn, add = server.accept() t = Thread(target=talk, args=(conn,)) t.start() if __name__ == '__main__': server = create_socket() p1 = Process(target=communication, args=(server,)) p2 = Process(target=communication, args=(server,)) p1.start() p2.start()
编写一个简单的文本处理工具,具备三个任务,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
from threading import Thread msg_l = [] format_l = [] def user_input(): while True: text = input('请输入内容:') if text is None: continue msg_l.append(text) def format_text(): while True: if msg_l: reg = msg_l.pop() format_l.append(reg.upper()) def save(): while True: if format_l: with open('db1.txt', 'a', encoding='utf-8') as f: res = format_l.pop() f.write('%s ' % res) f.flush() if __name__ == '__main__': t1 = Thread(target=user_input) t2 = Thread(target=format_text) t3 = Thread(target=save) t1.start() t2.start() t3.start()
四 Thread对象的其他属性或方法
Thread实例对象的方法 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。
threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
五 守护线程
无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁
需要强调的是:运行完毕并非终止运行
1、对主进程来说,运行完毕指的是主进程代码运行完毕
2、对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详细解释:
1、主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
2、主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
思考下述代码的执行结果有可能是哪些情况?为什么?
from threading import Thread import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': t1=Thread(target=foo) t2=Thread(target=bar) t1.daemon=True t1.start() t2.start() print("main-------") 以上代码首先会输出 123,456,main, 随后会输出end123,end456。因为t1守护的是主进程,让主进程执行完print("main-------")线程2已经在运行了所以主进程并没有结束,等到子线程运行完毕才会回收子进程的资源进程才会结束
六 GIL全局解释器锁
1 定义:
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. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)
结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
首先需要明确的一点是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
2 GIL解析
GIL本身就是一把互斥锁,所有互斥锁本质都是一样的,同一时间内共享数据只能被一个任务所修改进而保证数据安全.
在一个Python进程内不仅有当前任务的主进程或者当前主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程
总之所有线程都运行在一个进程内。
如果多个线程的target=work,那么执行流程是多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行
3 GIL与lock
很多人会有这样一个疑问::Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?
首先GIL与Lock和目的都是为了保护数据安全的,但是他们所保护的数据有所不同,前者是解释器级别(保护的就是解释器级别的数据,比如垃圾回收的数据),
后者保护的是用户自己开的应用程序的数据而GIL确不负责这件事
分析:
100个线程去抢GIL锁,即抢执行权限
肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程
代码演示:
from threading import Thread,Lock import os,time def work(): global n lock.acquire() temp=n time.sleep(0.1) n=temp-1 lock.release() if __name__ == '__main__': lock=Lock() n=100 l=[] for i in range(100): p=Thread(target=work) l.append(p) p.start() for p in l: p.join() print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全,不加锁则结果可能为99
七 死锁和递归锁
1 死锁的现象
所谓死锁是指两个或两个以上的进程或线程在执行过程中因争夺资源而造成的一种互相等待现象,若无外力作用他们将无法推进下去
死锁代码:
1 from threading import Thread,Lock 2 import time 3 mutexA=Lock() 4 mutexB=Lock() 5 6 class MyThread(Thread): 7 def run(self): 8 self.func1() 9 self.func2() 10 def func1(self): 11 mutexA.acquire() 12 print('