进程线程协程

Posted not_enough

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程线程协程相关的知识,希望对你有一定的参考价值。

 

进程
什么是进程?
程序就是一堆放在磁盘上的代码,进程是一段程序的运行过程
正规点说,进程一般由程序、数据集、进程控制块三部分组成

什么进程切换?
进程切换是,一个正在运行的进程被中断,操作系统指定另一个进程为运行态,并把CPU执行权交给这个进程。由操作系统控制调度,如单线程遇到io或执行时间过长就会被迫交出cpu执行权限
1 )如果没有遇到io操作,操作系统会按照一个固定的时间段去切换进程,几百毫秒(时间轮询切换)。进程每一次切换的时间都是微不足道的,但要命的是其切换次数。
2)遇到io操作,则主动交出cpu使用权。
3)最后是遇到一个更高优先级的进程,被操作系统的调度算法强行换下来

进程并发(切换)如何实现?

进程并发的实现在于,硬件中断一个正在运行的进程,把此时进程运行的所有状态保存下来,为此,操作系统维护一张表格,即进程表(process table),
每个进程占用一个进程表项(这些表项也称为进程控制块)该表存放了进程状态的重要信息:程序计数器、堆栈指针、内存分配状况、所有打开文件的状态、帐号和调度信息,以及其他在进程由运行态转为就绪态或阻塞态时,必须保存的信息,从而保证该进程在再次启动时,就像从未被中断过一样。

扩展知识:进程切换保存的上下文信息存到哪里去?别跟我说内存,太慢了

像进程切换所要保存的数据都会存储到寄存器上。
1.寄存器的产生原因是由于CPU运行速度,远远快于从内存读取数据,程序慢就是慢取这些指令,而不是运行这些指令。
2.寄存器紧贴cpu,如今大概有1k左右大小。设计出来的初衷是读取速度要配合cpu的高速运转速度,即读取速度较快
3.寄存器存储非常关键的数据,并且要求取数据速度快的数据,例如常存储进程切换的状态

进程有哪些状态?
进程的三种状态
运行、阻塞(挂起)、就绪
其中阻塞和就绪的差别就是有没有被操作系统考虑调度切换。阻塞就一直晾在一边,不考虑

什么是并发与并行?
并发:是指系统有处理多个任务(动作)的能力
   只要1核的cpu,不可能存在两个进程同时运行,而不用任何切换机制。
并行,是指系统具有‘同时’处理多个任务的能力
   这是多核cpu特有的,可以并行少于等于其核数的进程

进程相关代码

 

Process([group [, target [, name [, args [, kwargs]]]]])

  group # 线程组,目前还没有实现,库引用中提示必须是None; 
  target#要执行的方法; 
  name #进程名; 
  args/kwargs  # 要传入方法的参数。

实例方法:
  is_alive() #返回进程是否在运行。
  join([timeout]) #阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
  start()#进程准备就绪,等待CPU调度

  run() #strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
  terminate()#不管任务是否完成,立即停止工作进程
  p.getpid()#当前进程id
  p.getppid() #父进程id

属性:
  daemon#和线程的setDeamon功能一样,不过变了属性,设置守护进程的属性
  name#进程名字。
  pid  #进程号

 

 

 

进程锁
进程争同一个资源时,若要求要有先后顺序,则锁住相应的资源相关代码
例如:锁住屏幕,则让print上锁

from multiprocessing import Process,Lock

