Python的进程、线程和threading模块

(注:本文部分内容摘自互联网,由于作者水平有限,不足之处,还望留言指正。)

       怀念在学校念书的时候,我不小心触碰到了错误,老师会说:你错了;而我却总是倔强得以为自己没错。我的内心是不屑的,直到在真理面前低头。

      毕业后,有时其实快要进入误区(或者刚刚迈入一步),而自己却冥冥中意识到时。问大家:那个啥事我是不是做错了,该怎么抢救一下。“不,你没错”。此时此刻,我的内心却是一顿的紧迫感和无助感。直到。。。直到。。。在错误面前下跪。

      哎呀,生活好讨厌,不过没关系,幸好我机智。我就是我,那个用挖掘机炒菜的男人。

      好了,下面开始我们的表演,show time

      在python界一直有着一个古老的传说,那就是python的多线程是鸡肋,那么这个传说的信度到底有多少呢?如果我们的代码是CPU密集型(涉及到大量的计算),多个线程的代码很有可能是线性执行的,所以这种情况下多线程是鸡肋,效率可能还不如单线程,因为有context switch(其实就是线程之间的切换和线程的创建等等都是需要消耗时间的);但是:如果是IO密集型,多线程可以明显提高效率。例如制作爬虫,绝大多数时间爬虫是在等待socket返回数据。这个时候C代码里是有release GIL的,最终结果是某个线程等待IO的时候其他线程可以继续执行。

一、进程

      程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。执行中的程序是进程。

二、线程

      线程,有时被称为轻量级进程,是程序执行流的最小单元(操作系统最小的调度单位)。线程是属于进程的,我们平时写的简单程序,是单线程的,多线程和单线程的区别在于多线程可以同时处理多个任务。

三、协程

     协程是一种用户态的轻量级线程。如果说多进程对于多CPU,多线程对应多核CPU,那么事件驱动和协程则是在充分挖掘不断提高性能的单核CPU的潜力。我们既可以利用异步优势,又可以避免反复系统调用,还有进程切换造成的开销,这就是协程。协程也是单线程,但是它能让原来要使用异步+回调方式写的非人类代码,可以用看似同步的方式写出来。它是实现推拉互动的所谓非抢占式协作的关键。对于python来说,由于python多线程中全局解释器导致的同时只能有一个线程访问cpu,所以对协程需求就相比于其他语言更为紧迫。

四、进程、线程与协程的关系

     IO密集型应用: 多进程->多线程->事件驱动->协程

     CPU密集型应用:多进程-->多线程

     调度和切换的时间:进程  > 线程 > 协程

 

