操作系统线程理论

线程概念的引入背景

进程

  之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。这是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

有了进程为什么要有线程

  进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。

  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

  如果这两个缺点理解比较困难的话,举个现实的例子也许你就清楚了:如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;如果老师在黑板上写演算过程,我们开始记笔记,而老师突然有一步推不下去了,阻塞住了,他在那边思考着,而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。

  现在你应该明白了进程的缺陷了,而解决的办法很简单,我们完全可以让听、写、思三个独立的过程,并行起来,这样很明显可以提高听课的效率。而实际的操作系统中,也同样引入了这种类似的机制——线程。

线程的出现

  60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。
  因此在80年代,出现了能独立运行的基本单位——线程(Threads)
  注意:进程是资源分配的最小单位,线程是CPU调度的最小单位.
     每一个进程中至少有一个线程。 
进程和线程的关系

  

  线程与进程的区别可以归纳为以下4点:
  1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
  3)调度和切换:线程上下文切换比进程上下文切换要快得多。
  4)在多线程操作系统中,进程不是一个可执行的实体。
线程的特点
  在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
  1)轻型实体
  线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
  线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
#TCB包括以下信息:
#(1)线程状态。
#(2)当线程不运行时,被保存的现场资源。
#(3)一组执行堆栈。
#(4)存放每个线程的局部变量主存区。
#(5)访问同一个进程中的主存和其它资源。
#用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
  2)独立调度和分派的基本单位。
  在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
  3)共享进程资源。

  线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
  4可并发执行。
  在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
使用线程的实际场景

  开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。

内存中的线程

  多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。

  而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。多线程的运行也多进程的运行类似,是cpu在多个线程之间的快速切换。

  不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的,一个线程干死了另外一个线程的内存,那纯属程序员脑子有问题。

  类似于进程,每个线程也有自己的堆栈,不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。

  线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:

  1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程

  2. 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?

  因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。

用户级线程和内核级线程(了解)

  线程的实现可以分为两类:用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread),后者又称为内核支持的线程或轻量级进程。在多线程操作系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。

用户级线程

  内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu。

  

  在用户空间模拟操作系统对进程的调度,来调用一个进程中的线程,每个进程中都会有一个运行时系统,用来调度线程。此时当该进程获取cpu时,进程内再调度出一个线程去执行,同一时刻只有一个线程执行。

内核级线程

   内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程就是这样的。

  

用户级与内核级线程的对比

#用户级线程和内核级线程的区别

#1 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
#2 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
#3 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
#4 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
#5 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。
#内核线程的优缺点

#优点:当有多个处理机时,一个进程的多个线程可以同时执行。
#缺点:由内核进行调度。
#用户级线程的优缺点
#优点:
#线程的调度不需要内核直接参与,控制简单。
#可以在不支持线程的操作系统中实现。
#创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
#允许每个进程定制自己的调度算法,线程管理比较灵活。
#线程能够利用的表空间和堆栈空间比内核级线程多。
#同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。
#缺点:
#资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用

混合实现

  用户级与内核级的多路复用,内核同一调度内核线程,每个内核线程对应n个用户线程

  

  linux操作系统的 NPTL    

历史
在内核2.6以前的调度实体都是进程,内核并没有真正支持线程。它是能过一个系统调用clone()来实现的,这个调用创建了一份调用进程的拷贝,跟fork()不同的是,这份进程拷贝完全共享了调用进程的地址空间。LinuxThread就是通过这个系统调用来提供线程在内核级的支持的(许多以前的线程实现都完全是在用户态,内核根本不知道线程的存在)。非常不幸的是,这种方法有相当多的地方没有遵循POSIX标准,特别是在信号处理,调度,进程间通信原语等方面。 很显然,为了改进LinuxThread必须得到内核的支持,并且需要重写线程库。为了实现这个需求,开始有两个相互竞争的项目:IBM启动的NGTP(Next Generation POSIX Threads)项目,以及Redhat公司的NPTL。在2003年的年中,IBM放弃了NGTP,也就是大约那时,Redhat发布了最初的NPTL。 NPTL最开始在redhat linux 9里发布,现在从RHEL3起内核2.6起都支持NPTL,并且完全成了GNU C库的一部分。 设计
NPTL使用了跟LinuxThread相同的办法,在内核里面线程仍然被当作是一个进程,并且仍然使用了clone()系统调用(在NPTL库里调用)。但是,NPTL需要内核级的特殊支持来实现,比如需要挂起然后再唤醒线程的线程同步原语futex. NPTL也是一个1*1的线程库,就是说,当你使用pthread_create()调用创建一个线程后,在内核里就相应创建了一个调度实体,在linux里就是一个新进程,这个方法最大可能的简化了线程的实现。 除NPTL的1*1模型外还有一个m*n模型,通常这种模型的用户线程数会比内核的调度实体多。在这种实现里,线程库本身必须去处理可能存在的调度,这样在线程库内部的上下文切换通常都会相当的快,因为它避免了系统调用转到内核态。然而这种模型增加了线程实现的复杂性,并可能出现诸如优先级反转的问题,此外,用户态的调度如何跟内核态的调度进行协调也是很难让人满意。

