线程,进程,协程
进程与线程
什么的进程(process):
程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。
程序和进程的区别在于:程序的指令的集合,它是进程运行的静态描述文本,进程是程序的一次执行活动,属于动态概念。
在多道编程中我们允许多个程序同时加载到内存中,在操作系统的调度下可以实现并发执行。就是这样的设计,大大的提高的CPU的利用率,进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在cup上实现多道编程而提出的。
有了进程为什么还要线程?
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率,,其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在以下两点上:
1.进程只能在一个时间干一件事,不能同时干两件事或多件事。
2.进程在执行的过程中如果有阻塞,例如:等待输入,整个进程就会挂起,即使进程中有些工作不依赖与输入的数据,也无法执行。
例如:我们在使用QQ聊天,QQ做为一个独立进程,如果同一时间只能干一件事,那它是如何实现在同一时刻,即能监听键盘输入,又能监听其他人给你发的消息,同时还能把别人发的消息显示在屏幕上那?这个时候你可能会说,操作系统不是有分时吗?但是,分时是指在不同的进程间的分时,即操作系统处理一会你的QQ任务,又切换到word文档任务上去了,每个CPU时间片分给你的QQ程序时,你的QQ还是只能同时干一件事呀。
在自白一点,一个操作系统就像是一个工厂,工厂里面有很多生成车间,不同的车间生成不同的产品,每个车间就相当于一个进程,而且你的工厂很穷,供电不足,同一时间只能给一个车间供电,为了能让所有车间都能同时生产,你的工厂电工只能给不同的车间分时供电,但是论到你的QQ车间时,发现只有一个干活的工人,结果生产效率极低,为了解决这个问题,应该怎么办呢?。。。没错,就是你想到的,就是多加几个工人,让几个工人同时工作,这每个工人就是线程。
什么是线程(thread)
线程是操作系统能够运行调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程可以并发多个线程,每条线程并行执行不同的任务。
线程与进程的区别
- Threads share the address space of the process that created it; processes have their own address space.
- Threads have direct 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; processes must use interprocess communication to communicate with sibling processes.
- New threads are easily created; new processes require duplication of the parent process.
- Threads can exercise considerable control over threads of the same process; processes 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.
1 一个程序至少有一个进程,一个进程至少有一个线程.(进程可以理解成线程的容器) 2 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。 3 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和 程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 4 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调 度的一个独立单位. 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程 自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)但是 它可与同属一个进程的其他的线程共享进程所拥有的全部资源. 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
python的GIL
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.)
上面的核心意思就是,无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行
Python的线程与threading模块
一.线程的两种调用方式
threading模块建立在thread模块之上,tread模块以低级,原始的方式来处理和控制线程,而threading模块通过对thread进行二次封装,提供了更方便的api来处理线程。
直接调用
1 import threading, time 2 3 def sayhi(num): # 定义每个线程要运行的函数 4 print("running on number:%s" %num) 5 time.sleep(3) 6 7 if __name__ == "__main__": 8 t1 = threading.Thread(target=sayhi,args=(1,)) # 生成一个线程实例 9 t2 = threading.Thread(target=sayhi,args=(2,)) # 生成一个线程实例 10 11 t1.start() # 启动线程 12 t2.start() 13 14 print(t1.getName()) # 获取线程名 15 print(t2.getName()) 16 17 #Thread-1 18 #Thread-2
继承式调用:
import threading, time class MyTread(threading.Thread): def __init__(self, num): threading.Thread.__init__(self) self.num = num def run(self): print("running on number: %s" %self.num) time.sleep(3) if __name__ == "__main__": t1 = MyTread(1) t2 = MyTread(2) t1.start() t2.start() print("ending......")
join&Daemon方法
import time import threading def run(n): print('[%s]------running----\n' % n) time.sleep(2) print('--done--') def main(): for i in range(5): t = threading.Thread(target=run, args=[i, ]) t.start() t.join(1) print('starting thread', t.getName()) m = threading.Thread(target=main, args=[]) m.setDaemon(True) # 将main线程设置为Daemon线程,它做为程序主线程的守护线程,当主线程退出时,m线程也会退出,由m启动的其它子线程会同时退出,不管是否执行完任务 m.start() m.join(timeout=2) print("---main thread done----")
join():子线程完成以后,才执行子线程的父线程 setDaemon(True): 守护进程,主线程结束以后,子线程不管有没有结束,都会跟随主线程一起结束。 必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。 run(): 线程被cpu调度后自动执行线程对象的run方法 start():启动线程活动。 isAlive(): 返回线程是否活动的。 getName(): 返回线程名。 setName(): 设置线程名。 threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
线程锁(互斥锁,同步锁)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据,此时,如果两个线程同时要修改同一份数据,会出现什么样的状况?
1 import time, threading 2 3 def add(): 4 global num # 在每个线程都获取这个全局变量 5 # print("---get num", num) 6 temp = num 7 time.sleep(0.1) 8 num = temp -1 # 对此公共变量进行-1操作 9 print("---ening num", num) 10 num = 100 # 设定一个共享变量 11 thread_list = [] 12 for i in range(100): # 生成100个线程 13 t = threading.Thread(target=add) 14 t.start() 15 thread_list.append(t) 16 17 for i in thread_list: # 等待所有线程执行完毕 18 i.join() 19 20 21 print("finam num ", num)
正常来讲,这个 num结果应该是0,但是你多运行几次会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢?其实,很简单,假设你有A,B两个线程,此时都要对num进行减1的操作,由于两个线程是并发同时运行的,所以两个线程很有可能同时拿走了num=100这个初始变量,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。
1 import time, threading 2 3 def add(): 4 global num # 在每个线程都获取这个全局变量 5 # print("---get num", num) 6 lock.acquire() 7 temp = num 8 9 time.sleep(0.1) 10 num = temp -1 # 对此公共变量进行-1操作 11 12 lock.release() 13 print("---ening num", num) 14 15 16 17 num = 100 # 设定一个共享变量 18 thread_list = [] 19 lock = threading.Lock() 20 for i in range(100): # 生成100个线程 21 t = threading.Thread(target=add) 22 t.start() 23 thread_list.append(t) 24 25 for i in thread_list: # 等待所有线程执行完毕 26 i.join() 27 28 29 print("finam num ", num)
GIL VS Lock
python中有一个GIL来保证同一个时间只能有一个线程来执行,为什么这里还需要lock?注意啦,这里的lock跟GIL没有关系,具体的可以通过下图来看一下
在这里可能大家有疑问,既然用户程序已经自己有锁了,那为什么python还需要GIL呢?
加入GIL主要都原因是降低开发的复杂度,比如你现在写python不用关心内存回收的问题,因为python解释器帮你自动定期进行内存的回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看那些内存数据是可以被清空的,此时你自己的程序里的线程和python解释器自己的线程是并发运行的,假设你的线程删除一个变量,python解释器的垃圾回收线程在清空这个变量的过程中,可能一个其他的线程正好又重新给这个还没有来得及清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题, python解释器就加了锁,即当一个线程运行时,其他人都不要动,这样就解决了上述问题。
线程死锁和递归锁
在线程中共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,
因为系统判断这部分资源中使用中,所以这两个线程中无外力作用下将一直等下去,
死锁示例如下:
1 import threading,time 2 3 class myThread(threading.Thread): 4 def doA(self): 5 lockA.acquire() 6 print(self.name,"gotlockA",time.ctime()) 7 time.sleep(3) 8 lockB.acquire() 9 print(self.name,"gotlockB",time.ctime()) 10 lockB.release() 11 lockA.release() 12 13 def doB(self): 14 lockB.acquire() 15 print(self.name,"gotlockB",time.ctime()) 16 time.sleep(2) 17 lockA.acquire() 18 print(self.name,"gotlockA",time.ctime()) 19 lockA.release() 20 lockB.release() 21 22 def run(self): 23 self.doA() 24 self.doB() 25 if __name__=="__main__": 26 27 lockA=threading.Lock() 28 lockB=threading.Lock() 29 threads=[] 30 for i in range(5): 31 threads.append(myThread()) 32 for t in threads: 33 t.start() 34 for t in threads: 35 t.join()#等待线程结束,后面再讲。
根据上述例子我们发现,我们同时创建了5个线程,这5个线程需要同时执行 a函数, a函数执行完成后,在执行b函数,在这5个线程同时竞争的过程好,线程1胜出,线程1开始执行a函数,并同时给自己加了一把锁lockA,让其他的线程暂停,然后在给自己在加了一把锁lockB,--- 释放lockB,---释放lock A,这个时候线程1已经把a函数执行完了,开始执行b函数,同时其他4个线程开始抢占 a函数,这个时候线程2,抢占到了a函数,并进行锁定(过程于线程1相同),在这个同时线程1也抢占到 了b函数,这个时候两个线程都被锁住了,所以会出现线程一直等待的状态。
解决方法:使用递归锁
为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock,RLock内部维护着一个Lock和一个 counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire,直到这个线程所有的acquire都被release,其他线程才能获得资源。
应用(存取款)
1 import time 2 import threading 3 4 5 class Account: 6 def __init__(self, _id, balance): 7 self.id = _id 8 self.balance = balance 9 self.lock = threading.RLock() 10 11 def withdraw(self, amount): 12 '''取钱''' 13 with self.lock: 14 self.balance -= amount 15 16 def deposit(self, amount): 17 '''存款''' 18 with self.lock: 19 self.balance += amount 20 21 def drawcash(self, amount): 22 '''''' 23 with self.lock: 24 interest = 0.05 25 count = amount+amount*interest 26 27 self.withdraw(count) 28 29 30 def transfer(_from, to, amount): 31 _from.withdraw(amount) 32 to.deposit(amount) 33 34 alex = Account("alex", 1000) 35 yuan = Account("yuan", 1000) 36 37 t1 = threading.Thread(target=transfer, args=(alex, yuan, 1000)) 38 t1.start() 39 40 t2 = threading.Thread(target=transfer, args=(yuan, alex, 1000)) 41 t2.start() 42 43 t1.join() 44 t2.join() 45 46 print(">>>",alex.balance) 47 print(">>>",yuan.balance)
同步条件(Event)
event = threading.Event()
event.wait(). # 等待
event.set() # 柱塞event.clear(). # 清空1 import threading,time 2 class Boss(threading.Thread): 3 def run(self): 4 print("BOSS:今晚大家都要加班到22:00。") 5 print(event.isSet()) 6 event.set() 7 time.sleep(5) 8 print("BOSS:<22:00>可以下班了。") 9 print(event.isSet()) 10 event.set() 11 class Worker(threading.Thread): 12 def run(self): 13 event.wait() 14 print("Worker:哎……命苦啊!") 15 time.sleep(1) 16 event.clear() 17 event.wait() 18 print("Worker:OhYeah!") 19 if __name__=="__main__": 20 event=threading.Event() 21 threads=[] 22 for i in range(5): 23 threads.append(Worker()) 24 threads.append(Boss()) 25 for t in threads: 26 t.start() 27 for t in threads: 28 t.join()
信号量
信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数 器,每当调用acquire()时-1,调用release()时+1。
计数器不能小于0,当计数器为 0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。
BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数 器的值是否超过了计数器的初始值,如果超过了将抛出一个异常。
1 import threading 2 import time 3 4 5 class MyThread(threading.Thread): 6 7 def run(self): 8 if semaphore.acquire(): 9 print(self.name) 10 time.sleep(5) 11 semaphore.release() 12 13 if __name__ == "__main__": 14 semaphore = threading.Semaphore(5) 15 thrs = [] 16 for i in range(100): 17 thrs.append(MyThread()) 18 19 for t in thrs: 20 t.start()
多线程利器---队列(queue)
1 import threading 2 import time 3 4 5 li = [1,2,3,4,5] 6 7 8 def pri(): 9 while li: 10 a = li[-1] 11 print(a) 12 time.sleep(1) 13 try: 14 li.remove(a) 15 except Exception as e: 16 print("-----",a, e) 17 18 t1 = threading.Thread(target=pri,args=()) 19 t1.start() 20 21 t2 = threading.Thread(target=pri, args=()) 22 t2.start()
queue队列类的方法
1 创建一个队列对象 2 import queue 3 q = queue.Queue(maxsize=10) 4 queue.Queue类即是一个队列的同步实现,队列的长度可为无限或者有限,可通过Queue的构造函数的可选参数 maxsize来设定队列的长度,如果maxsize小于1就表示长度无限。 5 6 将一个值放入队列中 7 q.put(10) 8 调用队列的put()方法中队尾插入一个项目,put()有两个参数,第一个time为必须的,为插入项目的值,第二个block为可选参数,默认为1,如果队列当前为空且block为1,put()方法将使调用线程暂停,直到空出一个 数据单元,如果 block为0,put方法将引发Full异常。 9 10 11 将一个值从队列中取出 12 q.get() 13 调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True, 14 get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。 15 16 Python Queue模块有三种队列及构造函数: 17 1、Python Queue模块的FIFO队列先进先出。 class queue.Queue(maxsize) 18 2、LIFO类似于堆,即先进后出。 class queue.LifoQueue(maxsize) 19 3、还有一种是优先级队列级别越低越先出来。 class queue.PriorityQueue(maxsize) 20 21 此包中的常用方法(q = Queue.Queue()): 22 q.qsize() 返回队列的大小 23 q.empty() 如果队列为空,返回True,反之False 24 q.full() 如果队列满了,返回True,反之False 25 q.full 与 maxsize 大小对应 26 q.get([block[, timeout]]) 获取队列,timeout等待时间 27 q.get_nowait() 相当q.get(False) 28 非阻塞 q.put(item) 写入队列,timeout等待时间 29 q.put_nowait(item) 相当q.put(item, False) 30 q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号 31 q.join() 实际上意味着等到队列为空,再执行别的操作