def prt(i,lock):
    lock.acquire()
    print(\'love is \',i)
    lock.release()

if __name__ ==\'__main__\':
    l=Lock()
    for i in range(1,11):
        p = Process(target=prt,args=(i,l))
        p.start()


进程池
需求:比如有100块砖,自己一个人搬,需要100趟。当然也可以开100线程!,但是无论开100个线程还是进程,开销都很大。进程更是,相当于开100个内存独立空间
所以最好的办法是一般花点雇几个人帮你搬,并且搬完一趟之后继续搬,直至100块砖搬完

from  multiprocessing import Process,Pool
import time,os

def Foo(i):
    time.sleep(1)
    print(i)
    return i+100

def Bar(arg):

    print(os.getpid())
    print(os.getppid())
    print(\'logger:\',arg)

pool = Pool(5) #如果不传参,默认为电脑核数
Bar(1)
for i in range(10):
    #pool.apply(func=Foo, args=(i,))  #同步
    #pool.apply_async(func=Foo, args=(i,)) #异步
    pool.apply_async(func=Foo, args=(i,),callback=Bar) #回调函数,要有且仅有一个参数。进程池有个回调函数参数,回调函数是子进程执行完一次任务后,由主进程执行的函数。

pool.close()
pool.join() #注意一下,这里格式是死的,只要用进程词,close()后再join()一下,两者去其一,要么报错要么不执行
print(\'end\')

进程通信
1)进程队列
2)管道(类似于socket)
3)Manager(数据集共享)
Queue和Pipe只是实现了数据交互,却没有实现数据共享,即一个进程可以修改另一进程的数据

from multiprocess import Queue,Pipe,Manager
进程队列
q = multiprocess.Queue()

管道,类似于socket的用法
son_pipe,parent_pipe = Pipe()
son_pipe.send([\'love\'])
data=parent_pip.recv()
son_pipe.close()

Manager
with Manager() as manager
    d=manager.dict()
    l=manager.list(range(6))
#使用时注意:是将d,l等对象当做进程函数参数传过去

 




 

线程
线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。
一个程序运行起来,最少有一个进程。一个进程里面至少一个线程。
进程相当于为线程提供资源的容器,有了线程概念以后,真正切换和跑的是线程,线程无法脱离进程而存在,因为线程要共享进程里面的内存。

线程和进程的区别?
1)每个进程都有自己独立的内存空间。例如QQ的内存空间和360的内存空间没有任何关系。
2)一个进程中的线程可以共享该进程中的所有资源

什么是多线程?多线程对比多进程有什么好处?

由于多线程的运行与多进程的运行类似,是cpu在多个线程之间的快速切换。
进一步说,python的线程属于内核级别的(虽说是这样,但是由于GIL存在,表现得跟用户级线程差不多)
1)多线程共享一个进程的地址空间,数据共享更加容易,并且当处理的操作基本一样时,可以节省内存
  如果一个数据集在磁盘上,使用多进程读入,则会出现多个相同的内存,如果采用多线程,则是多个线程共享一份读入的数据集内存
2)线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,
  创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
3)线程切换的消耗更小
  多线程也是要做切换的,不过由于其单位更小,挂起恢复不大。
  而进程牵扯到独立的内存空间等其他,挂起恢复消耗的资源大

GIL
1)GIL的存在使得python不能利用多核cpu,因为GIL锁cpython解释器上。
2)在解释器层面,解释器线程锁(GIL)保证了内存管理的线程安全,例如保证解释器垃圾线程回收时不冲突
3)相比java的jvm,JVM 没有 GIL ,但是也有锁,只是锁的粒度细小得多,一般不会整个线程给你锁上

进程与线程有什么区别?

进程是最小的资源单位,操作系统分资源只能分到进程
进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位

线程最小执行单位,CPU调度分派的基本单位。(这个调度可以理解为,一段时间校长代表学校,一段时间由副校长代表学校)
线程自己基本不拥有系统资源,严格来说,只拥有一点运行起来必不可少的资源(程序计数器,栈,寄存器)
程序计数器是类似于看书的页码功能,也是一个寄存器,保存一个地址

守护进程和守护线程是什么?
其实两种概念差不多,旨在同时退出
守护线程:子线程会守护着主线程。即主线程和守护线程同时退出
若主线程退出,守护线程也退出,即使还没运行完。
若守护线程执行完了,守护线程也不退,直到主线程也执行完一同退出。

守护进程:主进程代码运行结束,守护进程随即终止,不过守护进程内无法再开启子进程,否则抛出异常(未验证)
注意:操作系统层面的守护进程是一直在后台运行的进程

