Python基础知识点——进程、线程、协程

1. 进程

进程的定义
进程是资源分配最小单位

  1. 一个运行起来的程序就是一个进程
  2. 什么是程序(程序是我们存储在硬盘里的代码、文件)
    当我们双击图标,打开程序的时候,实际上就是通过I/O操作(读写)内存条里面
    内存条就是我们所指的资源
  3. 进程之间内存独立,不能相互访问

进程定义拓展内容:

  1. 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,这种执行的程序就称之为进程
  2. 程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念
  3. 在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。
  4. 进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。
  5. 进程之间有自己独立的内存,各进程之间不能相互访问
  6. 创建一个新线程很简单,创建新进程需要对父进程进行复制

进程和程序的区别

  1. 程序只是一个普通文件,是一个机器代码指令和数据的集合,所以,程序是一个静态的实体
  2. 而进程是程序运行在数据集上的动态过程,进程是一个动态实体,它应创建而产生,应调度执行因等待资源或事件而被处于等待状态,因完成任务而被撤消
  3. 进程是系统进行资源分配和调度的一个独立单位
  4. 一个程序对应多个进程,一个进程为多个程序服务(两者之间是多对多的关系)
  5. 一个程序执行在不同的数据集上就成为不同的进程,进程可以控制 块 来唯一标识每个程序

多道编程概念:

  • 多道编程: 在计算机内存中同时存放几道相互独立的程序,他们共享系统资源,相互穿插运行
  • 单道编程: 计算机内存中只允许一个的程序运行

进程通信:

python提供了多种进程通信的方式,主要QueuePipe这两种方式,Queue用 于多个进程间实现通信,Pipe是两个进程的通信。

  • Queue有两个方法:

    1. Put方法:以插入数据到队列中
    2. Get方法:从队列读取并且删除一个元素
  • Pipe常用于两个进程,两个进程分别位于管道的两端
    Pipe方法返回(conn1,conn2)代表一个管道的两个端,Pipe方法有duplex参数,默认为True,即全双工模式,若为FALSE,conn1只负责接收信息,conn2负责发送,

  • managers

  • RabbitMQ、redis等

进程间互相访问数据的四种方法:
注:不同进程间内存是不共享的,所以互相之间不能访问对方数据

  1. 利用Queues实现父进程到子进程(或子进程间)的数据传递
  2. 使用管道pipe实现两个进程间数据传递
  3. Managers实现很多进程间数据共享
  4. 借助redis中间件进行数据共享

进程池:
为什么需要进程池?

  • 一次性开启指定数量的进程
  • 如果有十个进程,有一百个任务,一次可以处理多少个(一次性只能处理十个)
  • 防止进程开启数量过多导致服务器压力过大
  • 开进程池是为了效率,进程直接的切换是属于IO调度,每个进程的内存空间都有自己的寄存器,堆栈和文件。
from  multiprocessing import Process,Pool
import time,os
def foo(i):
    time.sleep(2)
    print("in the process",os.getpid()) #打印子进程的pid
return i+100

def call(arg):
print('-->exec done:',arg,os.getpid())

if __name__ == '__main__':
    pool = Pool(3)                      #进程池最多允许5个进程放入进程池
    print("主进程pid:",os.getpid())     #打印父进程的pid
    for i in range(10):
       #用法1 callback作用是指定只有当Foo运行结束后就执行callback调用的函数,父进程调用的callback函数
        pool.apply_async(func=foo, args=(i,),callback=call)
        #用法2 串行 启动进程不在用Process而是直接用pool.apply()
        # pool.apply(func=foo, args=(i,))
    print('end')
    pool.close()    #关闭pool
    pool.join()     #进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。

进程池优点:

  1. 不仅仅减少了IO而且还减少了内存。
  2. 下面的例子便可以区分 其他语言的进程池还可以根据服务器的压力来增减,有着上限和下限。
    建议:超过五个进程就用进程池

有了进程为什么还要线程?

  1. 进程优点:
    提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率
  2. 进程的两个重要缺点
    a. 第一点:进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
    b. 第二点:进程在执行的过程中如果阻塞,即使进程中有些工作不依赖于输入的数据,也将无法执行(例如等待输入,整个进程就会挂起)。
    c. 例如,我们在使用qq聊天, qq做为一个独立进程如果同一时间只能干一件事,那他如何实现在同一时刻 即能监听键盘输入、又能监听其它人给你发的消息
    d. 你会说,操作系统不是有分时么?分时是指在不同进程间的分时呀
    e. 即操作系统处理一会你的qq任务,又切换到word文档任务上了,每个cpu时间片分给你的qq程序时,你的qq还是只能同时干一件事呀