关于进程与线程的关系,网上有一副很好的解释:

     1. 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。

     2. 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,

         单个CPU一次只能运行一个任务。

     3. 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

     4. 一个车间里,可以有很多工人。他们协同完成一个任务。线程就好比车间里的工人。一个进程可以包括多个线程。

     5. 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用

         这些共享内存。

     6. 可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程

         使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

     7. 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就

        叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

     8. 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,

        只能供给固定数目的线程使用。

     9. 这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道

         必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。

     10. 不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,

           且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

     11. 操作系统的设计,因此可以归结为三点:

          (1)以多进程形式,允许多个任务同时运行;

          (2)以多线程形式,允许单个任务分成不同的部分运行;

          (3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

 

五、threading模块的介绍

      Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。

import threading                  #导入模块
import time
def show(arg):
    time.sleep(3)
    print(arg)

if __name__ == '__main__':
 for i in range(10):
    t = threading.Thread(target=show,args=(i,))     #target是函数返回值,args后依次输入变量,最后一个变量后要加,
    t.start()                      #启动线程
#启动了10个线程

      · start            线程准备就绪,等待CPU调度

      · setName      为线程设置名称

      · getName      获取线程名称

      · setDaemon   设置为后台线程或前台线程(默认)
                如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止
                如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止

      · join              逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义

      · run              线程被cpu调度后自动执行线程对象的run方法

 

加入主线程的情况:

import threading                  #导入模块
import time
def show(arg):
    print('hello%d'%arg)
    time.sleep(3)
    print(arg)

if __name__ == '__main__':
 for i in range(10):
    t = threading.Thread(target=show,args=(i,))     #target是函数返回值,args后依次输入变量,最后一个变量后要加,
    t.start()                      #启动线程
#启动了10个子线程
print('qwer')      #主线程

运行结果:

                                     image

     子线程启动后,直接进行主线程(而不是等子线程执行完再执行主线程)。

     如果要子线程执行完,再执行主线程,需要加入jion函数。

import threading                  #导入模块
import time
l1 = []
def show(arg):
    print('hello%d'%arg)
    time.sleep(3)
    print(arg)

if __name__ == '__main__':
 for i in range(10):
    t = threading.Thread(target=show,args=(i,))     #target是函数返回值,args后依次输入变量,最后一个变量后要加,
    t.start()                      #启动线程
    l1.append(t)
 for t in l1:
      t.join()      #等待每一个线程执行完毕
#启动了10个线程
print('qwer')

 

     介绍一下守护线程deamon:

import threading                  #导入模块
import time
def show(arg):
    print('hello%d'%arg)
    time.sleep(3)
    print(arg)

if __name__ == '__main__':
 for i in range(10):
    t = threading.Thread(target=show,args=(i,))     #target是函数返回值,args后依次输入变量,最后一个变量后要加,
    t.setDaemon(True)              #守护进程,主线程执行完就退出
    t.start()                      #启动线程
#启动了10个线程
print('qwer')

    运行结果:

                                           image

     主线程执行完毕后,子线程就都停掉了,show函数里sleep后面的就不会在执行。

六、线程锁(好像Python3中不用吧,我还要确认一下)

     由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁 - 同一时刻允许一个线程执行操作。

import time
import threading
def addNum():
    global num #在每个线程中都获取这个全局变量
    print('--get num:',num )
    time.sleep(1)
    num  -=1 #对此公共变量进行-1操作
num = 100  #设定一个共享变量
thread_list = []
for i in range(100):
    t = threading.Thread(target=addNum)
    t.start()
    thread_list.append(t)
for t in thread_list: #等待所有线程执行完毕
    t.join()
print('final num:', num )

      正常来讲,这个num结果应该是0, 但在python 2.7上多运行几次,会发现,最后打印出来的num结果不总是0,为什么每次运行的结果不一样呢? 哈,很简单,假设你有A,B两个线程,此时都 要对num 进行减1操作, 由于2个线程是并发同时运行的,所以2个线程很有可能同时拿走了num=100这个初始变量交给cpu去运算,当A线程去处完的结果是99,但此时B线程运算完的结果也是99,两个线程同时CPU运算的结果再赋值给num变量后,结果就都是99。那怎么办呢? 很简单,每个线程在要修改公共数据时,为了避免自己在还没改完的时候别人也来修改此数据,可以给这个数据加一把锁, 这样其它线程想修改此数据时就必须等待你修改完毕并把锁释放掉后才能再访问此数据。

image

 

      说一下四核CPU上多进程是怎么走的:

      对于四核的cpu,如果四个线程打到了四个核,同时启动,却不能执行,必须GIL锁才能执行。所以同一时间只能执行一个线程。加锁的目的:一个线程不被多个CPU执行。

      通俗:四个核相当于四个卫生间,四个工人A、B、C、D(线程)分别进入了四个卫生间,他们想要小便,但是为了害羞(避免多次执行),需要门上加一把锁(GIL锁,系统只有一把锁)。A上锁后,开始小便;小便2秒(大约CPU的100行底层命令);把锁给B,B开始小便(这个时候A、C、D憋住)。然后再给C,再给D。。。。。。所以,PYTHON同一时间只能执行一个线程。

     如果A、B线程属于同一进程,就有可能出错。比如for循环i=i+1(初始是0),

     如果A将i等于0赋值给寄存器,还没执行完,锁的时间到了;这时B得到的i值仍然是0,刚好在锁的时间内执行完,i的值变成1。然后锁到A,而寄存器里的值还是0,得到结果仍然是1。执行两次,结果依旧是1。(Python3.0以上已经解决这个问题。)

     嗯,就写这些吧。

     国庆八天,都在家里抱着电脑不能自拔,

     也是美滋滋吧。Laughing out loud

posted @ 2017-10-05 14:56  云过小桥  阅读(1060)  评论(2编辑  收藏  举报