• 线程
  • 1.对主进程来说,运行完毕指的是主进程代码运行完毕
  • 2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
  • 线程

    ​ 进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

    如果这两个缺点理解比较困难的话,举个现实的例子也许你就清楚了:如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;如果老师在黑板上写演算过程,我们开始记笔记,而老师突然有一步推不下去了,阻塞住了,他在那边思考着,而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。

    现在你应该明白了进程的缺陷了,而解决的办法很简单,我们完全可以让听、写、思三个独立的过程,并行起来,这样很明显可以提高听课的效率。而实际的操作系统中,也同样引入了这种类似的机制——线程。

    ​ 60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大。

    ​ 因此在80年代,出现了能独立运行的基本单位——线程(Threads)

    注意:进程是资源分配的最小单位,线程是CPU调度的最小单位.

    每一个进程中至少有一个线程。 

    ​ 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程

    ​ 线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程 ​ 车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线 ​ 流水线的工作需要电源,电源就相当于cpu

    ​ 所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。

    ​ 多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。

    ​ 例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。

    线程

    threading模块的完全模仿了multiprocess模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍(官方链接

    我们先简单应用一下threading模块来看看并发效果:

    线程创建的两种方式

    方式1

    import time
    from threading import Thread
    
    def sayhi(name):
        time.sleep(2)
        print('%s say hello' %name)
    
    if __name__ == '__main__':
        t=Thread(target=sayhi,args=('太白',))
        t.start()
        print('主线程')
    

    方式2

    import time
    from threading import Thread
    
    class Sayhi(Thread):
        def __init__(self,name):
            super().__init__()
            self.name=name
    
        def run(self):
            time.sleep(2)
            print('%s say hello' % self.name)
    
    if __name__ == '__main__':
        t = Sayhi('太白')
        t.start()
        print('主线程')
    

    线程进程效率对比

    import os
    import time
    
    from threading import Thread
    from multiprocessing import Process
    
    def work():
        print('hello')
    
    if __name__ == '__main__':
        s1 = time.time()
        #在主进程下开启线程
        t=Thread(target=work)
        t.start()
        t.join()
        t1 = time.time() - s1
        print('进程的执行时间:',t1)
        print('主线程/主进程')
    
        s2 = time.time()
        #在主进程下开启子进程
        p=Process(target=work)
        p.start()
        p.join()
        t2 = time.time() - s2
        print('线程的执行时间:', t2)
        print('主线程/主进程')
    

    结果

    hello 进程的执行时间: 0.0008144378662109375 主线程/主进程 hello 线程的执行时间: 0.03743290901184082 主线程/主进程

    同一进程中的线程是资源共享的

    import os
    
    from  threading import Thread
    from multiprocessing import Process
    
    def work():
        global n  #修改全局变量的值
        n=0
    
    if __name__ == '__main__':
    
        n=1
        t=Thread(target=work)
        t.start()
        t.join()   #必须加join,因为主线程和子线程不一定谁快,一般都是主线程快一些,所有我们要等子线程执行完毕才能看出效果
        print('主',n) #查看结果为0,因为同一进程内的线程之间共享进程内的数据
    # 通过一个global就实现了全局变量的使用,不需要进程的IPC通信方法
    

    进程和线程的区别

        进程是最小的内存分配单位
    
        线程是操作系统调度的最小单位
    
        线程被CPU执行了
    
        进程内至少含有一个线程
    
        进程中可以开启多个线程 
    
        开启一个线程所需要的时间要远小于开启一个进程 
    
        全局变量在多个线程之间是共享的
    

    其他常用方法

    Thread实例对象的方法
      # getName(): 返回线程名。
      # setName(): 设置线程名。
    
    threading模块提供的一些方法
      # threading.currentThread(): 返回当前的线程对象。
      # threading.get_ident():  获取线程id
    

    简单示例

    import threading, time
    
    def run(n):
        print('-'*30)
        print("Pid is :%s" % threading.get_ident())  # 返回线程pid
    
    if __name__ == '__main__':
        threading.main_thread().setName('Chengd---python')    #自定义线程名
        for i in range(3):
            thread_alive = threading.Thread(target=run, args=(i,))
            thread_alive.start()
        thread_alive.join()
        print('\n%s thread is done...'% threading.current_thread().getName())    #获取线程名
    

    结果

    Pid is :140410400802560
    ------------------------------
    ------------------------------
    Pid is :140410392348416
    Pid is :140410383894272
    
    Chengd---python thread is done...
    

    join方法

    用来等待子线程运行结束。

    import time
    from threading import Thread
    
    def sayhi(name):
        time.sleep(2)
        print('%s say hello' %name)
    
    if __name__ == '__main__':
        t=Thread(target=sayhi,args=('太白',))
        t2=Thread(target=sayhi,args=('alex',))
        t.start()
        t2.start()
        t.join()  #因为这个线程用了join方法,主线程等待子线程的运行结束
    
        print('主线程')
        print(t.is_alive())  #所以t这个线程肯定是执行结束了,结果为False
        print(t2.is_alive()) #有可能是True,有可能是False,看子线程和主线程谁执行的快
    

    结果

    第一次:
    太白 say hello
    alex say hello
    主线程
    False
    False
    第二次
    太白 say hello
    主线程
    False
    True
    alex say hello
    

    守护线程

    无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。需要强调的是:运行完毕并非终止运行

    1.对主进程来说,运行完毕指的是主进程代码运行完毕

    2.对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

    详细解释

    主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
    
    主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束,因为进程执行结束是要回收资源的,所有必须你里面的非守护子线程全部执行完毕。
    

    守护线程使用示例

    from threading import Thread
    import time
    def sayhi(name):
        time.sleep(2)
        print('%s say hello' %name)
    
    if __name__ == '__main__':
        t=Thread(target=sayhi,args=('taibai',))
        t.setDaemon(True) #必须在t.start()之前设置
        t.start()
    
        print('主线程')
        print(t.is_alive())
        '''
        主线程
        True
        '''
    

    线程同步

    互斥锁(同步锁)

    多个线程抢占资源时会造成数据混乱的问题,可以通过加锁来解决,看代码:

    import os,time
    
    from threading import Thread,Lock
    
    def work():
        global n
        lock.acquire() #加锁
        temp=n
        time.sleep(0.1)
        n=temp-1
        lock.release()
    
        with lock:
            temp=n
            time.sleep(0.1)
            n=temp-1
    if __name__ == '__main__':
        lock=Lock()
        n=100
        l=[]
        for i in range(100):
            t=Thread(target=work)
            l.append(t)
            t.start()
        for t in l:
            t.join()
    
        print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全
    

    加锁之后,数据不会出现混乱的问题了,这种情况称之为线程安全。

    锁的单例模式

    创建锁的时候,我们还可以采用单例模式,看下面的示例:

    from threading import Thread,Lock
    
    class SingleTon:
        __instance = None
        lock = Lock()
    
        def __new__(cls, *args, **kwargs):
            if cls.__instance:
                return cls.__instance
            with cls.lock:
                if not cls.__instance:
                    cls.__instance = super().__new__(cls)
                return cls.__instance
    
    def fun():
        s = SingleTon()
        print(id(s))
    
    for i in range(20):
        t1 = Thread(target=fun,)
        t1.start()
    

    死锁

    ​ 进程也有死锁与递归锁,进程的死锁和线程的是一样的,而且一般情况下进程之间是数据不共享的,不需要加锁,由于线程是对全局的数据共享的,所以对于全局的数据进行操作的时候,要加锁。

    ​ 所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁:

    现象1:将自己锁死

    import time
    from threading import Lock
    
    mutexA=Lock()
    mutexA.acquire()
    mutexA.acquire()
    print(123)
    mutexA.release()
    mutexA.release()
    

    现象2:锁嵌套引起的死锁

    import time
    from threading import Thread,Lock
    
    mutexA=Lock()
    mutexB=Lock()
    
    class MyThread(Thread):
        def run(self):
            self.func1()
            self.func2()
    
        def func1(self):
            mutexA.acquire()
            print('\033[41m%s 拿到A锁>>>\033[0m' %self.name)
            mutexB.acquire()
            print('\033[42m%s 拿到B锁>>>\033[0m' %self.name)
            mutexB.release()
            mutexA.release()
    
        def func2(self):
            mutexB.acquire()  
            print('\033[43m%s 拿到B锁???\033[0m' %self.name)
            time.sleep(2)
            #分析:当线程1执行完func1,然后执行到这里的时候,拿到了B锁,线程2执行func1的时候拿到了A锁,那么线程2还要继续执行func1里面的代码,再去拿B锁的时候,发现B锁被人拿了,那么就一直等着别人把B锁释放,那么就一直等着,等到线程1的sleep时间用完之后,线程1继续执行func2,需要拿A锁了,但是A锁被线程2拿着呢,还没有释放,因为他在等着B锁被释放,那么这俩人就尴尬了,你拿着我的老A,我拿着你的B,这就尴尬了,俩人就停在了原地
    
            mutexA.acquire()
            print('\033[44m%s 拿到A锁???\033[0m' %self.name)
            mutexA.release()
    
            mutexB.release()
    
    if __name__ == '__main__':
        for i in range(10):
            t=MyThread()
            t.start()
    

    递归锁

    死锁的解决方法:递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。 这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

    现象1的解决

    import time
    from threading import RLock as Lock
    
    mutexA=Lock()
    mutexA.acquire()
    mutexA.acquire()
    print(123)
    mutexA.release()
    mutexA.release()
    

    现象2的解决

    import time
    from threading import Thread,RLock
    fork_lock = noodle_lock = RLock()
    
    def eat1(name):
        noodle_lock.acquire()
        print('%s 抢到了面条'%name)
        fork_lock.acquire()
        print('%s 抢到了叉子'%name)
        print('%s 吃面'%name)
        fork_lock.release()
        noodle_lock.release()
    
    def eat2(name):
        fork_lock.acquire()
        print('%s 抢到了叉子' % name)
        time.sleep(1) 
        noodle_lock.acquire()
        print('%s 抢到了面条' % name)
        print('%s 吃面' % name)
        noodle_lock.release()
        fork_lock.release()
    
    for name in ['taibai','wulaoban']:
        t1 = Thread(target=eat1,args=(name,))
        t1.start()
    for name in ['alex','peiqi']:
        t2 = Thread(target=eat2,args=(name,))
        t2.start()
    

    GIL锁和锁的区别

    GIL VS Lock
    
      机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 
    
      首先我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据
    
      然后,我们可以得出结论:保护不同的数据就应该加不同的锁。
    
      最后,问题就很明朗了,GIL Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock
    
      过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限
    
      线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果
    
      既然是串行,那我们执行
    
      t1.start()
    
      t1.join
    
      t2.start()
    
      t2.join()
    
      这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码
    

    线程队列

    线程之间的通信我们列表行不行呢,当然行,那么队列和列表有什么区别呢?

    ​ queue队列 :使用import queue,用法与进程Queue一样

    ​ queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.

    先进先出

    import queue #不需要通过threading模块里面导入,直接import queue就可以了,这是python自带的
    #用法基本和我们进程multiprocess中的queue是一样的
    q=queue.Queue()
    q.put('first')
    q.put('second')
    q.put('third')
    # q.put_nowait() #数据多了就报错,可以通过try来搞
    print(q.get())
    print(q.get())
    print(q.get())
    # q.get_nowait() #没有数据就报错,可以通过try来搞
    '''
    结果(先进先出):
    first
    second
    third
    '''
    

    队列是线程安全的,不会出现多个线程抢占同一个资源或数据的情况。

    上一篇:进程

    下一篇:协程