介绍

线程和python

理论知识

全局解释器锁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线程模块的选择

  Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,threading提供了更高级别、功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
  避免使用thread模块,因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突;其次低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多;再者,thread模块中当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作,至少threading模块能确保重要的子线程退出后进程才退出。

  thread模块不支持守护线程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。而threading模块支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求它就在那等着,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。

threading模块

multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍(官方链接

线程的创建Threading.Thread类

线程的创建

#创建线程的方式1
from threading import Thread#引入线程模块
import time#引入时间模块
def sayhi(name):#定义一个函数
time.sleep(2)#睡2秒
print('%s say hello' %name)#打印内容 if __name__ == '__main__':#如果用户名等于当前用户名
t=Thread(target=sayhi,args=('egon',))#创建一个进程
t.start()#开始进程
print('主线程')
#创建线程的方式2
from threading import Thread#引入线程模块
import time#引入时间模块
class Sayhi(Thread):#创建一个一个类
def __init__(self,name):#初始化方法
super().__init__()#继承父类初始化
self.name=name定义一个动态属性
def run(self):#定义一个run方法
time.sleep(2)#睡2秒
print('%s say hello' % self.name)#打印 if __name__ == '__main__':#如果用户名等于当前用户名
t = Sayhi('egon')#实例化一个人
t.start()#开始线程
print('主线程')#打印

多线程与多进程

#pid的比较
from threading import Thread#引入一个线程模块
from multiprocessing import Process#引入一个进程模块
import os#引入操作系统模块 def work():#定义一个函数
print('hello',os.getpid())#打印 if __name__ == '__main__':#如果用户名等于当前文件名
#part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
t1=Thread(target=work)#创建一个线程
t2=Thread(target=work)#创建一个线程
t1.start()#开始线程
t2.start()#开始线程
print('主线程/主进程pid',os.getpid())#打印这个id #part2:开多个进程,每个进程都有不同的pid
p1=Process(target=work)#创建一个进程
p2=Process(target=work)#创建一个进程
p1.start()#开启进程
p2.start()#开启进程
print('主线程/主进程pid',os.getpid())
#开启效率的较量
from threading import Thread#引入线程模块
from multiprocessing import Process#引入进程模块
import os#引入系统模块 def work():#定义一个工作函数
print('hello')#打印内容 if __name__ == '__main__':#如果文件名等于当前文件名
#在主进程下开启线程
t=Thread(target=work)#创建一个线程
t.start()#开启线程
print('主线程/主进程')#打印内容
'''
打印结果:
hello
主线程/主进程
''' #在主进程下开启子进程
t=Process(target=work)#开启一个主进程
t.start()#开启主进程
print('主线程/主进程')
'''
打印结果:
主线程/主进程
hello
'''
#内存数据的共享问题
from threading import Thread#引入线程模块
from multiprocessing import Process#引入进程模块
import os#引入系统模块
def work():#定义一个工作方法
global n#定义一个全局变量
n=0#赋值 if __name__ == '__main__':#如果用户名等于当前用户名
# n=100#赋值n
# p=Process(target=work)#创建一个进程
# p.start()#开始这个进程
# p.join()#等待进程结束
# print('主',n) #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100 n=1#赋值一个n
t=Thread(target=work)#创建一个线程
t.start()#开启这个线程
t.join()#等待线程结束
print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
同一进程内的线程共享该进程的数据?

练习 :多线程实现socket

#server端
#_*_coding:utf-8_*_
#!/usr/bin/env python
import multiprocessing#引入多元进程
import threading#引入线程模块 import socket#引入套接字模块
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#实例化对象
s.bind(('127.0.0.1',8080))#绑定ip和端口
s.listen(5)#监听5个字节 def action(conn):#定义一个函数
while True:#循环为真
data=conn.recv(1024)#接收数据
print(data)#打印数据
conn.send(data.upper())#发送信息 if __name__ == '__main__':#如果用户名等于当前用户名 while True:#循环为真
conn,addr=s.accept()#接收两个参数 p=threading.Thread(target=action,args=(conn,))#创建一个线程
p.start()#开启线程
#_*_coding:utf-8_*_
#!/usr/bin/env python import socket#引入套接字模块 s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#创建一个一个套接字对象
s.connect(('127.0.0.1',8080))#绑定ip地址和接口 while True:#循环为真
msg=input('>>: ').strip()#输入你的内容
if not msg:continue#如果没有内容则跳过 s.send(msg.encode('utf-8'))#发送信息
data=s.recv(1024)#接收数据
print(data)#打印这个数据

Thread类的其他方法

Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。 threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
#代码示例
from threading import Thread#引入线程模块
import threading#引入线程
from multiprocessing import Process#引入进程模块
import os#引入系统模块 def work():#定义一个函数
import time#引入时间模块
time.sleep(3)#睡3秒
print(threading.current_thread().getName())#返回线程名 if __name__ == '__main__':#如果名字等于当前名字
#在主进程下开启线程
t=Thread(target=work)#开启线程
t.start()#开始这个线程 print(threading.current_thread().getName())#返回线程名
print(threading.current_thread()) #主线程
print(threading.enumerate()) #连同主线程在内有两个运行的线程
print(threading.active_count())#打印内容
print('主线程/主进程') '''
打印结果:
MainThread
<_MainThread(MainThread, started 140735268892672)>
[<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>]
主线程/主进程
Thread-1
'''
#join方法
from threading import Thread#引入线程模块
import time#引入时间模块
def sayhi(name):#另一一个函数
time.sleep(2)#睡2秒
print('%s say hello' %name)#打印内容 if __name__ == '__main__':#如果用户名等于当前用户名
t=Thread(target=sayhi,args=('egon',))#创建一个线程
t.start()#开启线程
t.join()#守护主线程,等待子进程执行完毕
print('主线程')
print(t.is_alive())#打印返回线程是否活动的
'''
egon say hello
主线程
False
'''

守护线程

无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行

#1.对主进程来说,运行完毕指的是主进程代码运行完毕
#2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
#详细解释
#1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
#2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
#守护线程例子1
from threading import Thread#引入线程模块
import time#引入时间模块
def sayhi(name):#定义一个函数
time.sleep(2)#睡2秒钟
print('%s say hello' %name)#打印内容 if __name__ == '__main__':#如果文件名等于当前文件名
t=Thread(target=sayhi,args=('egon',))#创建一个线程
t.setDaemon(True) #必须在t.start()之前设置开启守护线程
t.start()#开启线程 print('主线程')
print(t.is_alive())#返回线程是否还活着
'''
主线程
True
'''
锁与GIL

同步锁

#多个线程抢占资源的情况
from threading import Thread#引入线程模块
import os,time#引入系统模块和时间模块
def work():#定义一个函数
global n#定义一个全局变量
temp=n#得到一个中间量
time.sleep(0.1)#睡0.1秒
n=temp-1#得到一个n的结果
if __name__ == '__main__':#如果用户名等于当前用户名
n=100#定义一个变量
l=[]#得到一个空列表
for i in range(100):#循环100次
p=Thread(target=work)#创建一个线程
l.append(p)#添加到列表里
p.start()#开启这个线程
for p in l:#循环这个列表
p.join()#等待列表运行完毕 print(n) #结果可能为99
#import threading
#R=threading.Lock()
#R.acquire()
'''
对公共数据的操作
'''
#R.release()
#同步锁的引用
from threading import Thread,Lock#引入线程模块和锁模块
import os,time#引入系统模块和时间模块
def work():#定义一个函数
global n#定义一个全局变量
lock.acquire()#加一把锁
temp=n#得到一个变量
time.sleep(0.1)#睡0.1秒
n=temp-1#得到一个变量
lock.release()#还钥匙
if __name__ == '__main__':#如果用户名等于当前用户名
lock=Lock()#实例化一个锁变量
n=100#得到一个变量
l=[]#得到一个空列表
for i in range(100):#循环100次
p=Thread(target=work)#实例化一个线程
l.append(p)#添加到列表里
p.start()#开始这个线程
for p in l:#循环这个列表
p.join()#等待结束 print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全
#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock#引入
import os,time
def task():
global n
print('%s is running' %current_thread().getName())
temp=n
time.sleep(0.5)
n=temp-1 if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join() stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n)) '''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
''' #不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
#未加锁的代码并发运行
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
#加锁的代码串行运行
lock.acquire()
temp=n
time.sleep(0.5)
n=temp-1
lock.release() if __name__ == '__main__':
n=100
lock=Lock()
threads=[]
start_time=time.time()
for i in range(100):
t=Thread(target=task)
threads.append(t)
t.start()
for t in threads:
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n)) '''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
''' #有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
time.sleep(3)
print('%s start to run' %current_thread().getName())
global n
temp=n
time.sleep(0.5)
n=temp-1 if __name__ == '__main__':
n=100
lock=Lock()
start_time=time.time()
for i in range(100):
t=Thread(target=task)
t.start()
t.join()
stop_time=time.time()
print('主:%s n:%s' %(stop_time-start_time,n)) '''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖

斥锁与join的区别

死锁与递归锁

进程也有死锁与递归锁,在进程那里忘记说了,放到这里一切说了额

所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁

#死锁
from threading import Lock as Lock#引入锁模块
import time#引入时间模块
mutexA=Lock()#实例化一个锁
mutexA.acquire()#拿一个锁
mutexA.acquire()#拿一个锁
print(123)#打印
mutexA.release()#还钥匙
mutexA.release()#还钥匙

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

#递归锁RLock
from threading import RLock as Lock#引入递归锁
import time#引入时间模块
mutexA=Lock()#创建一个锁对象
mutexA.acquire()#拿一把锁
mutexA.acquire()#拿一把锁
print(123)#打印
mutexA.release()#还钥匙
mutexA.release()#还钥匙 

典型问题:科学家吃面

#著名的科学家吃面问题
import time#引入时间模块
from threading import Thread,Lock#引入线程和锁模块
noodle_lock = Lock()#实例化一个锁
fork_lock = Lock()#创建一个锁对象
def eat1(name):#定义一个函数
noodle_lock.acquire()#领取一把锁
print('%s 抢到了面条'%name)#打印内容
fork_lock.acquire()#拿到一把锁
print('%s 抢到了叉子'%name)#打印内容
print('%s 吃面'%name)#打印内容
fork_lock.release()#还钥匙
noodle_lock.release()#还钥匙 def eat2(name):#定义一个函数
fork_lock.acquire()#拿到一把锁
print('%s 抢到了叉子' % name)#打印内容
time.sleep(1)#睡1秒
noodle_lock.acquire()#拿到一把锁
print('%s 抢到了面条' % name)#打印内容
print('%s 吃面' % name)#打印内容
noodle_lock.release()#归还锁
fork_lock.release()#归还锁 for name in ['哪吒','egon','yuan']:#循环列表
t1 = Thread(target=eat1,args=(name,))#创建一个线程
t2 = Thread(target=eat2,args=(name,))#创建一个线程
t1.start()#开启这个线程
t2.start()#开启这个线程
#递归锁解决死锁的问题
import time#引入时间模块
from threading import Thread #引入线程和递归锁
from threading import RLock # 递归锁
fork_lock = noodle_lock = RLock() # 一个钥匙串上的两把钥匙
def eat1(name):#定义一个函数
noodle_lock.acquire() # 一把钥匙
print('%s拿到面条啦'%name)#打印
fork_lock.acquire()#拿到钥匙
print('%s拿到叉子了'%name)#打印
print('%s吃面'%name)#打印
fork_lock.release()#还钥匙
noodle_lock.release()#还钥匙 def eat2(name):#定义一个函数
fork_lock.acquire()#拿到一把钥匙
print('%s拿到叉子了'%name)#打印
time.sleep(1)#睡1秒
noodle_lock.acquire()#拿到钥匙
print('%s拿到面条啦'%name)#打印
print('%s吃面'%name)#打印
noodle_lock.release()#还锁
fork_lock.release()#还锁 Thread(target=eat1,args=('alex',)).start()#开始一个线程
Thread(target=eat2,args=('Egon',)).start()#开始一个线程
Thread(target=eat1,args=('bossjin',)).start()#开始一个线程
Thread(target=eat2,args=('nezha',)).start()#开始一个

信号量

同进程的一样

Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

#实例
from threading import Thread,Semaphore#引入线程模块和信号量模块
import threading#引入线程模块
import time#引入时间模块
# def func():#定义一个函数
# if sm.acquire():#如果有锁
# print (threading.currentThread().getName() + ' get semaphore')#打印内容
# time.sleep(2)#睡2秒
# sm.release()#还钥匙
def func():#定义一个函数
sm.acquire()#拿一把锁
print('%s get sm' %threading.current_thread().getName())#打印内容
time.sleep(3)#睡3秒
sm.release()#还钥匙
if __name__ == '__main__':#如果名字是当前用户名
sm=Semaphore(5)#创建5把锁
for i in range(23):#循环23次
t=Thread(target=func)#创建一个线程
t.start()#开始
#池与信号量
#与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

事件

同进程的一样

线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

#event.isSet():返回event的状态值;
#event.wait():如果 event.isSet()==False将阻塞线程;
#event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
#event.clear():恢复event的状态值为False。

例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作

#实例
import threading#引入线程模块
import time,random#引入时间模块和随机数模块
from threading import Thread,Event#引入线程和时间模块 def conn_mysql():#定义一个函数
count=1#创建一个变量
while not event.is_set():#循环 为真
if count > 3:#如果大于3
raise TimeoutError('链接超时')#增减一个时间超时错误
print('<%s>第%s次尝试链接' % (threading.current_thread().getName(), count))#打印内容
event.wait(0.5)#等待时间状态
count+=1#得到一个值
print('<%s>链接成功' %threading.current_thread().getName())#打印链接成功 def check_mysql():#定义一个检查函数
print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())#打印内容
time.sleep(random.randint(2,4))#随机睡2~4秒
event.set()#设置事件状态为True
if __name__ == '__main__':#如果用户名等于当前用户名
event=Event()#创建一个对象
conn1=Thread(target=conn_mysql)#创建子一个线程
conn2=Thread(target=conn_mysql)#创建子一个线程
check=Thread(target=check_mysql)#创建一个子线程 conn1.start()#开始一个子线程
conn2.start()#开始一个子线程
check.start()#开始一个子线程

条件

使得线程等待,只有满足某条件时,才释放n个线程

#详细说明
#Python提供的Condition对象提供了对复杂线程同步问题的支持。Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还提供了wait和notify方法。线程首先acquire一个条件变量,然后判断一些条件。如果条件不满足则wait;如果条件满足,进行一些处理改变条件后,通过notify方法通知其他线程,其他处于wait状态的线程接到通知后会重新判断条件。不断的重复这一过程,从而解决复杂的同步问题。

代码说明:

#实例
import threading#引入线程模块 def run(n):#定义一个函数
con.acquire()#拿到钥匙
con.wait()#等待
print("run the thread: %s" % n)#打印内容
con.release()#还钥匙 if __name__ == '__main__':#如果用户名等于当前用户名 con = threading.Condition()#创建一个条件变量对象
for i in range(10):#循环10次
t = threading.Thread(target=run, args=(i,))#创建一个线程
t.start()#开始这个线程 while True:#循环为真
inp = input('>>>')#输入你的内容
if inp == 'q':#如果输入为q
break#则打断
con.acquire()#拿到钥匙
con.notify(int(inp))#通知状态
con.release()#还钥匙
print('****')

定时器

定时器,指定n秒后执行某个操作

from threading import Timer                          #引入一个时间模块

def hello():                                         #定义一个函数
print("hello, world") #打印你好,世界 t = Timer(1, hello) #创建一个时间对象,延时1秒执行
t.start() # after 1 seconds, "hello, world" will be printed #开始这个进程
线程队列

queue队列 :使用import queue,用法与进程Queue一样

queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.

class queue.Queue(maxsize=0) #先进先出
#先进先出
import queue#引入队列模块 q=queue.Queue()#创建一个对象
q.put('first')#放到队列里一个参数
q.put('second')#放到队列里一个参数
q.put('third')#放到队列里一个参数 print(q.get())#取一个参数
print(q.get())#取一个参数
print(q.get())#取一个参数
'''
结果(先进先出):
first
second
third
'''

class queue.LifoQueue(maxsize=0) #last in fisrt out

#后进先出
import queue#引入队列模块 q=queue.LifoQueue()#引入先进后出队列
q.put('first')#添加到管道里一个参数
q.put('second')#添加到管道里一个参数
q.put('third')#添加到管道里一个参数 print(q.get())#取出管道里一个参数
print(q.get())取出管道里一个参数
print(q.get())取出管道里一个参数
'''
结果(后进先出):
third
second
first
'''

class queue.PriorityQueue(maxsize=0) #存储数据时可设置优先级的队列

#优先级队列方法
import queue#引入队列模块 q=queue.PriorityQueue()#创建一个优先级队列对象
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))#放进队列里一个参数
q.put((10,'b'))#放进队列里一个参数
q.put((30,'c'))#放进队列里一个参数 print(q.get())#取出队列里一个参数
print(q.get()) #取出队列里一个参数
print(q.get()) #取出队列里一个参数
'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''
Constructor for a priority queue. maxsize is an integer that sets the upperbound limit on the number of items that can be placed in the queue. Insertion will block once this size has been reached, until queue items are consumed. If maxsize is less than or equal to zero, the queue size is infinite.

The lowest valued entries are retrieved first (the lowest valued entry is the one returned by sorted(list(entries))[0]). A typical pattern for entries is a tuple in the form: (priority_number, data).

exception queue.Empty
Exception raised when non-blocking get() (or get_nowait()) is called on a Queue object which is empty. exception queue.Full
Exception raised when non-blocking put() (or put_nowait()) is called on a Queue object which is full. Queue.qsize()
Queue.empty() #return True if empty
Queue.full() # return True if full
Queue.put(item, block=True, timeout=None)
Put item into the queue. If optional args block is true and timeout is None (the default), block if necessary until a free slot is available. If timeout is a positive number, it blocks at most timeout seconds and raises the Full exception if no free slot was available within that time. Otherwise (block is false), put an item on the queue if a free slot is immediately available, else raise the Full exception (timeout is ignored in that case). Queue.put_nowait(item)
Equivalent to put(item, False). Queue.get(block=True, timeout=None)
Remove and return an item from the queue. If optional args block is true and timeout is None (the default), block if necessary until an item is available. If timeout is a positive number, it blocks at most timeout seconds and raises the Empty exception if no item was available within that time. Otherwise (block is false), return an item if one is immediately available, else raise the Empty exception (timeout is ignored in that case). Queue.get_nowait()
Equivalent to get(False). Two methods are offered to support tracking whether enqueued tasks have been fully processed by daemon consumer threads. Queue.task_done()
Indicate that a formerly enqueued task is complete. Used by queue consumer threads. For each get() used to fetch a task, a subsequent call to task_done() tells the queue that the processing on the task is complete. If a join() is currently blocking, it will resume when all items have been processed (meaning that a task_done() call was received for every item that had been put() into the queue). Raises a ValueError if called more times than there were items placed in the queue. Queue.join() block直到queue被消费完毕

更多方法

Python标准模块--concurrent.futures

https://docs.python.org/dev/library/concurrent.futures.html

#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)
回调函数
#介绍
The ProcessPoolExecutor class is an Executor subclass that uses a pool of processes to execute calls asynchronously. ProcessPoolExecutor uses the multiprocessing module, which allows it to side-step the Global Interpreter Lock but also means that only picklable objects can be executed and returned. class concurrent.futures.ProcessPoolExecutor(max_workers=None, mp_context=None)
An Executor subclass that executes calls asynchronously using a pool of at most max_workers processes. If max_workers is None or not given, it will default to the number of processors on the machine. If max_workers is lower or equal to 0, then a ValueError will be raised. #用法
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import os,time,random
def task(n):
print('%s is runing' %os.getpid())
time.sleep(random.randint(1,3))
return n**2 if __name__ == '__main__': executor=ProcessPoolExecutor(max_workers=3) futures=[]
for i in range(11):
future=executor.submit(task,i)
futures.append(future)
executor.shutdown(True)
print('+++>')
for future in futures:
print(future.result())

ProcessPoolExecutor

#介绍
ThreadPoolExecutor is an Executor subclass that uses a pool of threads to execute calls asynchronously.
class concurrent.futures.ThreadPoolExecutor(max_workers=None, thread_name_prefix='')
An Executor subclass that uses a pool of at most max_workers threads to execute calls asynchronously. Changed in version 3.5: If max_workers is None or not given, it will default to the number of processors on the machine, multiplied by 5, assuming that ThreadPoolExecutor is often used to overlap I/O instead of CPU work and the number of workers should be higher than the number of workers for ProcessPoolExecutor. New in version 3.6: The thread_name_prefix argument was added to allow users to control the threading.Thread names for worker threads created by the pool for easier debugging. #用法
与ProcessPoolExecutor相同

ThreadPoolExecutor

#回调函数
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor#引入线程池异步调用模块和进程池异步调用模块
from multiprocessing import Pool#引入进程池模块
import requests#引入申请模块
import json#引入序列化模块
import os#引入操作系统模块 def get_page(url):#定义一个函数
print('<进程%s> get %s' %(os.getpid(),url))#打印内容
respone=requests.get(url)#创建一个申请模块获取方法的对象
if respone.status_code == 200:#
return {'url':url,'text':respone.text}#返回信息 def parse_page(res):#定义一个函数
res=res.result()#
print('<进程%s> parse %s' %(os.getpid(),res['url']))#打印内容
parse_res='url:<%s> size:[%s]\n' %(res['url'],len(res['text']))
with open('db.txt','a') as f:#爬取内容
f.write(parse_res)#写入文件 if __name__ == '__main__':#如果用户名为当前用户名
urls=[
'https://www.baidu.com',
'https://www.python.org',
'https://www.openstack.org',
'https://help.github.com/',
'http://www.sina.com.cn/'
] # p=Pool(3)#实例化一个进程池
# for url in urls:
# p.apply_async(get_page,args=(url,),callback=pasrse_page)#异步开启
# p.close()关闭对象
# p.join()#等待结束 p=ProcessPoolExecutor(3)#实例化一个进程池 for url in urls:
p.submit(get_page,url).add_done_callback(parse_page) #parse_page拿到的是一个future对象obj,需要用obj.result()拿到结果
#map的用法
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor#引入一个线程池和进程池 import os,time,random#引入系统模块,时间模块、随机数模块
def task(n):#定义一个函数
print('%s is runing' %os.getpid())#打印内容
time.sleep(random.randint(1,3))#随机睡1~3秒
return n**2#返回一个结果 if __name__ == '__main__':#如果用户名等于当前用户名 executor=ThreadPoolExecutor(max_workers=3)#实例化一个线程池 # for i in range(11):#循环11个数
# future=executor.submit(task,i)##异步提交任务 executor.map(task,range(1,12)) #map取代了for+submit
 
 

线程 day40的更多相关文章

  1. day40 线程

    目录 一.开启线程的两种方式 二.用进程和线程分别实现tcp 三.线程对象实现join方法 四.同一个进程下的多个线程数据是共享的 五.线程对象属性及其他方法 六.守护线程 七.线程互斥锁 八.GIL ...

  2. [ 高并发]Java高并发编程系列第二篇--线程同步

    高并发,听起来高大上的一个词汇,在身处于互联网潮的社会大趋势下,高并发赋予了更多的传奇色彩.首先,我们可以看到很多招聘中,会提到有高并发项目者优先.高并发,意味着,你的前雇主,有很大的业务层面的需求, ...

  3. [高并发]Java高并发编程系列开山篇--线程实现

    Java是最早开始有并发的语言之一,再过去传统多任务的模式下,人们发现很难解决一些更为复杂的问题,这个时候我们就有了并发. 引用 多线程比多任务更加有挑战.多线程是在同一个程序内部并行执行,因此会对相 ...

  4. 多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类)

    前言:刚学习了一段机器学习,最近需要重构一个java项目,又赶过来看java.大多是线程代码,没办法,那时候总觉得多线程是个很难的部分很少用到,所以一直没下决定去啃,那些年留下的坑,总是得自己跳进去填 ...

  5. Java 线程

    线程:线程是进程的组成部分,一个进程可以拥有多个线程,而一个线程必须拥有一个父进程.线程可以拥有自己的堆栈,自己的程序计数器和自己的局部变量,但不能拥有系统资源.它与父进程的其他线程共享该进程的所有资 ...

  6. C++实现线程安全的单例模式

    在某些应用环境下面,一个类只允许有一个实例,这就是著名的单例模式.单例模式分为懒汉模式,跟饿汉模式两种. 首先给出饿汉模式的实现 template <class T> class sing ...

  7. 记一次tomcat线程创建异常调优:unable to create new native thread

    测试在进行一次性能测试的时候发现并发300个请求时出现了下面的异常: HTTP Status 500 - Handler processing failed; nested exception is ...

  8. Android线程管理之ThreadLocal理解及应用场景

    前言: 最近在学习总结Android的动画效果,当学到Android属性动画的时候大致看了下源代码,里面的AnimationHandler存取使用了ThreadLocal,激起了我很大的好奇心以及兴趣 ...

  9. C#多线程之线程池篇3

    在上一篇C#多线程之线程池篇2中,我们主要学习了线程池和并行度以及如何实现取消选项的相关知识.在这一篇中,我们主要学习如何使用等待句柄和超时.使用计时器和使用BackgroundWorker组件的相关 ...

随机推荐

  1. python桌面端开发手记(序列化、压缩包、加密、图形界面GUI)

    0x00   前段时间接到一个小项目是给某行业内部开发离线桌面端,业务流实现上总体分信息录入.加密导出.因为是win桌面端,所以老板说依托Access用VBA做,我据理力争了一下.之前就是用Acces ...

  2. 最邻近算法(KNN)识别数字验证码

    应用场景   对于简单的数字型验证码的自动识别.前期已经完成的工作是通过切割将验证码图片切割成一个一个的单个数字的图片,并按照对应的数字表征类别进行分类(即哪些图片表示数字7,哪些表示8),将各种数字 ...

  3. 浏览器唤起APP的思路(本文转载)

    在做 h5 页面中,会遇到这样一个需求,有一个立即打开的按钮,如果本地安装了我们的 app,那么点击就直接唤起本地 app,如果没有安装,则跳转到下载. 首先想到的是两个问题:一是如何唤起本地 app ...

  4. Scrapyd 的远程部署和监控

    1. 安装Scrapyd sudo pip3.6 install scrapyd # 安装scrapyd服务 sudo pip3.6 install scrapyd-client # 安装scrapy ...

  5. trie数的实现

    Trie树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种.典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计.它的优点是:利用字符串 ...

  6. js高级-函数变量提升

    var a = 10; function f1(){ console.log(a) //undefined  函数变量提升了 函数执行之前想创建了函数的EC 把函数里面声明的变量初始化undefine ...

  7. 项目IDEA启动配置

    在所有java启动项中加入 -Djute.maxbuffer=2048000 tomcat 在catalina.bat 中第一行加入 set JAVA_OPTS=-Djute.maxbuffer=20 ...

  8. 常用官方php版本下载链接

    windows 版本 https://windows.php.net/downloads/releases/archives/ https://windows.php.net/download/#ph ...

  9. 跨时代的分布式数据库 – 阿里云DRDS详解(转)

    原文章地址:https://www.csdn.net/article/a/2015-08-28/15827676 跨时代的分布式数据库 – 阿里云DRDS详解 发表于2015-08-28 18:39| ...

  10. vue bus 的使用

    简单的状态管理,可以用vue bus vue bus可以实现不同组件间.不同页面间的通信,比如我在A页面出发点击事件,要B页面发生变化,使用方法如下: 全局定义:main.js window.even ...