博客

  • Python Scrapy 爬虫框架

    Scrapy 相关细节

    • 安装scrapy框架
      • pip install scrapy -i https://pypi.douban.com/simple
    • 创建scrapy爬虫
      • 进入项目文件夹
        • cd project_name/project_name/spiders
      • 运行命令
        • scrapy startproject project_name url
        • D:\Practice\Python\Scrapy_20230226\scrapy_carhome\scrapy_carhome\spiders> scrapy genspider car https://car.autohome.com.cn/price/brand-15.html
    • 运行爬虫
      • 进入当前爬虫所在文件夹
        • scrapy crawl spider_name
        • D:\Practice\Python\Scrapy_20230226\scrapy_carhome\scrapy_carhome\spiders> scrapy crawl car

     

     

     

    Scrapy 数据流程

    https://docs.scrapy.org/en/latest/topics/architecture.html

    1. 引擎得到来自于Spider的url请求
    2. 引擎调用调度器将待爬取url放入列队中
    3. 调度器返回请求给引擎
    4. 引擎发送请求给下载器
    5. 下载器获取互联网资源, 获取数据后返回给引擎
    6. 引擎收到来自下载器的响应, 将其发送给Spider, 以进行处理
    7. Spider返回处理过的数据给引擎(同时也发送下一个新的URL给引擎)
    8. 引擎发送处理过的数据给Pipelines, 以保存数据
    9. 重复第三步之后的步骤, 直到没有新的任务

     

     

    The data flow in Scrapy is controlled by the execution engine, and goes like this:

    1. The Engine gets the initial Requests to crawl from the Spider.
    2. The Engine schedules the Requests in the Scheduler and asks for the next Requests to crawl.
    3. The Scheduler returns the next Requests to the Engine.
    4. The Engine sends the Requests to the Downloader, passing through the Downloader Middlewares (see process_request()).
    5. Once the page finishes downloading the Downloader generates a Response (with that page) and sends it to the Engine, passing through the Downloader Middlewares (see process_response()).
    6. The Engine receives the Response from the Downloader and sends it to the Spider for processing, passing through the Spider Middleware (see process_spider_input()).
    7. The Spider processes the Response and returns scraped items and new Requests (to follow) to the Engine, passing through the Spider Middleware (see process_spider_output()).
    8. The Engine sends processed items to Item Pipelines, then send processed Requests to the Scheduler and asks for possible next Requests to crawl.
    9. The process repeats (from step 3) until there are no more requests from the Scheduler.

     

     

  • AI-ML-DL-NNs-用gensim学习word2vec

    了解Gensim

    Gensim(generate similarity)是一个简单高效的自然语言处理Python库,用于抽取文档的语义主题(semantic topics)。Gensim的输入是原始的、无结构的数字文本(纯文本),内置的算法包括Word2Vec,FastText,潜在语义分析(Latent Semantic Analysis,LSA),潜在狄利克雷分布(Latent Dirichlet Allocation,LDA)等,通过计算训练语料中的统计共现模式自动发现文档的语义结构。这些算法都是非监督的,这意味着不需要人工输入——仅仅需要一组纯文本语料。一旦发现这些统计模式后,任何纯文本(句子、短语、单词)就能采用语义表示简洁地表达。

     

    特点

      • Memory independence: 不需要一次性将整个训练语料读入内存,Gensim充分利用了Python内置的生成器(generator)和迭代器(iterator)用于流式数据处理,内存效率是Gensim设计目标之一。
      • Memory sharing: 训练好的模型可以持久化到硬盘,和重载到内存。多个进程之间可以共享相同的数据,减少了内存消耗。
      • 多种向量空间算法的高效实现: 包括Word2Vec,Doc2Vec,FastText,TF-IDF,LSA,LDA,随机映射等。
      • 支持多种数据结构
      • 基于语义表示的文档相似度查询

    核心概念

     

    corpus

    一组纯文本的集合,在Gensim中,语料有两个角色:

    1. 模型训练的输入。此时语料用于自动训练机器学习模型,如LsiModel,LdaModel,模型使用训练语料发现公共主题,初始化模型参数。因为Gensim聚焦于非监督模型,因此无需人工干预。
    2. Documents to organize。模型训练好后,可以用于从新文档(训练语料中没有出现过的)抽取主题。

     

    向量空间模型(vector space model,VSM)

    在向量空间模型中,每一篇文档被表示成一组特征。特征可以认为是问答对(question-answer pair),例如:

    1. How many times does the word splonge appear in the document? Zero.
    2. How many paragraphs does the document consist of? Two.
    3. How many fonts does the document use? Five.
    • 单词splonge在文档中出现过几次? 0
    • 文档包含几个段落? 2
    • 文档里包含几种字体? 5

     

    通常对于每一个question分配一个id,因此这篇文档可以表示成一系列的二元对:

    • (1, 0.0): 第1个问题, 答案是0.0
    • (2, 2.0): 第2个问题, 答案是2.0
    • (3, 5.0): 第3个问题, 答案是5.0

    如果我们事先知道所有的question,我们可以隐式的省略这些id只保留answer序列,简写成:

    • ( 0.0 , 2.0 , 5.0 )

    这组answer序列就可以被认为是一个向量,用于代表这篇文档。

    每一篇文档的questions都是相同的,因此在观察两个向量后,我们希望能够得到如下结论:两个向量很相似,因此原始文档一定也很相似。 当然,这个结论是否正确取决于questions选择的好坏。

    我们最常用的词袋模型就是一种向量空间模型,question是词汇表中的词w i w_iwi是否出现在文档中,因此用词袋模型表示文档,向量的维度等于词汇表中单词的数量V

    sparse vector

    为了节约空间,在Gensim中省略了所有值为0.0的元素,例如,对于上面的向量( 0.0 , 2.0 , 5.0 ) (0.0,2.0,5.0),我们只需要写[ ( 2 , 2.0 ) , ( 3 , 5.0 ) ] ,向量中每一个元素是一个二元元组(feature_id, feature_value)。如果某个特征在稀疏表示中缺省,可以很自然的认为其值为0.0。

    streamed corpus

    Gensim没有规定任何指定的数据格式,语料是一组稀疏向量序列。例如:[ [ ( 2 , 2.0 ) , ( 3 , 5.0 ) ]]是一个包含两篇文档的简单语料,两篇文档被表示成两个稀疏向量,第一个有两个非零元素,第二个有一个非零元素。这个例子中,我们将语料用Python中list表示,但是Gensim并没有规定语料必须表示成list,Numpy中array,Pandas中dataframe或者其他任何对象,迭代时,将依次产生这些稀疏向量。这个灵活性允许我们创建自己的语料类,直接从硬盘、网络、数据库……中流式产生稀疏向量。

    model, transformation

    Gensim中用model指代将一篇文档转换(transform)为另一种形式的模型代码以及相关参数,因为文档被表示成向量,因此model可以认为是从一个向量空间到另一个向量空间的变换,这个变换的参数是从训练数据中学习得到的。训练好的models可以被持久化到硬盘,之后在重载回来,无论是在新的训练文档中继续训练还是用于转换一篇文档。Gensim实现了很多模型,比如:Word2Vec,LsiModel,LdaModel,FastText等,具体的可以参考Gensim API。

     

    一个例子

    给定语料包含9篇文档,12个特征:

    • 9篇文档, 9个内层list
    • 12个特征, 12个特征id
    corpus=[[(0, 1.0), (1, 1.0), (2,1.0)],
            [(2, 1.0), (3, 1.0), (4, 1.0), (5, 1.0), (6, 1.0), (8, 1.0)],
            [(1, 1.0), (3, 1.0), (4, 1.0), (7, 1.0)],
            [(0, 1.0), (4, 2.0), (7, 1.0)],
            [(3, 1.0), (5, 1.0), (6, 1.0)],
            [(9, 1.0)],
            [(9, 1.0), (10, 1.0)],
            [(9, 1.0), (10, 1.0), (11, 1.0)],
            [(8, 1.0), (10, 1.0), (11, 1.0)]]

    接下来,我们初始化一个转换:

    from gensim import corpora, models, similarities
    
    tfidf = models.TfidfModel(corpus)
    

    我们采用tfidf模型训练这个转换,即原始bag-of-words向量→ \rightarrowtfidf向量,训练好的模型存储在变量tfidf中,接下来我们用这个tfidf模型将一篇新的文档vec转换为tfidf向量:

    vec = [(0, 1), (4, 1)]
    print(tfidf[vec])

    得到的结果是[ ( 0 , 0.80752440244407231 ) , ( 4 , 0.58983416267400446 ) ] [(0, 0.80752440244407231)]

    同样,我们可以利用语料的tfidf向量训练一个相似度计算模型:

    index = similarities.SparseMatrixSimilarity(tfidf[corpus], num_features=12)
    

    然后计算文档vec与语料每一篇文档的相似度:

    sims = index[tfidf[vec]]
    print(list(enumerate(sims)))

    [(0,0.4662244),(1,0.19139354),(2,0.24600551),(3,0.82094586),(4,0.0),(5,0.0),(6,0.0),(7,0.0),(8,0.0)],表示vec与第一篇文档有46.6%的相似度,与第二篇文档有19.1%的相似度……

     

    gensim的处理流程也可以分为三步:首先将文档表示成特征向量,然后利用训练语料训练转换模型,最后将训练好的模型应用到新文档上。

     

  • Python 环境问题

    Pycharm

    Anaconda

    Conda

    创建Conda虚拟环境

    • 指定python版本和环境名称
      • conda create -n env_name python=3.8
    • 指定python版本, 环境名称, 环境安装路径
      • conda create –prefix=C:/ProgramData/Anaconda3/envs/ENV_spider python=3.9
    • 激活所安装的环境
      • conda activate env_name
    • 在当前激活的环境里安装包
      • conda install package_name
    • 退出当前激活的环境
      • conda deactivate

    PIP

     

    Jupyter

    安装内核

    • 激活/切换到要添加的虚拟环境, 确认是否安装 ipykernel
      • python -m ipykernel –version
    • 如果没有,则安装
      • python -m pip install ipykernel
    • 为 jupyter 添加内核
      • python -m ipykernel install –name env_name

    查看已安装内核

    • jupyter kernelspec list
    (ENV_machinelearning) λ jupyter kernelspec list
    Available kernels:
      %s    %s python3             C:\ProgramData\Anaconda3\envs\ENV_machinelearning\share\jupyter\kernels\python3
      %s    %s pycharm-78712231    C:\Users\Jin\AppData\Roaming\jupyter\kernels\pycharm-78712231
      %s    %s pycharm-ae39b4c1    C:\Users\Jin\AppData\Roaming\jupyter\kernels\pycharm-ae39b4c1
      %s    %s env_machinelearning C:\ProgramData\jupyter\kernels\env_machinelearning

     

    删除 jupyter 内核

    • jupyter kernelspec remove kernelname

     

     

     

  • 诡秘之主里的故乡

    我认识了位吹骨笛的弗萨克人,他叫奥涅格,自称出身于弗萨克巨人血统最纯正的几个族群之一,他吹骨笛的样子很忧郁,仿佛并不属于这里,却又不知道该去哪里,我和奥涅格闲聊了一会,提到了刚才的那个疑问,他说,他只是在思念故乡。

    可问题在于,他是标准的圣密隆人,从未离开过这里。

    奥涅格没立刻回答我,又吹了几分钟的骨笛,然后才说,他思念的是巨人血脉的源头,神话传说里的那个巨人王庭。

    他告诉我,他和他那个族群的弗萨克人,经常会梦见高高的山峰、以此为基座的巨大城墙、永远沐浴在黄昏光芒里的宫殿、高塔和各种建筑,这和黄昏巨殿很像,却又更加不可思议,更有史诗感和奇迹感。无需别人告知,奥涅格和他的族人确信这是巨人王庭。

    聊到最后,奥涅格缓慢站了起来,说感谢我的倾听,他要离开弗萨克,去寻找巨人王庭,寻找他精神与心灵的故乡了。他认为苏尼亚海最东面的区域,也许就藏着通往巨人王庭的道路。

    他说,几千年过去,巨人们从未忘记故乡,现在,轮到他追寻祖辈们的脚步了,这条路,除非抵达终点,否则永不停止。
    他又吹起了骨笛,在那悠扬却悲伤的旋律里逐渐远去。

     

    “故乡啊……”

    故乡啊……读到这里,克莱恩第一次觉得自己与大帝的情感有了百分之百的共鸣。

    虽然罗塞尔最后只写下了看似重复的三个字,没有阐述自己的内心,但克莱恩却完全读懂了大帝那一刻的心绪起伏,因为,他和罗塞尔都像奥涅格一样,有一个精神与心灵的故乡。

     

    “我考虑起一个问题,那就是要不要私下教贝尔纳黛中文。

    “不行不行,我前面写的日记要是被她看懂,我这个当爹的还不如找块豆腐撞死算了,在我女儿心里,父亲必须充满光辉。

    “可是,用中文写日记已经是我和地球和过去最后的羁绊了,我的女儿,应该在某种程度上继承这个羁绊。”

    “四月六日,经过几天的考虑,我决定将两个汉字当成特殊的符号教导贝尔纳黛,告诉她,这是她父亲给她的护身咒文,让她永远铭记。

    “她不需要懂相应的意思,只用记住。

    “这两个汉字是:

    “故乡。”

     

    故乡……克莱恩又一次重复起这个词语,眼眶甚至有点发热。

    他终于明白“神秘女王”贝尔纳黛为什么会挑选这三页日记了,因为上面有她父亲留给她的护身符号:“故乡。”

    这一刻,克莱恩仿佛看见了一条情感之河,它表面沉静地流淌着,水下激流无数,翻滚不休。

    克莱恩随即收回视线,让日记消失,抬头望向“隐者”嘉德丽雅道:

    “你的问题是什么?”

    “尊敬的‘愚者’先生,我想知道这两个符号的意思?”

    得到允许后,她具现出了两个汉字,分别是“故”与“乡”。

    果然……克莱恩无声叹息道:

    “它们合在一起的意思是,家乡,心灵与精神的归宿。”

    合在一起……心灵与精神的归宿……“隐者”嘉德丽雅将重点放在了后面半句话,因为她非常清楚“神秘女王”的家乡在哪里,也不认为这有什么特殊的地方。

     

    无论怎么样,小说作者,爱潜水的乌贼,背叛了自己的故乡。

  • Django Rest Framework

    Web应用开发模式

    前后端分离

     

    通俗地讲, 其实判定很简单:如果前端和后端这两个角色, 只通过API 文档就能进行数据交流,就说明他们的逻辑是分离的。我们可以称之为 “前后端代码分离”。

    如果除了 API 文档之外还需要各种其他的数据交流方式,比如后端把数据藏在一个 div 的属性里,那么就不是前后端分离的。像各种框架里的模板引擎和渲染功能。

    至于所谓的 “最佳实践”并不存在, 但通用的, 类似的方案如下:

    • 前端服务器
      • 将前端的所有代码使用javascript+node.js的方式部署在node.js服务器上,作为前端稳定的服务,提供页面使用。
    • 后端服务器
      • 后端的服务器写各种业务逻辑代码, 如业务逻辑, 鉴权逻辑.
    • 前后端交互
      • 后端服务为前端提供一个统一的访问入口(一般是单独部署一个服务),使用restful等风格提供http服务供前端调用,实现前后端数据传输(通常以JSON来交换数据)。

     

     

    这样一来, 现在 前端服务器不再处理任何业务,它接收到请求后,经过转换,发送给各个相关后端服务器,将各个后端服务器返回的,处理过的业务数据填入 HTML 模板,最后发送给浏览器。前端服务器和后端服务器间,可以选用任何你觉得合适的通信手段,可以是 REST,可以是 RPC,选用什么样的通信手段,这是另一个议题了。

    前后端分离, 是浏览器和后端服务分离吗?

    不是,前后端分离里的前端不是浏览器,指的是生成 HTML 的那个服务,它可以是一个仅仅生成 HTML 的 Web 服务器,也可以是在浏览器中通过 JS 动态生成 HTML 的 单页应用。

     

    实践中,有实力的团队往往在实现前后端分离里时,前端选用 node 服务器,后端选用 C#、Java 等。

    API接口规范

     

    Restful

    • 基于http协议
    • 传统的路由设计
      • 增: localhost.com/user/add
      • 删: localhost.com/user/delete/1
      • 改: localhost.com/user/edit/1
      • 查: localhost.com/user/query/1
    • Restful的设计规范, 基于资源, 资源操作由请求方式决定
    • 比如 api.localhost/users/
      • 增: post
      • 删: delete
        • 删除单个资源: api.localhost.com/users/1
        • 删除所有资源: localhost/users/
      • 改:
        • put: 更新单个资源
        • patch: 更新局部/部分资源
      • 查: get
        • 查单个资源: api.localhost.com/users/1
        • 查所有资源: api.localhost.com/users/
      • 过滤,通过在url上传参的形式传递搜索条件
        • https://api.localhost.com/v1/zoos?limit=10:指定返回记录的数
        • https://api.localhost.com/v1/zoos?offset=10:指定返回记录的开始位置
        • https://api.localhost.com/v1/zoos?page=2&per_page=100:指定第几页,以及每页的记录数
        • https://api.localhost.com/v1/zoos?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序
        • https://api.localhost.com/v1/zoos?animal_type_id=1:指定筛选条件

    RPC

    • 性能更高
    • 基于TCP/IP协议(传输层)

    序列化

    什么是序列化?

    序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。

    • 序列化:把对象转化为可传输的字节序列过程称为序列化。
    • 反序列化:把字节序列还原为对象的过程称为反序列化。

     

    为什么要序列化?

    如果光看定义我想你很难一下子理解序列化的意义,那么我们可以从另一个角度来推导出什么是序列化, 那么究竟序列化的目的是什么?

    其实序列化最终的目的是为了对象可以跨平台存储,和进行网络传输。而我们进行跨平台存储和网络传输的方式就是IO,而我们的IO支持的数据格式就是字节数组

     

    在大多数开发者, 所谓序列化是指数据在 对象和json之前的相互转换.

     

     

    Django Rest Framework

    DRF的安装

    创建新环境

    conda create –prefix=C:/ProgramData/Anaconda3/envs/ENV_DRF_01 python=3.9

    – 激活环境

    • conda activate C:\ProgramData\Anaconda3\envs\ENV_DRF_01

    安装django

    • conda install django=3.2.*

    创建django项目

    • django-admin startproject myshop

    安装drf, drf是基于django的一个包

    • pip install djangorestframework
    • pip install pymysql

    创建django项目内的APP

    • python manage.py startapp LearnDRF

    进行django配置

    • 数据库
    • INSTALLED_APPS

     

    DRF快速体验

    创建并配置数据库

    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',
            'NAME': 'learndrf',  # 数据库名字
            'USER': 'root',
            'PASSWORD': 'zxcvbnm',
            'HOST': '127.0.0.1',  # 那台机器安装了MySQL
            'PORT': 3306,
        }
    }

    模型类

    class Student(models.Model):
        # 模型字段
        name = models.CharField(max_length=100,verbose_name="姓名")
        sex = models.BooleanField(default=1,verbose_name="性别")
        age = models.IntegerField(verbose_name="年龄")
        class_null = models.CharField(max_length=5,verbose_name="班级编号")
        description = models.TextField(max_length=1000,verbose_name="个性签名")
    
        class Meta:
            db_table="tb_student"
            verbose_name = "学生"
            verbose_name_plural = verbose_name

    执行迁移

    python manage.py makemigrations
    python manage.py migrate

    可以添加一些数据, 方便测试.

     

    FBV 和CBV

    FBV

    fbv是指function based view, 望文生义, 义如其名.

    fbv 模式的路由

    path(‘login_fbv/’, login)

    • login是view.py的login函数.
    CBV

    path(‘student/’, StudentView.as_view())

    • StudentView是view.py的类
    • as_view()是drf包里面视图方法

     

    DRF的使用

    DRF的优势?

    谈到DRF的使用, 很容易会产生一堆问题? DRF有什么作用? 通常用于哪些场景? 它的优势是什么? 为什么用DRF, 而不是django?

    一句话回答: DRF可以更快, 更高效的完成REST API开发.

    DRF如何实现这些优势的?

    DRF包含一系列高度抽象和功能丰富的组件

    序列化组件

    1.作用: 把python中的对象,转成json格式字符串

    使用步骤1: 写一个类继承Serializer或者ModelSerializer
    举例(类中选取字段进行序列化):
    class BookSerializer(serializers.Serializer):
    id = serializers.CharField()
    title = serializers.CharField()
    price = serializers.CharField()
    举例(把类中字段全部进行序列化):

    class TestSer(serializers.ModelSerializer):
    class Meta:
    model = models.Takes
    fields = ‘__all__’
    总结:
    变量名和source指定的值不能一样

    source=’publish.name’还支持继续 .

    source 还支持方法(没用)

    支持写方法,如下
    方法一定传一个参数,是当前book对象

    publish_dic=serializers.SerializerMethodField()
    def get_publish_dic(self,obj):
    return
    结果:{‘id’:obj.publish.pk,’name’:obj.publish.name}

    认证组件

    1.认证组件的好处:
    比如要访问books/路径,必须登录之后才能访问。一旦登录成功,在响应结果中写一个随机字符串

    举例: {status:100
    msg:登录成功
    token:sdafsdfasd
    }

    2.使用步骤1:写一个类,继承BaseAuthentication

    3.使用步骤2:def authenticate(self,request) ,记住传request对象

    4.在视图类中使用:(不要加括号):

    1.局部使用:authentication_classes=[AuthLogin](写在views中)

    2.全局使用:-REST_FRAMEWORK=
    {“DEFAULT_AUTHENTICATION_CLASSES”:[“app01.auth.AuthLogin”,](在setting中配置)

    3.全局使用的局部禁用:authentication_classes = [](写在views中)

     

    权限组件

    1.使用步骤1:写一个类,继承BasePermission

    2.使用步骤2:def has_permission(self, request, view): ,记住传
    request对象和view

    3.在视图类中使用:(不要加括号):

    1.局部使用:permission_classes=[MyPer](写在views中)

    2.全局使用:-REST_FRAMEWORK={“DEFAULT_PERMISSION_CLASSES”:[‘app01.auth.MyPer’]}(在setting中配置)

    3.全局使用的局部禁用:permission_classes = [](写在views中)

     

    节流组件

    1.使用步骤1:写一个类,继承SimpleRateThrottle

    2.使用步骤2:def get_cache_key(self, request, view):,记住传request对象和view

    3.使用步骤3
    :’DEFAULT_THROTTLE_RATES’: {
    这个key跟scope对应,value值3/m 3/h 4/d
    ‘xx’: ‘3/m’
    }(在setting中配置)

    节流作用:节流(Throttling)类似于权限,因为它决定了是否应该对请求进行授权。节流表示一个临时状态,并用于控制客户端对API的请求率。

    视图组件

    使用:
    1.局部使用:throttle_classes = [VisitThrottle](写在views中)
    2.全局使用:REST_FRAMEWORK={“DEFAULT_THROTTLE_CLASSES”:[“app01.auth.VisitThrottle”]}(在setting中配置)
    3.全局使用的局部禁用:throttle_classes = [](写在views中)

    视图作用: 数据库查询, 2. 构建序列化器, 进行序列化操作, 返回数据

     

    解析器

    1.局部使用:parser_classes=[JSONParser,](写在views中)
    2.全局使用:’DEFAULT_PARSER_CLASSES’:[‘rest_framework.parsers.JSONParser’](在setting中配置)

    解析器作用:解析器的作用就是服务端接收客户端传过来的数据,把数据解析成自己可以处理的数据。本质就是对请求体中的数据进行解析。

     

    渲染器

    DRF提供的渲染器有很多,默认是
    ‘DEFAULT_RENDERER_CLASSES’: (
    ‘rest_framework.renderers.JSONRenderer’,
    ‘rest_framework.renderers.BrowsableAPIRenderer’,
    ),

    渲染器的作用:渲染器同解析器相反,它定义了框架按照content_type来返回不同的响应。

     

    分页组件

    1.路由

    urlpatterns = [
    re_path(‘(?P<version>[v1|v2]+)/page1/’, Pager1View.as_view(),) #分页1
    ]
    序列化

    from rest_framework import serializers
    from api import models
    class PagerSerialiser(serializers.ModelSerializer):
    class Meta:
    model = models.Role
    fields = “__all__”
    视图

    from api.utils.serializsers.pager import PagerSerialiser
    from rest_framework.response import Response
    from rest_framework.pagination import PageNumberPagination
    class Pager1View(APIView):
    def get(self,request,*args,**kwargs):
    #获取所有数据
    roles = models.Role.objects.all()
    #创建分页对象
    pg = PageNumberPagination()
    #获取分页的数据
    page_roles = pg.paginate_queryset(queryset=roles,request=request,view=self)
    #对数据进行序列化
    ser = PagerSerialiser(instance=page_roles,many=True)
    return Response(ser.data)
    settings配置

    REST_FRAMEWORK = {
    #分页
    “PAGE_SIZE”:2 #每页显示多少个
    }

    版本组件

    自定义版本处理

    class ParamVersion(object):
    def determine_version(self, request, *args, **kwargs):
    # version = request._request.GET.get(“version”)
    # version = request.query_params.get(‘version’)
    version = request.GET.get(“version”)
    return version

    class UsersView(APIView):
    versioning_class = ParamVersion

    def get(self, request, *args, **kwargs):
    version = request.version
    return JsonResponse({‘version’: version})

    路由组件

    使用方法

    # format参数是给渲染器用的
    url(r’^(?P<version>[v1|v2]+)/myview\.(?P<format>\w+)$’, MyView.as_view({“get”: “list”, “post”: “create”})),
    创建router对象,并注册视图集。

    from rest_framework import routers
    router = routers.DefaultRouter()
    router.register(r’myview’, MyView)

    最原始的 Serializer + APIView

    2. ModelSerializer + APIView

     

    序列化和视图类的使用

    前置了解

    视图类

    • APIView
      • 继承APIView父类(Django中View的子类)
      • 具备View的所有特性
      • 提供了认证、授权、限流等功能
    • GenericAPIView
      • GenericAPIView为APIView的子类,它拓展了过滤、查询、分页的功能

    序列化器

    • Serializer
    • ModelSerializer

    这里将会通过以下三种组合来熟悉DRF的写法.

    DRF的 Serializer + DRF的 APIView

    DRF的 ModelSerializer + DRF的 APIView

    DRF的 ModelSerializer + DRF的 GenericAPIView

     

    Serializer + APIView

    path('student/', StudentView.as_view()), #drf的as_view方法
    re_path('student/(\d+)/', StudentDetailView.as_view()), #drf的as_view方法

     

    class StudentView(APIView):
    
    
        def get(self, request):
            print(request.GET)
            print(request.query_params)
            students = Student.objects.all()
            print(type(students))
            # students 是一个queryset 数据集. 所谓的序列化就是对该数据再进行封装, 这里其实是for循环, 将里面的数据都拿到,
    
            serializer = StudentsSerializer(instance=students, many=True)
            return Response(serializer.data)
    
    
    
        def post(self, request):
            print(request)
            print(request.data)
            print(type(request.data))
    
            serializer = StudentsSerializer(data=request.data)
            if serializer.is_valid():
                stu = Student.objects.create(**serializer.validated_data)
                ser = StudentsSerializer(instance=stu, many=False)
                return Response(ser.data)
    
            else:
                 return Response(serializer.errors)
    
    
    
    class StudentDetailView(APIView):
    
        print('StudentDetailView')
        def get(self, request, id):
            students = Student.objects.get(pk=id)
            serializer = StudentsSerializer(instance=students, many=False)
            return Response(serializer.data)
    
    
        def delete(self, request, id):
            Student.objects.get(pk=id).delete()
    
            return Response("已删除")
    
        def put(self, request, id):
            print(request)
            print(request.data)
            print(type(request.data))
    
            serializer = StudentsSerializer(data=request.data)
            if serializer.is_valid():
                print(serializer.validated_data)
    
                # 成功修改数据的条数, 为1
                # n = Student.objects.filter(pk=id).update(**serializer.validated_data)
                print(n)
    
                #要修改的数据对象
                stu = Student.objects.get(id=id)
                print(id)
                ser = StudentsSerializer(instance=stu, many=False)
                print(ser.data)
                return Response(ser.data)
    
            else:
                return Response(serializer.errors)

    get 查询所有数据

    • http://127.0.0.1:8000/student/

    post 添加数据

    • http://127.0.0.1:8000/student/

    delete 删除单条数据

    • http://127.0.0.1:8000/student/1/

    put 修改单条数据

    • http://127.0.0.1:8000/student/1/

    get 查询单条数据

    • http://127.0.0.1:8000/student/1/

    ModelSerializer + APIView

    path('publish/', PublishView.as_view()),
    re_path('publish/(\d+)/', PublishDetailView.as_view()),

     

    class PublishSerializer(serializers.ModelSerializer):
        class Meta:
            model = Publish
            # fiedls = "__all__"
            fields = ["name", "email"]
    
    
    class PublishView(APIView):
        def get(self, reqeust):
            publishes = Publish.objects.all()
            ps = PublishSerializer(instance=publishes, many=True)
            return Response(ps.data)
    
        def post(self, request):
            print(request.data, type(request.data))
    
            serializer = PublishSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save()  # create方法 将符合条件的数据插入到数据库
                return Response(serializer.data)
            else:
                return Response(serializer.errors)
    
    
    class PublishDetailView(APIView):
    
        def get(self, reqeust, id):
            publish = Publish.objects.get(pk=id)
            ps = PublishSerializer(instance=publish, many=False)
            return Response(ps.data)
    
        def delete(self, request, id):
            Publish.objects.get(pk=id).delete()
            return Response()
    
        def put(self, request, id):
            # post请求
            instance = Publish.objects.get(pk=id)
            serializer = PublishSerializer(data=request.data, instance=instance)
    
            if serializer.is_valid():
                serializer.save()  # instance的 update
                return Response(serializer.data)
    
            else:
                return Response(serializer.errors)

     

     

    ModelSerializer + GenericAPIView

    path('author/', AuthorView.as_view()),
    re_path('author/(?P<pk>\d+)/', AuthorDetailView.as_view()),

     

    from rest_framework.generics import GenericAPIView
    
    
    class AuthorSerializer(serializers.ModelSerializer):
        class Meta:
            model = Author
            fields = ["name", "age"]
    
    
    class AuthorView(GenericAPIView):
        queryset = Author.objects.all()
        serializer_class = AuthorSerializer
    
        def get(self, reqeust):
            # serializer = self.serializer_class(instance=self.queryset,many=True)
            serializer = self.get_serializer(instance=self.get_queryset(), many=True)
    
            return Response(serializer.data)
    
        def post(self, request):
    
            serializer = self.get_serializer(data=request.data)
    
            if serializer.is_valid():
                serializer.save()  # create方法 将符合条件的数据插入到数据库
                return Response(serializer.data)
            else:
    
                return Response(serializer.errors)
    
    
    class AuthorDetailView(GenericAPIView):
        queryset = Author.objects.all()
        serializer_class = AuthorSerializer
    
        def get(self, request, pk):  # url的pk名
            serializer = self.get_serializer(instance=self.get_object(), many=False)
            return Response(serializer.data)
    
        def delete(self, request, pk):
            self.get_object().delete()
            return Response()
    
        def put(self, request, id):
            # post请求
            instance = self.get_object()
            serializer = self.get_serializer(data=request.data, instance=instance)
    
            if serializer.is_valid():
                serializer.save()  # instance的 update
                return Response(serializer.data)
    
            else:
                return Response(serializer.errors)

     

    ModelSerializer + Mixin

    from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin, DestroyModelMixin, \
        UpdateModelMixin
    
    from rest_framework.generics import GenericAPIView
    
    
    class AuthorSerializer(serializers.ModelSerializer):
        class Meta:
            model = Author
            fields = ["name", "age"]
    
    
    class AuthorView(GenericAPIView, ListModelMixin, CreateModelMixin):
        queryset = Author.objects.all()
        serializer_class = AuthorSerializer
    
        def get(self, reqeust):
            return self.list(reqeust)
    
        def post(self, request):
            return self.create(request)
    
    
    class AuthorDetailView(GenericAPIView, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin):
        queryset = Author.objects.all()
        serializer_class = AuthorSerializer
    
        def get(self, request, pk):  # url的pk名
            return self.retrieve(request)
    
        def delete(self, request, pk):
            return self.destroy(request)
    
        def put(self, request, pk):
            return self.update(request)

    基于Mixin的进一步封装

    ListModelMixin, CreateModelMixin, RetrieveModelMixin, DestroyModelMixin, \ UpdateModelMixin

     

    from rest_framework.generics import ListCreateAPIView,RetrieveUpdateDestroyAPIView
    
    
    class AuthorSerializer(serializers.ModelSerializer):
        class Meta:
            model = Author
            fields = ["id","name", "age"]
    
    
    class AuthorView(ListCreateAPIView):
        queryset = Author.objects.all()
        serializer_class = AuthorSerializer
    
    
    class AuthorDetailView(RetrieveUpdateDestroyAPIView):
        queryset = Author.objects.all()
        serializer_class = AuthorSerializer
    

     

  • 哲学基础概念和词条

    基本概念

     

    中国哲学时期划分

     

    西方哲学时期划分

    • 6 th century BC – 6 th century AC 古代哲学
    • 2 nd century AC – 16 th century AC 中古哲学
    • 17 th century AC – 17 th century AC 近代哲学
    • 20 th century 现代西方哲学

     

    基础词汇

    很多基础概念, 随着发展和演化, 其含义也会发生变动.

    • 中国哲学语境下的, 儒道释相互纠缠的.
    • 欧洲大陆语境下的, 不同学派间有或明显, 或不明显差异的.
    • 很多东西从欧洲到马克思, 再从苏联进入到中国时, 附加上唯物主义角度的解读, 其语义再一次发生变化.
      • 典型的词语如: 形而上学.
      • 但有时候我们需要的是经马克思主义解读之前的初始含义.
    • 其他

     

    思辨

    起源

    思辨这个词条来源与亚里士多德, 亚里士多德所研究的学问分为三大类, 而形而上学包含于Speculative philosophy, 而自然哲学和实践哲学则与其并列.

    • Speculative philosophy
      • Logic
      • Metaphysics
    • Natural philosophy
    • Practical philosophy

    韦氏词典的定义是a philosophy professing to be founded upon intuitive or a priori insight and especially insight into the nature of the Absolute or Divine

    即一种声称建立在直觉或先验的洞察力上的哲学,特别是对绝对或神性的洞察力。

    哲学上指运用逻辑推导而进行纯理论,纯概念的思考,毫无客观坐标;

    思考辨析:思辨,首先是说它是一种思考方式,不是与外界相关的,甚至可以不符合逻辑。这是因为思辨方法在其它方法之前,以欧氏几何为例,首先是命题其次是公设公理,那么这些命题就是先于经验也先于逻辑。从定义、公设、公理再推出定理、也就是这个命题的逻辑性解释(如果推衍定理失败,那么命题为假。)。

    思辨方法在一段时间很受推崇,哲学家认为通过思辨,人可以为自然立法,也就是为自然界建立规则。
    近代科学的发展将思辨方法逐渐从主流的地位赶下来,而把科学实验方法推上王座,实践才可使主观见之于客观。

     

    思辨性

    思辨性,即脱离社会实践,在书斋中,通过抽象的思考、推理、论证得出结论的哲学。

     

    思辨能力

     

    思辨能力就是思考辨析能力。所谓思考指的是分析、推理、判断等思维活动;所谓辨析指的是对事物的情况、类别、事理等的辨别分析。 思辨能力首先是一种抽象思维能力。例如,能区分鸡蛋和鸭蛋,这不能算有思辨能力,因为仅凭经验观察就能够区分鸡蛋和鸭蛋;若要搞清楚“鸡和蛋谁先谁后”这个问题,只靠经验观察是不够的,必须有较强的思辨能力才行。

    在日常思维中,对于“鸡和蛋谁先谁后”这个问题,我们可以做以下三个层面的分析:

    第一,如果我们在经验的层面上进行辨析,这个问题就简单了。即某一个鸡蛋所孵化出的小鸡而言,当然是先有蛋,后有鸡;可这只小鸡长大后所生的蛋而言,当然是先有鸡,后有蛋。如果在这个经验的层面上,仍然有人质问:你说先有蛋后有鸡,那鸡蛋又是从何而来?这显然是在玩弄混淆概念的把戏,因为孵化出鸡的那只蛋,与由孵化出的鸡所生的那只蛋,两者辈分不同。

    第二,如果我们从追根问底这个根本的层面上进行分析,“先有鸡还是先有蛋”这个问题就成了一个不恰当的问题。凭什么说它是一个不恰当的问题呢?因为提出这一问题,并准备对回答这个问题的人做进一步质询的人,他必须假定“蛋是由鸡生的”和“鸡是由蛋孵的”这两件事实。可根据生物进化的常识,无论是鸡还是蛋,都是从非鸡非蛋的其它物种遗传、变异而来,如同人是由类人猿演变而来的一样。也就是说,在根源这个意义上,“鸡和蛋谁先谁后”这个问题必须依靠两个不真实的假设才能提出来,故说它是一个不恰当的问题。

    第三,如果我们从逻辑思维这个层面上进行分析,“先有鸡还是先有蛋”的问题通常是指称“恶性循环”这种思维错误的代名词。什么是“恶性循环”?比如,有这样一段议论:“许多人并不了解自己,却试图去了解别人,那是不会成功的。因为连自己都不了解的人是不可能了解别人的。可是,话又说回来,要了解自己也确实困难,因为不了解别人对自己的评价,又怎么能做到自我了解呢?可见,了解别人是了解自我的一面镜子。”这段议论就是“恶性循环”,前一半说“了解自我”是“了解别人”的前提;后一半又说“了解别人”是“了解自我”的前提,这让人听了就会产生“是先有鸡还是先有蛋”的困惑。

    在不同的层面上对同一个问题展开有条理的分析是非常重要的。分析问题首先要把思考的层面区分开,因为在不同的层面上对同一个问题所做出的分析,得出的认识或结论常常不同,甚至完全相反。比如“鸡和蛋谁先谁后”的问题,在经验的层面上是不成问题的问题,它是一个容易解决和不大可能引起争议的问题;在理论或者科学的层面上,它变成了一个不恰当的问题;在逻辑的层面上,它不再是个问题,而是一个指称“恶性循环”这种思维错误的代名词。其次,要注意在同一个层面上分析问题的条理性。这是分析方面的特征,也就是说,分析要讲究层次和条理。 还有一个重要的特征就是说理。说理就是对所做出的分析进行解释和论证,比如,为什么说它是一个简单的问题?为什么说它是一个不恰当的问题?为什么说它不再是一个问题?解释和论证要明白、有力,比如,概念的使用、语言的表达要清楚、准确,理由的陈述、结论的导出要明白、有力。

    简要地说,层次分明、条理清楚的分析,清楚准确、明白有力的说理,即是思辨能力的主要特征。如果一个人在思考问题时能做到条理清楚,说理明白,我们就说其具有较好的思辨能力。

  • 从足球到自己, 再从世界杯和梅西到百年孤独

    熬夜看完了2022年世界杯决赛, 但心里更多想得是足球之外的东西。

    从足球到自己

    首先是自己,  自己不是个足球爱好者, 甚至不是体育爱好者, 也无法理解更快更高更强的含义, 尽管有着一米八身高这种还算行的身高条件。因为个人的身体再快再高再强,  快不过子弹, 高不过科技, 抢不过战斗机与核武器。人和文明的生存与发展, 更加依赖的是智力。

    但是每一个个体自身的身体素质和武力值依然是很重要, 智力和心灵依赖于生命体。强劲的生命体会拥有更加澎湃的智力核心。当然,  这也取决于如何有效开发和利用它们。

    另外一种可能性则是, 即使人类已经远离了原始社会几千甚至上万年, 但有时候在生存的博弈中, 这种最原始的身体武力值是影响生存还是死亡的重要因素。

    稍微回想一下, 童年和少年时期, 自己还是比较热爱运动。小学时期受到一部名字叫一脚定江山, 讲述蹴鞠的电视剧的影响,  把抛柑当作足球,  规则大家事先商量好, 球场就是大门前的大片空地, 当时一度痴迷于。当然这种痴迷终究是如同童年记忆里夏日午后的捕蝉活动那样, 某一个夏天曾经拿着竹竿走遍了附近的一棵棵树,  眼睛寻遍了每一个树枝。但仅限于那一个夏天,  自那之后, 听到再多的鸣蝉, 也不会去捕。在之后, 读书或者工作时的环境与以往不同, 蝉鸣都很久没有听到过了。

    自己的足球故事已然逝去, 与之类似的还有乒乓球和乒乓球,  在家里拼接两张桌子, 迅速就吸引来了周围大大小小的孩子, 每个人轮流上, 每人只有四个球。发挥得好, 暗自窃喜;打得不好, 心底惋惜, 暗下决心。当时就是这般跌宕起伏, 每一个孩子选手如同春秋战国时的王, 每一个球像一场场征战, 每次输球后的总结和改进就如同变法。而球技最好的, 经常长时间位于球桌上的选手, 则是春秋战国时的霸主。而现在去租的球馆打球, 心里平静无波。

    世界杯和梅西

    说完自己, 是时候到了谈谈世界杯和梅西, 以及更多值得去解读的运动员了。说到足球就不读提到梅西, 随着本届世界杯最后一场比赛中点球大战的结束, 至此, 梅西的足球事业已经圆满。 而家庭上也同样如此, 梅西的妻子与他五岁时就开始认识, 而且她还是梅西青训营时的队友的表妹。

    但关于梅西, 值得解读的是不止这些。我喜欢关注童年, 童年的经历和会影响着一个人潜在的价值取向。1997年,梅西被诊断出生长激素缺乏症。但梅西的父亲无法承担每月至少1000美元的治疗费用, 最终依靠巴塞罗那俱乐部, 父亲的努力, 自己的足球天赋, 才解决了这个问题。而从梅西的这段经历, 则可以用来解读出很多事情。

    • 梅西自己无偿出资帮助国家队队友, 因为他受过别人的帮助并将善举传递;
    • 梅西的身高在足球运动员里有些偏矮, 这或许是因为长激素缺乏症;
    • 梅西和巴塞罗那俱乐部之前的纠葛, 我想梅西是感激和爱巴塞罗那的。

    另一个是阿根廷的守门员 – 马丁内斯, 直接帮助队伍在点球大战中战胜对手。他的人生, 同样如同很多南美足球运动员, 年少穷困, 远走他乡, 砥砺艰辛, 最后王者归来功成名就。

    每每想到这里, 我都不由得为梅西, 为阿根廷感到喜悦, 但除了喜悦, 还有为阿根廷和南美洲感到的忧愁和孤独感。

    从世界杯和梅西到南美洲的百年孤独

    • 从高中课文里的《百年孤独》, 再到在大学的冬季时完整阅读《百年孤独》, 心底一次次发问:
    • 究竟什么是百年的“孤独”?
    • 为什么南美洲会“百年孤独”?
    • 如何破除这种孤独?
    • 为什么南美洲, 抑或整个拉丁美洲, 不像中国自清末那样, 从天平天国开始, 到之后的各种反封建、反军阀、反帝国主义侵略, 直至最后建立一个完全属于本族本国人名的国家?
    • 南美洲足球和百年孤独有什么内在联系?

    作为书籍属性的《百年孤独》

    如今大部分人谈到《百年孤独》, 会第一时间想到两点:

    • 多年以后,面对行刑队,奥里雷亚诺· 布恩迪亚上校将会回想起父亲带他去见识冰块的那个遥远的下午
    • 魔幻现实主义

    然后, 国内对其的通行评价则是:

    • “通过小镇马孔多的百年兴衰,反映了拉丁美洲一个世纪以来风云变幻的历史。作品融入神话传说、民间故事、宗教典故等神秘因素,巧妙地糅合了现实与虚幻,展现出一个瑰丽的想象世界”。

    问题正在如此, 我不认为 加西亚·马尔克斯, 仅仅是让读者去领略“拉丁美洲风情和孤独”。 加西亚·马尔克斯一定是想通过百年孤独来表达一些什么。

    从字面上来理解:

    • 百年:是布恩迪亚家族的百年,从第一代布恩迪亚到第七代布恩迪亚之间,这个家族长达一个世纪的历史。
    • 孤独:是布恩迪亚家族的孤独和家族以外的孤独。

    从布恩迪亚家族七代人在小镇马孔多由的故事来理解:

    • 第一代布恩迪亚开拓、创建
    • 第七代布恩迪亚被蚂蚁吃, 小镇被飓风从世界上抹除, 至此消亡

    整个书籍里描写的是故事是一个隐喻:

    1. 布恩迪亚家族代表着印第安三大古老文明
      1. 玛雅文明
      2. 阿兹特克文明
      3. 印加文明
    2. 布恩迪亚家族的创建, 努力
      1. 代表这三种文明的发展
    3. 第七代布恩迪亚被蚂蚁吃, 小镇被飓风从世界上抹除, 至此消亡
      1. 代表着原生的印第安文明, 随着西班牙的侵略, 最终死亡。
    4. 外来者杀死了印第安文明, 印第安原居民所剩无几, 如今这片土地上的人大部分是西班牙和欧洲男性和印第安女性的混血者。印第安文明, 有过抗争, 但最后无法避免死亡。
    5. 真正的印第安已死, 这种文明意义上的死亡就是“百年孤独”。
      1. 纵使如今印第安文明的某些宗教习俗仍然在角落里生存, 但这些有可能以另一种的, 闪耀的, 主流的, 熠熠生辉的, 高昂的生存方式来进行传承。

    百年孤独和足球

    • 印第安文明已死, 这片土地上, 曾经的侵略者和部分原居民混合,  形成一个新的群体
    • 或许这个群体天生就有对曾经侵略者群体的认同感
    • 抑或历史, 种族, 利益, 政治, 阴谋交织下的斗争
      • 比如拉丁美洲的某些国家也曾像布恩迪亚家族那样努力过, 并且成功建立起工业种类比较完善的现代化国家, 但又因为某些因素自废武功, 趋于“孤独”。
    • 最终只剩下一片“孤独”
    • 孤独需要消解
    • 足球死消除“孤独的”药物

    非洲同样孤独, 非洲也同样也尝试用体育和运动来治疗。

     

    足球能消除孤独吗?

    这个问题的真正含义是:足球能拯救, 发展一个文明吗?

    当然不行。

    但足球已经是这片孤独土地上, 能提供给孤独的人们最好的东西了。足球能给予他们以希望。

    • 梅西最终得到了这些美好的东西。

    中国的足球是什么?

    南美洲之所以需要足球, 是因为其文明的劣势与孤独。但足球除了是治疗孤独的药物, 也是神和上位者的锚。

    中国的足球, 不是中国足球队, 是高考, 是各种商业公司, 是考公, 是科研, 是种地。

    所有能让人们过上更好生活的事物, 都是足球。

    印第安文明已死, 印第安的土地上只有足球, 足球是他们很多人的所有。

    华夏文明生生不息, 神州大地上什么都有, 足球只是选择之一。

    • 南美洲的孩子五六岁时开始踢足球, 中国的孩子在读书和上补习班
    • 他们参与比赛, 开始进入各种少年队, 青训队; 我们参加中考, 高考, 进入大学。
    • 他们加入俱乐部;我们进入各种公司。

    批评中国足球队是没有意义的, 中国足球队有一种非常强烈的气场, 你进去一样会被影响。非洲和南美的很多孩子, 他们从小就知道一件事情, 踢好了才能有好的生活。踢不好, 很可能之后要面对的就是贫民窟和死亡。他们用生命在踢球, 我们用商人思维在踢球。

    美国的足球是什么?

    美国的足球,是锚。

    巨大的差异, 需要体育来弥合。

    • 你不需要读书
    • 你不需要把书读得很好
    • 你不需要有深刻的见识
    • 需要读很多书很多见识才能做的事, 我已经安排我的人去做了, 不需要你,
    • 你懂太多, 可能不利于我,
    • 你只需要四肢发达,
    • 你不用担心会饿死, 因为我有发达的农业和生产技术, 我会养着你, 甚至偶尔会给你发钱
    • 你只需要在大选时, 投我的票即可

    当然, 美国的足球还可以是其它很多事物, 所有能让人们过上更好生活的事物, 都是足球。

    比如金融交易员, 程序员, 议员职位, 阴谋家, 金融家, 政客, 掮客等等。

     

    你心中的足球是什么?

    梅西选择足球, 作为他一生奋斗的目标, 并且大圆满。

    你选择的是什么?

     

  • Python 进程, 线程, 协程

    什么是进程?

    进程是对资源进行分配和调度的最小单位,是操作系统结构的基础,是线程的容器(就像是一幢房子,一个空壳子,并不能运动)。

    • 进程是一个实体,每个进程都有自己的地址空间,一般包括文本区域(text region)、数据区域(data region)和堆栈(stack region)
    • 文本区域存储处理器执行的代码;数据区域存储变量和进程在执行期间所使用的动态分配的内存;堆栈区域存储在活动过程中所调用的指令和本地变量
    • 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有在操作系统调用时,他才会成为一个活动的实体:进程。

    什么是线程

    线程被称为轻量级进程,是操作系统能够运算调度的最小单位,线程被包含在进程中,是进程中实际处理单位(就像是房子里的人,人才能动)

    • 一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组 成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,
    • 线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个 进程的其它线程共享进程所拥有的全部资源。

    线程的三种状态

    一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程 在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。

    • 就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;
    • 运行状态是指线程占有处理机正在运行
    • 阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。
    • 每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

     

    什么是协程

    协程又叫微线程,一个程序可以包含多个协程,就好比一个进程包含多个线程。协程的调度完全由用户控制。

    协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。

    直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

    协程和线程的阻塞是有本质区别的。协程的暂停完全由程序控制,线程的阻塞状态是由操作系统内核来进行切换。因此,协程的开销远远小于线程的开销。

     

    相互比较

     

    进程与线程的区别:

      • 进程有自己独占的地址空间,每启动一个进程,系统就需要为它分配地址空间;
      • 而一个进程下所有线程共享该进程的所有资源,使用相同的地址空间,因此CPU在线程之间切换远远比在进城之间切换花费小,而且创建一个线程的开销也远远比开辟一个进程小得多。
      • 线程之间通信更加方便,同一进程下所有线程共享全局变量、静态变量等数据。
      • 而进程之间通信需要借助第三方。
      • 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。
      • 当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。
      • 处理IO密集型任务或函数用线程;
      • 处理计算密集型任务或函数用进程。

    线程和协程的区别:

     

    • 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。
    • 线程进程都是同步机制,而协程则是异步
    • 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

     

    我们常说python中的多线程都是假的,因为无论你启多少个线程,你有多少个cpu, Python在执行的时候会淡定的在同一时刻只允许一个线程运行。

    这又是为什么呢?其实这主要是由于GIL的存在而造成的,详情查阅 http://bayestalk.com/592

     

     

    总结

     

    至此, 不难发现, 在某个角度来讲。它们三者体现的是一种颗粒粗细度的关系。就像切菜,

    • 你用青龙偃月刀来切, 肯定是可以的, 但应该切得比较大块。
    • 用菜刀, 这种对生活而言, 颗粒粗细度刚好。
    • 但如果是做出蔬菜工艺品, 那么可能要用到非常小巧, 锋利的雕刻工具了。

    再比如对于PC而言,

    • 一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
    • 有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
    • 由于每个进程至少要干一件事,所以,一个进程至少有一个线程。
      • 当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

    大部分情况下, Python程序,都是执行单任务的进程,也就是只有一个线程。如果我们要同时执行多个任务怎么办?

     

    • 一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。
    • 还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。
    • 当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

    多进程和多线程的程序涉及到同步、数据共享的问题,编写起来更复杂。

    多线程编程

    多线程编程导致的问题

    import time, threading
    
    # 假定这是你的银行存款:
    balance = 0
    
    def change_it(n):
        # 先存后取,结果应该为0:
        global balance
        balance = balance + n
        balance = balance - n
    
    def run_thread(n):
        for i in range(1000000):  # 循环的次数要设置得足够大。
            change_it(n)
    
    t1 = threading.Thread(target=run_thread, args=(5,))
    t2 = threading.Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    
    
    run_thread(1)
    print(balance)

    以单线程的思路来解释, 就是两个人玩一个非常无聊的游戏, A给B 1块, B又给A 1块, 重复无数次。 那么亿万年后, 他们的财产不会因这个游戏有任何影响。

    • balance 初始值 = 0
    • change_it(n) 函数
      • balance+1
      • balance-1
    • 一加一减, 相抵消
    • 所以最后balance

    但是在多线程中, 而多线程中,所有变量都由所有线程共享, 任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量。

    怎么解决?

    import time, threading
    
    # 假定这是你的银行存款:
    balance = 0
    
    def change_it(n):
        # 先存后取,结果应该为0:
        global balance
        balance = balance + n
        balance = balance - n
    
    def run_thread(n):
        for i in range(10000000): # 循环的次数要设置得足够大。
            change_it(n)
    
    t1 = threading.Thread(target=run_thread, args=(5,))
    t2 = threading.Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    
    
    run_thread(1)
    print(balance)

    结果始终为0, 不同线程间被lock隔绝吗相互独立, 互不干扰。

     

    threading 类

    Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

    启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

     

     

    其他

     

    1. 假设你经营着一家物业管理公司。最初,公司的业务量很小,事事都需要你亲力亲为,给老张家修完暖气管道,立马就去老李家换电灯泡—这叫单线程,所有的工作都得顺序执行。
    2. 后来业务拓展了,你雇用了几个工人,这样你的物业公司就可以同时为多户人家提供服务—这叫多线程,而你是主线程。
      1. 工人们使用的工具是物业管理公司提供的,这些工具由大家共享,并不专属于某一个人—这叫多线程资源共享。
      2. 工人们在工作中都需要管钳,可是管钳只有一把—这叫冲突。解决冲突的办法有很多,如排队等候、等同事用完后微信通知等—这叫线程同步。
      3. 你给工人布置任务—这叫创建线程。布置任务后你还要告诉他,可以开始工作了,不然他会一直停在那儿不动—这叫启动线程(start)。
      4. 如果某个工人(线程)的工作非常重要,你(主线程)也许会亲自监工一段时间;如果不指定时间,则表示你会一直监工到该项工作完成—这叫线程参与(join)。
      5. 业务不忙的时候,你就在办公室喝喝茶。下班时间一到,你群发微信,通知工人该下班了,所有的工人不管手里正在做的工作是否完成,都立刻下班。因此如果有必要,你得避免在工人正忙着的时候发下班通知—这叫线程守护属性设置和管理(daemon)。
      6. 再后来,公司规模扩大了,同时为很多生活社区服务,在每个生活社区都设置了分公司,分公司由分公司经理管理,运营机制和总公司几乎一模一样—这叫多进程,总公司叫主进程,分公司叫子进程。
      7. 总公司和分公司,以及各个分公司之间使用的工具都是独立的,不能借用、混用—这叫进程间不能共享资源。各个分公司之间可以通过专线电话联系—这叫管道。各个分公司之间还可以通过公司公告栏交换信息—这叫进程间共享内存。另外,各个分公司之间还有各种协同手段,以便完成更大规模的作业—这叫进程间同步。
      8. 分公司可以跟着总公司一起下班,也可以把当天的工作全部做完之后再下班—这叫守护进程设置。

     

  • MySql binlog 日志

    前言

     

    日志是mysql数据库的重要组成部分,记录着数据库运行期间各种状态信息。mysql日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。作为开发,我们重点需要关注的是二进制日志(binlog)和事务日志(包括redo log和undo log),本文接下来会详细介绍这三种日志。

     

    binlog

    binlog用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。binlog是mysql的逻辑日志,并且由Server层进行记录,使用任何存储引擎的mysql数据库都会记录binlog日志。

    • 逻辑日志:可以简单理解为记录的就是sql语句。
    • 物理日志:因为mysql数据最终是保存在数据页中的,物理日志记录的就是数据页变更。

    binlog是通过追加的方式进行写入的,可以通过max_binlog_size参数设置每个binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

    binlog使用场景

    在实际应用中,binlog的主要使用场景有两个,分别是主从复制和数据恢复。

    • 主从复制:在Master端开启binlog,然后将binlog发送到各个Slave端,Slave端重放binlog从而达到主从数据一致。
    • 数据恢复:通过使用mysqlbinlog工具来恢复数据。

    binlog刷盘时机

    对于InnoDB存储引擎而言,只有在事务提交时才会记录biglog,此时记录还在内存中,那么biglog是什么时候刷到磁盘中的呢?mysql通过sync_binlog参数控制biglog的刷盘时机,取值范围是0-N:

    • 0:不去强制要求,由系统自行判断何时写入磁盘;
    • 1:每次commit的时候都要将binlog写入磁盘;
    • N:每N个事务,才会将binlog写入磁盘。

    从上面可以看出,sync_binlog最安全的是设置是1,这也是MySQL 5.7.7之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。

    binlog日志格式

    binlog日志有三种格式,分别为 STATMENT、ROW和MIXED。

    在 MySQL 5.7.7之前,默认的格式是STATEMENT,MySQL 5.7.7之后,默认值是ROW。日志格式通过binlog-format指定。

    STATMENT

    基于SQL语句的复制(statement-based replication, SBR),每一条会修改数据的sql语句会记录到binlog中。

    • 优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO, 从而提高了性能;
    • 缺点:在某些情况下会导致主从数据不一致,比如执行sysdate()、slepp()等。

    ROW

    基于行的复制(row-based replication, RBR),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了。

    • 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题;
    • 缺点:会产生大量的日志,尤其是alter table的时候会让日志暴涨

    MIXED

    基于STATMENT和ROW两种模式的混合复制(mixed-based replication, MBR),一般的复制使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog.

    redo log

     

     

    为什么需要redo log

    我们都知道,事务的四大特性里面有一个是持久性,具体来说就是只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态。那么mysql是如何保证一致性的呢?最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面:

    • 因为Innodb是以页为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!
    • 一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机IO写入性能太差!

    因此mysql设计了redo log,具体来说就是只记录事务对数据页做了哪些修改,这样就能完美地解决性能问题了 (相对而言文件更小并且是顺序IO)。

    redo log基本概念

    redo log包括两部分:一个是内存中的日志缓冲(redo log buffer),另一个是磁盘上的日志文件(redo log file)。mysql每执行一条DML语句,先将记录写入redo log buffer,后续某个时间点再一次性将多个操作记录写到redo log file。这种先写日志,再写磁盘的技术就是MySQL里经常说到的WAL(Write-Ahead Logging) 技术。

    在计算机操作系统中,用户空间(user space)下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间(kernel space)缓冲区(OS Buffer)。因此,redo log buffer写入redo log file实际上是先写入OS Buffer,然后再通过系统调用fsync()将其刷到redo log file中,过程如下:

    mysql支持三种将redo log buffer写入redo log file的时机,可以通过innodb_flush_log_at_trx_commit参数配置,各参数值含义如下:

    redo log记录形式

    前面说过,redo log实际上记录数据页的变更,而这种变更记录是没必要全部保存,因此redo log实现上采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志。如下图:

     

    同时我们很容易得知,在innodb中,既有redo log需要刷盘,还有数据页也需要刷盘,redo log存在的意义主要就是降低对数据页刷盘的要求。在上图中,write pos表示redo log当前记录的LSN(逻辑序列号)位置,check point表示数据页更改记录刷盘后对应redo log所处的LSN(逻辑序列号)位置。

    write pos到check point之间的部分是redo log空着的部分,用于记录新的记录;check point到write pos之间是redo log待落盘的数据页更改记录。当write pos追上check point时,会先推动check point向前移动,空出位置再记录新的日志。

    启动innodb的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。因为redo log记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如binlog)要快很多。

    重启innodb时,首先会检查磁盘中数据页的LSN,如果数据页的LSN小于日志中的LSN,则会从checkpoint开始恢复。

    还有一种情况,在宕机前正处于checkpoint的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度,此时会出现数据页中记录的LSN大于日志中的LSN,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。

    redo log与binlog区别

    由binlog和redo log的区别可知:binlog日志只用于归档,只依靠binlog是没有crash-safe能力的。但只有redo log也不行,因为redo log是InnoDB特有的,且日志上的记录落盘后会被覆盖掉。因此需要binlog和redo log二者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。

    undo log

    数据库事务四大特性中有一个是原子性,具体来说就是 原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况。

    实际上,原子性底层就是通过undo log实现的。undo log主要记录了数据的逻辑变化,比如一条INSERT语句,对应一条DELETE的undo log,对于每个UPDATE语句,对应一条相反的UPDATE的undo log,这样在发生错误时,就能回滚到事务之前的数据状态。

    同时,undo log也是MVCC(多版本并发控制)实现的关键.

    本文整理于网络.

  • 什么是CPython GIL?

    什么是Python GIL?

    什么是解释器?

    Python作为一门解释性语言,先把源代码编译为字节码,再放进虚拟机中执行,整个过程是由解释器执行并完成的。类似的还有JavaScript和PHP等。

    但是解释器并不是只有一种,官方的解释器是基于C语言开发的CPython。但是除了CPython,还有基于Java实现的Jython、基于R 语言实现的RPython等等。

     

     

     什么是GIL?

    GIl 是一种互斥锁

    什么是互斥锁?

    在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为” 互斥锁” 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

     

    当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。

    线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

    互斥锁为资源引入一个状态:锁定/非锁定

    某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

     

    为什么会产生GIL?

    GIL的产生是因为CPython的内存管理不安全

    In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. The GIL prevents race conditions and ensures thread safety. A nice explanation of how the Python GIL helps in these areas can be found here. In short, this mutex is necessary mainly because CPython’s memory management is not thread-safe。

    在CPython中,GIL是一个互斥锁,它在任一时刻只允许一个线程对字节码进行执行。这样避免了竞争危害,从而保证了线程安全。简单来说就是,互斥锁之所以存在是因为CPython的内存管理不是“线程安全的”

    由此Python的官方文档可知, 因为因为CPython的内存管理不是“线程安全的”, 所以需要互斥锁, 那么自然会引出另外两个问题。

    GIL的产生是因为CPython的内存管理不安全

    为什么CPython的内存管理不安全?

    Python 第一次发布是在 1991 年,当时的 CPU 都是单核,单核中,多线程主要为了一边做IO,一边做 CPU 计算而设计的,Python 编译器是由 C 语言编写的,因此也叫 CPython,那时候很多编程语言没有自动内存管理的功能,为了实现自动垃圾回收,Python 为每一个对象进行了引用计数,当引用计数为 0 的时候说明该对象可以回收,从而释放内存了,比如:

    >>> import sys
    >>> a = []
    >>> b = a
    >>> sys.getrefcount(a)
    3

    这里 a对象就有 3 个引用,

    • 一个是本身,
    • 一个是变量 b,
    • 一个是 getrefcount 函数的参数,

    如果此时又有一个线程引用了 a,那么引用计数再增加 1,如果某个线程使用了 a 后运行结束,那么引用计数就减少 1,多线程对同一个变量「引用计数」进行修改,就会遇到 race conditions(竞争)。

     

    怎么解决内存管理不安全的问题?

    为了避免 race conditions,最简单有效的办法就是加一个互斥锁。但如果对每一个对象都加锁,有可能引发另一个问题,就是死锁,而且频繁的获取和释放会导致性能下降。

    所以至此, 最简单有效的方法就是加一个解释器锁,线程在执行任何字节码时都先获取解释器锁,这就避免了死锁,而且不会有太多的性能消耗。当时 CPU 都是单核,而且这种 GIL 设计简单,并不会影响性能,因此一直沿用至今天。GIL 存在最主要的原因,就是因为 Python 的内存管理不是线程安全的,这就是 GIL 产生并存在的主要缘由。

    互斥锁的代码实例

    threading模块中定义了Lock类,可以方便的处理锁定:

     # 创建锁
     mutex = threading.Lock()
     ​
     # 锁定
     mutex.acquire()
     ​
     # 释放
     mutex.release()
    • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
    • 如果在调用acquire对这个锁上锁之前,它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止

     

    互斥锁在for循环外面
    import threading
    import time
    
    # 定义一个全局变量
    g_num = 0
    
    
    def test1(num):
     global g_num
     # 上锁,如果之前没有被上锁,那么此时 上锁成功
     # 如果上锁之前 已经被上锁了,那么此时会堵塞在这里,直到 这个锁被解开位置
     mutex.acquire()
     for i in range(num):
         g_num += 1
     mutex.release()   # 解锁
     print("-----in test1 g_num=%d----" % g_num)
    
    
    def test2(num):
     global g_num
     mutex.acquire()   # 上锁
     for i in range(num):
         g_num += 1
     mutex.release()   # 解锁
     print("-----in test2 g_num=%d=----" % g_num)
    
    
    # 创建一个互斥锁,默认是没有上锁的
    mutex = threading.Lock()
    
    
    def main():
     t1 = threading.Thread(target=test1, args=(1000000,))
     t2 = threading.Thread(target=test2, args=(1000000,))
    
     t1.start()
     t2.start()
    
     # 等待上面的2个线程执行完毕....
     time.sleep(2)
    
     print("-----in main Thread g_num = %d---" % g_num)
    
    if __name__ == "__main__":
     main()
    
    #-----in test1 g_num=1000000----
    #-----in test2 g_num=2000000=----
    #-----in main Thread g_num = 2000000---
    互斥锁在for循环里面
    import threading
    import time
    
    # 定义一个全局变量
    g_num = 0
    
    def test1(num):
     global g_num
     for i in range(num):
         mutex.acquire()  # 上锁
         g_num += 1
         mutex.release()  # 解锁
    
     print("---test1---g_num=%d"%g_num)
    
    def test2(num):
     global g_num
     for i in range(num):
         mutex.acquire()  # 上锁
         g_num += 1
         mutex.release()  # 解锁
    
     print("---test2---g_num=%d"%g_num)
    
    # 创建一个互斥锁
    # 默认是未上锁的状态
    mutex = threading.Lock()
    
    # 创建2个线程,让他们各自对g_num加1000000次
    p1 = threading.Thread(target=test1, args=(1000000,))
    p1.start()
    
    p2 = threading.Thread(target=test2, args=(1000000,))
    p2.start()
    
    # 等待计算完成
    while len(threading.enumerate()) != 1:
     time.sleep(1)
    
    print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
    
    
    # ---test1---g_num=1909909
    # ---test2---g_num=2000000
    # 2个线程对同一个全局变量操作之后的最终结果是:2000000

     

    上锁解锁过程
    • 当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。
    • 每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。
    • 线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
    锁的好处
    • 确保了某段关键代码只能由一个线程从头到尾完整地执行
    锁的坏处
    • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
    • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。

     

    死锁代码实例

     

    import threading
    import time
    
    
    #创建互斥锁
    lock = threading.Lock()
    
    
    #根据下标去取值, 保证同一时刻只能有一个线程去取值
    def get_value(index):
    
        # 上锁
        lock.acquire()
        print(threading.current_thread())
        my_list = [3,6,8,1]
        # 判断下标释放越界
        if index >= len(my_list):
            print("下标越界:", index)
    
            return
        value = my_list[index]
        print(f'值是:{value}')
        time.sleep(0.2)
        # 释放锁
        lock.release()
    
    
    if __name__ == '__main__':
        # 模拟大量线程去执行取值操作
        for i in range(30):
            sub_thread = threading.Thread(target=get_value, args=(i,))
            sub_thread.start()

    出现死锁的情况, 程序无法正常停止, 一直在等待

    <Thread(Thread-1, started 30364)>
    值是:3
    <Thread(Thread-2, started 27120)>
    值是:6
    <Thread(Thread-3, started 29632)>
    值是:8
    <Thread(Thread-4, started 29988)>
    值是:1
    <Thread(Thread-5, started 20984)>
    下标越界: 4
    

    避免死锁的代码示例

    # 在合适的地方释放锁
    import threading
    import time
    
    #创建互斥锁
    lock = threading.Lock()
    
    
    #根据下标去取值, 保证同一时刻只能有一个线程去取值
    def get_value(index):
    
        # 上锁
        lock.acquire()
        print(threading.current_thread())
        my_list = [3,6,8,1]
        if index >= len(my_list):
            print("下标越界:", index)
            # 当下标越界需要释放锁,让后面的线程还可以取值
            lock.release()
            return
        value = my_list[index]
        print(value)
        time.sleep(0.2)
        # 释放锁
        lock.release()
    
    
    if __name__ == '__main__':
        # 模拟大量线程去执行取值操作
        for i in range(10):
            sub_thread = threading.Thread(target=get_value, args=(i,))
            sub_thread.start()

     

    <Thread(Thread-1, started 30336)>
    3
    <Thread(Thread-2, started 5920)>
    6
    <Thread(Thread-3, started 28308)>
    8
    <Thread(Thread-4, started 27324)>
    1
    <Thread(Thread-5, started 26840)>
    下标越界: 4
    <Thread(Thread-6, started 30104)>
    下标越界: 5
    <Thread(Thread-7, started 28900)>
    下标越界: 6
    <Thread(Thread-8, started 2676)>
    下标越界: 7
    <Thread(Thread-9, started 28912)>
    下标越界: 8
    <Thread(Thread-10, started 30068)>
    下标越界: 9
    
    Process finished with exit code 0

     

    最后, GIL导致了什么结果?

    正面

    解决安全问题。

    负面

    单线程CPU消耗

    约16% (i5 11代)

     

    import threading
    
    def dead_loop():
        while True:
            pass
    dead_loop()

     

    双线程CPU消耗

    仍然约16%, 而不是32%。

    import threading
    
    def dead_loop():
        while True:
            pass
        # 新起一个死循环线程
        t = threading.Thread(target=dead_loop)
    
        t.start()
        # 主线程也进入死循环
        dead_loop()
        t.join()
    
    dead_loop()

     

    结论:

    • 当双线程时, Cpython 缩小好的CPU的资源, 和单线程时一致。
    • Cpython 当前只能运行一个GIL线程。

     

     

    如果再更进一步, 尝试十个或N个线程, Python的CPU利用率仍然不变。

    但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,为什么Python不行?正是GIL。

    Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

    在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。

    不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

    其它

    • 对于 “Python的GIL” 这种表述是不够严谨, 但也不算错。
    • GIL是相对于Cpython 解释器而言, 而不是Python 语言。
    • Cpython是用来解析Python代码.
    • Cpython是目前最流行的, 主流的解释器.

     

    最后, 只要你愿意, 你可以自己开发一个没有GIL的解释器.