线程相关代码

t.join()等用于waitforsingleobject,在子线程执行完之前,父线程会一直阻塞,注意只是父线程。
t.setDaemeon(True) 设为守护线程,在t.start()之前调用
t.start(),因为线程一被创建默认被挂起,这个是用来启动的(其实也没有真正跑起来,只是让线程处于\'就绪\'状态)
t.isAlive() 判断线程是否还在运行。

t.getname、t.setname,设置线程名字,如果不设的话有默认的名字
threading.active_count() 此时此刻到底有几个线程在活动。 
threading.current_thread() 自己所在线程的名字

#常用:主线程等待子线程结束
#等待所有线程运行完毕
def wait_allcomplete(self):
    for item in self.threads:
        if item.isAlive():
            item.join()

 

通过继承的方式运行线程:
#总结来说:继承threading.Thread ,覆写run(self),创建对象后t.start()
import threading
import time

class MyThread(threading.Thread):
    def __init__(self,num):
        threading.Thread.__init__(self)  # 等同于 super().__init__()
        self.num = num

    def run(self):#定义每个线程要运行的函数
        print("running on number:%s" %self.num)
        time.sleep(3)

if __name__ == \'__main__\':

    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()
    
    print("ending......")

 

线程安全

threadlocal
  threading对象里面的不同属性用于被不同线程读写(相当于美化版的dict,不用dict[thread_id]那么狼狈), 且互相不干扰。保证各个子线程的数据安全。
例如:Threading.local最大的用处就是HTTP请求时绑定用户的信息,这样每个用户线程可以非常方便访问各自的资源而互不干扰

import threading

# 创建全局ThreadLocal对象:
local_school = threading.local()

def process_student():
    print \'Hello, %s (in %s)\' % (local_school.student, threading.current_thread().name)

def process_thread(name):
    # 绑定ThreadLocal的student:
    local_school.student = name
    process_student()

t1 = threading.Thread(target= process_thread, args=(\'Alice\',), name=\'Thread-A\')
t2 = threading.Thread(target= process_thread, args=(\'Bob\',), name=\'Thread-B\')
t1.start()
t2.start()
t1.join()
t2.join()

 




同步锁(互斥锁)

数据安全问题是线程切换引起的。
那么只要告诉操作系统,进入某代码块时跟操作系统说好了让cpu不能切换即可。即加锁,加锁的代码块会变为串行,同一时间只有你在运行,其他运行到这部分会阻塞。

例如:我们执行三步过程中,跟操作系统说好了让cpu不能切换。

lock.acquie()
temp=num #这一部分代码变串行了
time.sleep(0.1)
temp=num-1
lock.release()

递归锁
可以有效解决死锁,死锁是线程间谁都不愿意释放自己的资源(锁),导致整体停滞不前。
常见于不同名锁的递归操作。
在锁里面在上一次相同的锁,因为锁内部维护着一个计数器。只有内部计数器为0时才能被线程请求
直到一个线程所有的acquire(计数+1)都被release(计数-1),其他的线程才能获得资源。

a=100
b = 0
lock =  threading.local()
lock.acquire()
lock.acquire()
a-=1
b+=2
lock.release()
lock.release()


同步条件
详细解析
同步条件适用于让不同种类的线程按次序执行。
例如:老板类线程先执行到一半之后,工人类线程才解开阻塞。

event =threading.Event()  #一个event对象可以用到多个线程里面去
event.wait()  #如果没有set设定的条件,该函数会一直阻塞,有的话无视该函数
event.set()  #设置同步条件
event.clear()  #清除同步条件
event.isSet() #检查标志是否有设定

信号量
信号量用来控制线程并发数的,即控制处理同一个被信号量锁住的代码块的线程进入数目。
需要注意的是:进入的线程处理代码块是异步的。要考虑线程安全。

num = 100
def a():
    if semaphore.acquire():  ###
        global num
        num1=num
        time.sleep(0.1)
        num=num1-1
        semaphore.release()####
li =[]
if __name__ == \'__main__\':

    semaphore=threading.Semaphore(5) ### 最多只能由五个线程同时处理处理
    for i in range(100):
        t = threading.Thread(target=a)
        t.start()
        li.append(t)
    for i in li:
        i.join()
    print(num)

队列
队列是线程安全的数据结构,操作它不需要加锁

import queue
q = queue.Queue() #可以传参,表示队列大小
q.put(\'xx\') #若队列满了,该函数会阻塞线程,若block参数为False则直接报错
q.get()  #若队列没数据,则该函数会阻塞线程,若block参数为False则直接报错
q.qsize()
q.empty() #是否为空?
q.full()    #是否满了?
q.put_notwait()  #不阻塞满直接报错
q.get_nowait()    

q.task_done() #在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
q.join() #接收到由q.task_done函数发送的信号(队列为空)后才继续走,否则就一直阻塞

还有几个不同种类的队列:

q =queue.LifoQueue()   #先进后出
 q=queue.PriorityQueue()   #优先级队列
#优先级高的先出来,压进去的时候带优先级,数据都是[优先级,数据]
#例如:q.put([10,\'info\']),10是优先级

队列应用:生产者与消费者
生产者与消费者模型是将同步转化为异步的好例子。
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

import time,random
import queue,threading

q = queue.Queue()

def Producer(name):
  count = 0
  while count <10:
    print("making........")
    time.sleep(random.randrange(3))
    q.put(count)
    print(\'Producer %s has produced %s baozi..\' %(name, count))
    count +=1
    #q.task_done()
    #q.join()
    print("ok......")
def Consumer(name):
  count = 0
  while count <10:
    time.sleep(random.randrange(4))
    if not q.empty():
        data = q.get()
        #q.task_done()
        #q.join()
        print(data)
        print(\'\\033[32;1mConsumer %s has eat %s baozi...\\033[0m\' %(name, data))
    else:
        print("-----no baozi anymore----")
    count +=1

p1 = threading.Thread(target=Producer, args=(\'A\',))
c1 = threading.Thread(target=Consumer, args=(\'B\',))
# c2 = threading.Thread(target=Consumer, args=(\'C\',))
# c3 = threading.Thread(target=Consumer, args=(\'D\',))
p1.start()
c1.start()
# c2.start()
# c3.start()

 



协程

协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。、
协程是协作式的,而线程和进程都是抢占式的。
协程是程序员自己规定调度,即在哪里切换,切换之前保护起来,哪里换回来。目的就是要使得cpu不能闲置

协程的原理是什么?
 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率,!!!非io操作的切换会降低效率。协程面对的是主要也是io操作
  具体地说,对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。

协程对比线程优点是什么?
优点
1)协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2)单线程内就可以实现并发的效果,最大限度地利用cpu
3)修改共享数据不需加锁

缺点
 1)协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2)协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程(调用time.sleep可是,所以打monkey补丁)。
3) 因为协程在线程里面,所以只用协程也不能用多核,要配合多进程才行。


协程代码
如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制)

 注意:gevent是遇到IO就会切换,换句话说,若开启的任务都没有IO的话,是不会切换的。
  并且主线程,也算一个任务,即使不是协程开启

#用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)#创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的
g2=gevent.spawn(func2)  #gevent.spawn()方法会创建一个新的greenlet协程对象,并运行它。
gevent.joinall([g1,g2])  #gevent.joinall()方法会等待所有传入的greenlet协程运行结束后
g1.join() #等待g1结束
g2.join() #等待g2结束
#或者上述两步合作一步:gevent.joinall([g1,g2])
g1.value#拿到func1的返回值

#猴子布丁,下列顺序不能乱。
from gevent import monkey; monkey.patch_socket()
import gevent
import socket
 

 

以上是关于进程线程协程的主要内容,如果未能解决你的问题,请参考以下文章

python 多进程,多线程,协程

进程线程协程

进程线程协程的区别

进程_线程 之 --- 协程

协程

进程线程和协程的区别