2. 线程

线程的定义:

  1. 线程是系统调度的最小单位
  2. 同进程下线程资源共享
  3. 进程无法自己执行,只有通过线程操作CPU,内存
  4. 为了保证数据安全,必须使用线程锁

线程定义拓展内容:

  1. 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
  2. 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
  3. 无论你启多少个线程,你有多少个cpu,Python在执行的时候会淡定的在同一时刻只允许一个线程运行
  4. 进程本身是无法自己执行的,要操作cpu,必须创建一个线程,线程是一系列指令的集合
  5. 所有在同一个进程里的线程是共享同一块内存空间的,不同进程间内存空间不同
  6. 同一个进程中的各线程可以相互访问资源,线程可以操作同进程中的其他线程,但进程仅能操作子进程
  7. 两个进程想通信,必须要通过一个中间代理
  8. 对主线程的修改可能回影响其他子线程,对主进程修改不会影响其他进程因为进程间内存相互独立,但是同一进程下的线程共享内存

进程和线程的区别:

  1. 进程包含线程
  2. 线程共享内存空间
  3. 进程内存是独立的(不可互相访问)
  4. 进程可以生成子进程,子进程之间互相不能互相访问(相当于在父级进程克隆两个子进程)
  5. 在一个进程里面线程之间可以交流。两个进程想通信,必须通过一个中间代理来实现
  6. 创建新线程很简单,创建新进程需要对其父进程进行克隆。
  7. 一个线程可以控制或操作同一个进程里面的其它线程。但进程只能操作子进程。
  8. 父进程可以修改不影响子进程,但不能修改。
  9. 线程可以帮助应用程序同时做几件事

for循环同时启动多个线程:

import threading
import time

def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)
for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.start()

t.join(): 实现所有线程都执行结束后再执行主线程:

import threading
import time
start_time = time.time()

def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)
t_objs = []    #将进程实例对象存储在这个列表中
for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.start()          #启动一个线程,程序不会阻塞
    t_objs.append(t)
print(threading.active_count())    #打印当前活跃进程数量
for t in t_objs: #利用for循环等待上面50个进程全部结束
    t.join()     #阻塞某个程序
print(threading.current_thread())    #打印执行这个命令进程

print("----------------all threads has finished.....")
print(threading.active_count())
print('cost time:',time.time() - start_time)

setDaemon(): 守护线程,主线程退出时,需要子线程随主线程退出:

import threading
import time
start_time = time.time()

def sayhi(num): #定义每个线程要运行的函数
    print("running on number:%s" %num)
    time.sleep(3)
for i in range(50):
    t = threading.Thread(target=sayhi,args=('t-%s'%i,))
    t.setDaemon(True)  #把当前线程变成守护线程,必须在t.start()前设置
    t.start()          #启动一个线程,程序不会阻塞
print('cost time:',time.time() - start_time)

GIL全局解释器锁:保证同一时间仅有一个线程对资源有操作权限:
作用:在一个进程内,同一时刻只能有一个线程执行
说明:python多线程中GIL锁只是在CPU操作时(如:计算)才是串行的,其他都是并行的,所以比串行快很多

  1. 为了解决不同线程同时访问同一资源时,数据保护问题,而产生了GIL
  2. GIL在解释器的层面限制了程序在同一时间只有一个线程被CPU实际执行,而不管你的程序里实际开了多少条线程
  3. CPython自己定义了一个全局解释器锁,同一时间仅仅有一个线程可以拿到这个数据
  4. python之所以会产生这种不好的状况是因为python启用一个线程是调用操作系统原生线程,就是C接口
  5. 但是这仅仅是CPython这个版本的问题,在PyPy,中就没有这种缺陷

线程锁:

  1. 当一个线程对某个资源进行CPU计算的操作时加一个线程锁,只有当前线程计算完成主动释放锁,其他线程才能对其操作
  2. 这样就可以防止还未计算完成,释放GIL锁后其他线程对这个资源操作导致混乱问题
  3. 线程锁本质把线程中的数据加了一把互斥锁

有了GIL全局解释器锁为什么还需要线程锁

因为cpu是分时使用的

GIL是限制同一个进程中只有一个线程进入Python解释器。。。。。
而线程锁是由于在线程进行数据操作时保证数据操作的安全性(同一个进程中线程之间可以共用信息,如果同时对数据进行操作,则会出现公共数据错误)
其实线程锁完全可以替代GIL,但是Python的后续功能模块都是加在GIL基础上的,所以无法更改或去掉GIL,这就是Python语言最大的bug…只能用多进程或协程改善,或者直接用其他语言写这部分

死锁定义

两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去

用户锁:

import time
import threading
lock = threading.Lock()          #1 生成全局锁
def addNum():
    global num                  #2 在每个线程中都获取这个全局变量
    print('--get num:',num )
    time.sleep(1)
    lock.acquire()              #3 修改数据前加锁
    num  -= 1                   #4 对此公共变量进行-1操作
    lock.release()              #5 修改后释放

Semaphore(信号量):

  • 互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据
  • 比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去
  • 作用就是同一时刻允许运行的线程数量

多线程:

  • GIL锁:
    全局解释锁,每次只能一个线程获得cpu的使用权:为了线程安全,也就是为了解决多线程之间的数据完整性和状态同步而加的锁,因为我们知道线程之间的数据是共享的。
  • join()作用:
    在进程中可以阻塞主进程的执行, 直到等待子线程全部完成之后, 才继续运行主线程后面的代码
  • setDaemon():
    将该线程标记为守护线程或用户线程

线程池

  • 使用以下模块创建线程池:
  1. 使用threadpool模块,这是个python的第三方模块,支持python2和python3
  2. 使用concurrent.futures模块,这个模块是python3中自带的模块,但是,python2.7以上版本也可以安装使用
  • 线程池实现并发:
import requests
from concurrent.futures import ThreadPoolExecutor

def fetch_request(url):
    result = requests.get(url)
    print(result.text)

url_list = [
    'https://www.baidu.com',
    'https://www.google.com/',         #google页面会卡住,知道页面超时后这个进程才结束
    'http://dig.chouti.com/',          #chouti页面内容会直接返回,不会等待Google页面的返回
]

pool = ThreadPoolExecutor(10)            # 创建一个线程池,最多开10个线程
for url in url_list:
    pool.submit(fetch_request,url)       # 去线程池中获取一个线程,线程去执行fetch_request方法

pool.shutdown(True)                      # 主线程自己关闭,让子线程自己拿任务执行


3. 协程

什么是协程(进入上一次调用的状态)

  1. 协程,又称微线程,纤程,协程是一种用户态的轻量级线程。
  2. 线程的切换会保存到CPU的栈里,协程拥有自己的寄存器上下文和栈,
    协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
  3. 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态
  4. 协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)
  5. 协程能在单线程处理高并发

协程的定义:

  1. 协程在单线程下实现并发效果
  2. 协程遇IO自动切换
  3. 协程保留上一次调用状态

协程的优点:

  1. 无需线程上下文切换的开销
  2. 无需原子操作锁定及同步的开销,因为协程是串行的
  3. 方便切换控制流,简化编程模型
  4. 高并发,高扩展,低成本,一个cpu支持上万个协程没有问题,所以非常适合高并发处理

协程的缺点:

  1. 无法利用多核的优势,但是协程和进程配合就可以使协程运行在不同的cpu上,就可以利用 多核的优势,但是在现实中,大部分场景都没有这个需要
  2. 只要一个协程阻塞(Blocking),就会阻塞整个协程,因为协程是串行的,这个问题必须要解决,才能让协程大范围应用
  • 解决方法:
    如果遇到io操作,则进行协程切换,去执行其他的协程,可以用gevent来实现,具体的实现是这样的,
    比如协程1通过os去读一个file,这个时候就是一个 io操作,在调用os的接口前,就会有一个列表,协议1的这个操作就会被注册到这个列表中,然后就切换到其他协程去处理;等待os拿到要读file后,也会把这个文件句柄放在这个列表中,然后等待在切换到
    协程1的时候,协程1就可以直接从列表中拿到数据,这样就可以实现不阻塞了

协程处理并发:

  • Gevent
    遇IO自动切换
  1. Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
  2. 协程之所以快是因为遇到I/O操作就切换(最后只有CPU运算)
  3. 其实Gevent模块仅仅是对greenlet的再封装,将I/O间的手动切换变成自动切换
  • Greenlet
    遇IO手动切换
  1. Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

使用协程处理并发
注:Gevent只用起一个线程,当请求发出去后gevent就不管,永远就只有一个线程工作,谁先回来先处理

import gevent
import requests
from gevent import monkey

monkey.patch_all()

# 这些请求谁先回来就先处理谁
def fetch_async(method, url, req_kwargs):
    response = requests.request(method=method, url=url, **req_kwargs)
    print(response.url, response.content)
    
    ##### 发送请求 #####
gevent.joinall([
    gevent.spawn(fetch_async, method='get', url='https://www.python.org/', req_kwargs={}),
    gevent.spawn(fetch_async, method='get', url='https://www.google.com/', req_kwargs={}),
    gevent.spawn(fetch_async, method='get', url='https://github.com/', req_kwargs={}),
])

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值