今日要整理的内容有html
1. 操做系统中线程理论python
2.python中的GIL锁程序员
3.线程在python中的使用算法
开始今日份整理编程
1. 操做系统中线程理论小程序
1.1 线程引入背景安全
以前咱们已经了解了操做系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,咱们容许多个程序同时加载到内存中,在操做系统的调度下,能够实现并发地执行。这是这样的设计,大大提升了CPU的利用率。进程的出现让每一个用户感受到本身独享CPU,所以,进程就是为了在CPU上实现多道编程而提出的。服务器
那么有了进程为何还须要线程多线程
进程有不少优势,它提供了多道编程,让咱们感受咱们每一个人都拥有本身的CPU和其余资源,能够提升计算机的利用率。不少人就不理解了,既然进程这么优秀,为何还要线程呢?其实,仔细观察就会发现进程仍是有不少缺陷的,主要体如今两点上:并发
进程只能在一个时间干一件事,若是想同时干两件事或多件事,进程就无能为力了。
进程在执行的过程当中若是阻塞,例如等待输入,整个进程就会挂起,即便进程中有些工做不依赖于输入的数据,也将没法执行。
若是这两个缺点理解比较困难的话,举个现实的例子也许你就清楚了:若是把咱们上课的过程当作一个进程的话,那么咱们要作的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而若是只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能作一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;若是老师在黑板上写演算过程,咱们开始记笔记,而老师忽然有一步推不下去了,阻塞住了,他在那边思考着,而咱们呢,也不能干其余事,即便你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。
如今你应该明白了进程的缺陷了,而解决的办法很简单,咱们彻底可让听、写、思三个独立的过程,并行起来,这样很明显能够提升听课的效率。而实际的操做系统中,也一样引入了这种相似的机制——线程。
注意:进程是资源分配的最小单位,线程是cpu调度的最小单位,每个进程中至少一个线程
1.2 进程与线程的区别
进程与线程之间的区别主要是有如下几点
1.3 线程的特色
在多线程的操做系统中,一般是在一个进程中包括多个线程,每一个线程都是做为利用CPU的基本单位,是花费最小开销的实体。线程具备如下属性。
补充TCB的内容
TCB包括如下信息: (1)线程状态。 (2)当线程不运行时,被保存的现场资源。 (3)一组执行堆栈。 (4)存放每一个线程的局部变量主存区。 (5)访问同一个进程中的主存和其它资源。 用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
1.4 使用线程的使用场景
开启一个字处理软件进程,该进程确定须要办不止一件事情,好比监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操做的都是同一块数据,于是不能用多进程。只能在一个进程里并发地开启三个线程,若是是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
1.5 内存中的线程
多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。
而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其余物理资源。多线程的运行也多进程的运行相似,是cpu在多个线程之间的快速切换。
不一样的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,若是迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序建立,因此同一进程内的线程是合做关系,一个线程能够访问另一个线程的内存地址,你们都是共享的,一个线程干死了另一个线程的内存,那纯属程序员脑子有问题。
相似于进程,每一个线程也有本身的堆栈,不一样于进程,线程库没法利用时钟中断强制线程让出CPU,能够调用thread_yield运行线程自动放弃cpu,让另一个线程运行。
线程一般是有益的,可是带来了不小程序设计难度,线程的问题是:
1. 父进程有多个线程,那么开启的子线程是否须要一样多的线程
2. 在同一个进程中,若是一个线程关闭了文件,而另一个线程正准备往该文件内写内容呢?
所以,在多线程的代码中,须要更多的心思来设计程序的逻辑、保护程序的数据。
2.python中的GIL锁
2.1 python如何使用GIL锁
全局解释器锁GIL
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中能够“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
在多线程环境中,Python 虚拟机按如下方式执行:
a、设置 GIL;
b、切换到一个线程去运行;
c、运行指定数量的字节码指令或者线程主动让出控制(能够调用 time.sleep(0));
d、把线程设置为睡眠状态;
e、解锁 GIL;
d、再次重复以上全部步骤。
在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(因为在这期间没有Python的字节码被运行,因此不会作线程切换)编写扩展的程序员能够主动解锁GIL。
python中线程不能同时访问cpu的确是一个缺点,不过咱们在写代码的时候能够判断这个代码块是多I/O仍是多计算,多I/O即便是单cpu也是能够顺畅执行的,不过对于高计算的则开启多进程而后进程中开启多线程并发的执行计算,对于python还有一个缘由是他是解释性语言,在编译的同时不知道下面的代码的具体执行状况,不能多cpu同时调用。
3.线程在python中的使用
3.1线程的俩种方式
方式一:
from threading import Thread import time import os def func(i): time.sleep(1) print('子进程%s!'%i,os.getpid()) for i in range(10): t = Thread(target= func,args=(i,)) t.start() print('主进程!',os.getpid())
方式二:
#自建类的方式 from threading import Thread import time import os class MyThread(Thread): def __init__(self,name): super().__init__() self.name = name def run(self):#和进程同样,自建类,必须用run函数 time.sleep(1) print('子进程%s'%self.name,os.getpid()) if __name__ == '__main__': print('主线程!',os.getpid()) for i in range(10): p = MyThread(i) p.start()
执行代码咱们会发现,肉眼所看基本上全部的线程都是一块儿创建成功,这个时候就会有疑问,不是有GIL全局解释锁吗,不该该每隔一秒才建立一个线程吗,看图
步骤以下
注意:程序中的IO操做是不占用全局解释器锁和CPU的
3.2 threading内的其余用法
3.2.1 join方法
线程中join方法和进程中的join方法时一致的,都是对主线程的阻塞,直到子线程的中的代码执行结束
看代码
from threading import Thread import time def func(i): time.sleep(1) print('子线程%s'%i) t_list =[] for i in range(10): t = Thread(target= func,args=(i,)) t_list.append(t) t.start() for t in t_list: t.join() print('全部线程已经所有执行完毕!')
3.2.2 守护线程
守护线程和守护进程是差很少的,只是开启的方式一个是参数一个是内置方法
from threading import Thread import time def main(): print('主线程开始!') time.sleep(3) print('主线程结束') def func(): print('子线程开始') time.sleep(5) print('子线程结束') def daemon(): while True: time.sleep(1) print('我活的很好!') t = Thread(target= daemon,) t.setDaemon(True)#设定为守护线程的方法 t.start()
t1 = Thread(target= main,)
t2 = Thread(target= func,)
t1.start()
t2.start()
看结果会发现,守护线程不紧守护了主线程,同时守护了子线程,能够得出如下结果
守护线程和守护进程不一样,守护线程会守护主线程直到主线程结束,若是这个时候主线程要等待子线程的执行结束,那么守护线程同时对子线程进行守护
3.2.3 数据共享
同一个进程内的线程因为是共享一个资源空间,因此一个进程内的线程是天生共享资源,看代码
# from threading import Thread # # n = 100 # def func(): # global n # n-=1 # # t_list =[] # for i in range(100): # t = Thread(target= func,) # t_list.append(t) # t.start() # for i in t_list: # i.join() # print(n)
3.2.4 查看线程id
俩种方式
方式一:
#方式一:面向对象的方式 from threading import Thread import os class MyThread(Thread): def __init__(self,name): super().__init__() self.name = name def run(self): print('子进程%s'%self.name,self.ident,os.getpid()) for i in range(10): t = MyThread(i) t.start()
方式二:
#方式二:普通方式 from threading import Thread,currentThread import os def func(i): print('子进程%s'%i,currentThread().ident,os.getpid()) for i in range(10): t = Thread(target= func,args= (i,)) t.start()
3.2.5 threading的其余方法
from threading import Thread,currentThread,enumerate,active_count import os import time def func(i): time.sleep(1) print('子进程%s'%i,currentThread().ident,os.getpid()) for i in range(10): t = Thread(target= func,args= (i,)) t.start() print(enumerate())#枚举查看活着的线程 print(active_count())#查看活着的线程数量,子线程数量加上主线程
3.3 线程中的锁
3.3.1 互斥锁
虽然说一个进程内线程都是共享资源,,可是实际中咱们仍是在线程中若是有+=、-=、*=、/=等操做仍是要加锁,先看例子,为何会这样
from threading import Thread import time n = 0 def func(): global n count = n time.sleep(0.1)#用阻塞模拟时间片轮转 n = count+1 t_list =[] for i in range(100): t = Thread(target= func) t_list.append(t) t.start() for i in t_list: i.join() print(n) #结果为1 #若是将阻塞的代码注释后就会变成100 #对于操做系统来讲,必定数量的线程并不会出现问题,不过在实际使用中为了安全性,建议不要作,须要加锁 #修改后的代码 from threading import Thread,Lock n = 0 def func(lock):#加锁操做 global n with lock: n +=1 t_list =[] lock = Lock() for i in range(100): t = Thread(target= func,args=(lock,)) t_list.append(t) t.start() for i in t_list: i.join() print(n) #结果:100 #这杨无论如何,结果都是100
结论:
3.3.2 迭代锁
3.3.2.1 死锁问题
要说迭代锁首先要说死锁问题,而说明死锁问题就要说一下著名的科学家吃面问题
科学家吃面问题:四个科学家吃面,桌子上只有一盘面和一把叉子,只有在同时拿到面和叉子,才能够迟到,拿到面或者叉子是不能作任何事情,下面模拟下科学家吃面这个问题,代码以下
#死锁版
from threading import Thread,Lock import time nooodle_lock = Lock() fork_lock = Lock() def eat1(name): nooodle_lock.acquire() print('%s 拿到了面条!'%name) fork_lock.acquire() print('%s 拿到了叉子!' % name) print('%s 开始吃面'%name) time.sleep(1)#模拟吃面 fork_lock.release() print('%s 放下了叉子!' % name) nooodle_lock.release() print('%s 放下了面条'% name) def eat2(name): fork_lock.acquire() print('%s 拿到了叉子!' % name) nooodle_lock.acquire() print('%s 拿到了面条!' % name) print('%s 开始吃面'%name) time.sleep(1)#模拟吃面 nooodle_lock.release() print('%s 放下了面条'% name) fork_lock.release() print('%s 放下了叉子!' % name) t1 = Thread(target= eat1,args=('饭桶1',)) t2 = Thread(target= eat2,args=('饭桶2',)) t3 = Thread(target= eat1,args=('饭桶3',)) t4 = Thread(target= eat2,args=('饭桶4',)) t1.start() t2.start() t3.start() t4.start() #结果 饭桶1 拿到了面条! 饭桶1 拿到了叉子! 饭桶1 开始吃面 饭桶1 放下了叉子! 饭桶2 拿到了叉子! 饭桶1 放下了面条 饭桶3 拿到了面条! #饭桶2 和饭桶3 各拿面条和叉子互不放弃,这样就形成了死锁
3.3.2.2 迭代锁
根据上面的例子,咱们就须要引入一个迭代锁的概念
正常的互斥锁,就至关于一个门,一把钥匙,谁抢到谁进去,如图
而迭代锁就是一个门,但是门口挂着一串钥匙,拿到这一串钥匙的人就能够进入门后面的门,如图
把上面的科学家吃面问题解决一下
#迭代锁版
from threading import Thread,RLock import time nooodle_lock =fork_lock= RLock() def eat1(name): nooodle_lock.acquire() print('%s 拿到了面条!'%name) fork_lock.acquire() print('%s 拿到了叉子!' % name) print('%s 开始吃面'%name) time.sleep(1)#模拟吃面 fork_lock.release() print('%s 放下了叉子!' % name) nooodle_lock.release() print('%s 放下了面条'% name) def eat2(name): fork_lock.acquire() print('%s 拿到了叉子!' % name) nooodle_lock.acquire() print('%s 拿到了面条!' % name) print('%s 开始吃面'%name) time.sleep(1)#模拟吃面 nooodle_lock.release() print('%s 放下了面条'% name) fork_lock.release() print('%s 放下了叉子!' % name) t1 = Thread(target= eat1,args=('饭桶1',)) t2 = Thread(target= eat2,args=('饭桶2',)) t3 = Thread(target= eat1,args=('饭桶3',)) t4 = Thread(target= eat2,args=('饭桶4',)) t1.start() t2.start() t3.start() t4.start() #结果就是每个人均可以吃到面了,只是Lock改成RLock
总结:
那么按照这个思路,咱们能够改变一下上面的科学家吃面问题
#最终版
from threading import Thread,Lock import time lock = Lock() def eat1(name): lock.acquire() print('%s 拿到了面条!'%name) print('%s 拿到了叉子!' % name) print('%s 开始吃面'%name) time.sleep(1)#模拟吃面 print('%s 放下了叉子!' % name) print('%s 放下了面条'% name) lock.release() def eat2(name): lock.acquire() print('%s 拿到了叉子!' % name) print('%s 拿到了面条!' % name) print('%s 开始吃面'%name) time.sleep(1)#模拟吃面 print('%s 放下了面条'% name) print('%s 放下了叉子!' % name) lock.release() Thread(target= eat1,args=('饭桶1',)).start() Thread(target= eat2,args=('饭桶2',)).start() Thread(target= eat1,args=('饭桶3',)).start() Thread(target= eat2,args=('饭桶4',)).start()
最终结论:使用迭代锁的时候,绝大多数是由于代码中各类各样的锁太多,机制混乱,在一头雾水的时候能够先用带带锁解决问题,而后在想着一点点的去解决问题!
小练习:基于多线程的socket的套接字,简易版
#server端 import socket from threading import Thread sk = socket.socket() sk.bind(('127.0.0.1',8500)) sk.listen() def talk(conn,addr): print(addr) while True: conn.send(b'hello world!') while True: conn,addr = sk.accept() Thread(target=talk,args=(conn,addr)).start()#将具体聊天的过程交给不一样的线程去执行 #客户端 import socket sk = socket.socket() sk.connect(('127.0.0.1',8500)) while True: msg = sk.recv(1024) print(msg) sk.close() #最后的结果就是服务器端能够接收多个客户端链接,每一个客户端能够不受影响的去接收
3.4 线程队列
首先说一下,一个进程中的多线程共用一块资源空间,为何还须要用到队列,主要是有俩个方面
不一样于多进程中的有专门的队列模块,线程中使用队列就须要调用python中自带模块queue模块
3.4.1 线程队列中经常使用的方法
#线程队列中的经常使用方法 1.队列的实例化: q = queue.Queue(num)#num能够为空,加上则设定队列的大小 2.队列的放入以及取出: q.put() q.get() #他俩都是阻塞事件 3.队列状态的判断 q.size() q.empty() q.full() 在多进程中状态常常有误,不多去使用 4.队列的快速取出以及快速放入 import queue q = queue.Queue(1) q.put(1) try: print(q.put_nowait('abc'))#快速放入,无论队列是都满,若是满则丢弃,因此常常不用,队列满会报queue.Full错误 except queue.Full: pass try: print(q.get_nowait())#快速取出,无论队列是否空,空则报错queue.Empty except queue.Empty: pass
3.4.2 算法中的经常使用数据类型
在算法中有如下经常使用数据类型
3.4.3 其余补充队列
第一种:栈,后进先出
import queue q = queue.LifoQueue()#栈 q.put(1) q.put(2) q.put(3) print(q.get()) print(q.get()) print(q.get()) #结果 3 2 1
第二种:优先级队列,put的时候是一个元祖,按照元祖第一个元素的ASCII码的位置
import queue q = queue.PriorityQueue() q.put((2,'abc')) q.put((1,'bbb')) q.put((3,'ccc')) print(q.get()) print(q.get()) print(q.get()) #结果 (1, 'bbb') (2, 'abc') (3, 'ccc')
3.5 线程池
线程在最开始建立的时候是没有线程池的概念的,因此线程与进程不一样的是没有专门的线程池的方法,须要调用其余模块
Python标准模块--concurrent.futures,官方参考介绍文章
https://docs.python.org/dev/library/concurrent.futures.html
concurrent.futures模块经常使用方法
#1 介绍 concurrent.futures模块提供了高度封装的异步调用接口 ThreadPoolExecutor:线程池,提供异步调用 ProcessPoolExecutor: 进程池,提供异步调用 Both implement the same interface, which is defined by the abstract Executor class. #2 基本方法 #submit(fn, *args, **kwargs) 异步提交任务 #map(func, *iterables, timeout=None, chunksize=1) 取代for循环submit的操做 #shutdown(wait=True) 至关于进程池的pool.close()+pool.join()操做 wait=True,等待池内全部任务执行完毕回收完资源后才继续 wait=False,当即返回,并不会等待池内的任务执行完毕 但无论wait参数为什么值,整个程序都会等到全部任务执行完毕 submit和map必须在shutdown以前 #result(timeout=None) 取得结果 #add_done_callback(fn) 回调函数
3.5.1 线程模块
#线程模块 #1.普通建池 from threading import currentThread from concurrent.futures import ThreadPoolExecutor import time def func(i): time.sleep(1) print('子线程%s'%i,currentThread().ident)#查看线程名,以及线程id tp = ThreadPoolExecutor(4)#建池 for i in range(20): tp.submit(func,i)#就至关于进程池中的apply_async异步的提交任务 tp.shutdown()#至关于进程池中的.close()与.join() #2.map建池 from threading import currentThread from concurrent.futures import ThreadPoolExecutor import time def func(i): time.sleep(1) print('子线程%s'%i,currentThread().ident)#查看线程名,以及线程id tp = ThreadPoolExecutor(4)#建池 tp.map(func,range(20)) #3.获取返回值 from threading import currentThread from concurrent.futures import ThreadPoolExecutor import time def func(i): time.sleep(1) print('子线程%s'%i,currentThread().ident)#查看线程名,以及线程id return i**2 tp = ThreadPoolExecutor(4)#建池 ret_l =[] for i in range(20): ret = tp.submit(func,i)#就至关于进程池中的apply_async异步的提交任务 ret_l.append(ret) tp.shutdown()#至关于进程池中的.close()与.join() for i in ret_l: print(i.result())#就至关于进程中的结果的get() #4.回调函数 from threading import currentThread from concurrent.futures import ThreadPoolExecutor import time def func(i): time.sleep(1) print('子线程%s'%i,currentThread().ident)#查看线程名,以及线程id return i**2 def call_back(ret): print(ret.result())#ret只是获取到结果的对象,须要对对象进行result() tp = ThreadPoolExecutor(4)#建池 for i in range(20): tp.submit(func,i).add_done_callback(call_back)#就至关于进程池中的apply_async异步的提交任务,和进程池中的callback=函数名一致 tp.shutdown()#至关于进程池中的.close()与.join()
3.5.2 进程模块
#进程模块 #1.普通建池 from concurrent.futures import ProcessPoolExecutor import os import time def func(i): time.sleep(1) print('子进程%s'%i,os.getpid()) if __name__ == '__main__': pp= ProcessPoolExecutor(4) for i in range(20): pp.submit(func,i) pp.shutdown() #2.map建池 from concurrent.futures import ProcessPoolExecutor import os import time def func(i): time.sleep(1) print('子进程%s'%i,os.getpid()) if __name__ == '__main__': pp= ProcessPoolExecutor(4) pp.map(func,range(20)) #3.返回值 from concurrent.futures import ProcessPoolExecutor import os import time def func(i): time.sleep(1) print('子进程%s'%i,os.getpid()) return i**2 if __name__ == '__main__': pp= ProcessPoolExecutor(4) ret_l =[] for i in range(20): ret = pp.submit(func,i) ret_l.append(ret) pp.shutdown() for ret in ret_l: print(ret.result()) #4.回调函数 from concurrent.futures import ProcessPoolExecutor import os import time def func(i): time.sleep(1) print('子进程%s'%i,os.getpid()) return i**2 def call_back(ret): print(ret.result()) if __name__ == '__main__': pp= ProcessPoolExecutor(4) for i in range(20): pp.submit(func,i).add_done_callback(call_back) pp.shutdown()
最后:后期咱们建池的话主要使用concurrent.futures,一是比较新,二是既可建线程池也能够创建进程池,方便使用!
总结: