多任务-线程

线程简介
  • 线程
    • 线程是操作系统进行资源调度的基本单位
  • 线程与进程的区别 
    • 线程 :
      • 线程间共享进程资源;  (当多个任务需要同时执行且需要实时的共享数据的时候优先选用多线程)
      • 操作系统进行资源调度的基本单位,所有线程占用同一块内存空间
      • 单个线程拥有的资源更小,线程切换代码更小。
      • 线程的独立性并不是很好,主线程会等待子线程都结束才会结束,一个子线程崩溃所有线程都可能会崩溃
    • 进程: 
      • 进程之间的数据是不共享的,需要使用列队进行数据的传递
      • 是操作系统进程资源分配的基本单位, 占用不同的内存空间,
      • 独立性比较好,单个进程崩溃不影响其他进程的运行
创建子线程--threading.Tread类
  • 创建子线程的方法(第一种)
    • thd = threading.Thread(target=None, args=())
  • Tread类__init__方法常用的参数
    • target 表示线程执行的入口函数代码
    • args 表示给入口函数传递的函数参数
  • Thread类常用的实例方法
    • is_alive()    判断线程是否处于运行状态
    • start()   判断子线程的创建和执行      “start()方法有两个主要步骤1.启动子进程 2.执行run方法(run()方法会去默认调用target参数传入的函数)”
    • run()    子线程默认执行的线程入口代码
    • join()    阻塞等待线程结束  “在主线程中默认会等待所有的子线程结束后再结束程序,所以一般情况下不需要使用join()”
  • 创建子线程的方法(第二种)
    • 1.import threading 模块
    • 2.继承threading.Thread类
    • 3.重写子类中的run方法
    • 4.创建子线程实例(通过该方法创建的实例不需要传入target,默认会调用重写的父类方法,重写的run方法需要的参数而已通过实例属性获取。)
    • 例子:
  • 线程间的数据竞争与同步的概念*
    • 数据竞争描述:
      • 当多个线程对一个全局变量进行操作的时候,会产生资源竞争的情况,从而导致操作的结果不正确
    • 多线程同步的描述:
      • 在多线程的工作模式下,使各个线程有序的协作,有序的处理数据,避免数据竞争等异常现象的发生
    • 线程安全:
      • 由于线程之间的数据操作经常是不安全的,所以通常需要对数据加上保护措施,保证数据操作的逻辑正确性
    • 注意: 
      • Queue 是线程安全的,线程间的操作不会产生数据竞争的情况
  • 实现多线程同步的方法---队列(Queue)
    • 介绍Queue
      • Queue是python中的标准库,可以直接import Queue引用,队列是线程间最常用的交换数据的形式。
    • Queue的作用
      • python下多线程对于资源的处理,加锁是个重要的环节,因为python原生的list,dict等,都是not thread safe的,所以使用Queue,可以不用手动添加资源锁就能解决资源竞争的问题。
      • 在使用Queue进行参数传递的时候,只有Queue列队中有数据,线程就可以获得并执行任务,不需要等待其他线程对数据的处理并传递,更方便数据的传递和处理;
    • 使用方法
      • 创建队列对象
        • from queue import Queue
        • queue = Queue(maxsize = n)        创建一个最大容量为n的queue列队
      • 将一个数值放在队列中
        • queue.put( )      向列队中添加任务,列队满后会阻塞等待
        • myqueue.put_nowait( )    向列队中添加任务,列队满后会报错
      • 将一个数值从队列中取出
        • queue.get( )     从列队中取出任务
        • queue.get([block[, timeout]])        从列队中取出任务,超时后会报错
        • 备注:从列队取出任务后,列队计数并不会减一,需要标记任务task_done
      • 任务完成,列队计数减一
        • queue.task_done( )      列队计数减一
      • 列队任务计数不为0时,阻塞进程
        • Queue.join( )                 列队如果不为空,阻塞主线程
      • 其他查询列队状态的方法
        • Queue.qsize()              返回队列当前计数大小
        • Queue.empty()           如果队列为空,返回True,反之False
        • Queue.full()                  如果队列满了,返回True,反之False
        • Queue.full                    与 maxsize 大小对应
  • 实现多线程同步的方法---互斥锁(threading.Lock类)
    • 互斥锁的概念
      • 有我不能有你,有你一定不能有我;即多个线程里面如果一个线程上了锁,其他使用该锁的线程则会发生阻塞,直到该线程解锁
    • 使用方法
      • 安装锁
        • import threading 
        • mutex_lock = threading.Lock()
      • 上锁
        • mutex_lock.acquire(True, timeout)      其中的参数默认为True,表示如果没有获取到锁会一直阻塞等待,直到获取到互斥锁的资源,并且返回True ; False 表示如果没有获取到锁的资源不会阻塞,会返回False  ;  timeout为阻塞时间, 默认为无穷大,只能在锁为阻塞状态下使用
      • 解锁
        • mutex_lock.release()
    • 互斥锁两种状态的图示
    • 互斥锁的优点与缺点
      • 优点:能够使多线程同步运行保证结果正确
      • 缺点:丧失了并发优势可能会造成deadlock 死锁
    • 死锁
      • (场景一)某个任务对锁不正常的请求/释放导致的多任务相互阻塞等待的情况
        • 例子:(一个线程中没有对锁的资源进行释放)
          •    
      • (场景二)在线程共享资源的时候,如果连个线程分别占有一部分资源的时候,两个线程分别占有一部分资源并且同时等待对方的资源导致死锁    “临界资源”同一时间点只允许一个任务访问的资源
        • 例子:(双方都等待对方的资源释放)
          •   
    • 避免死锁的方法
      • 正确的请求锁,释放锁
      • 为锁设置非阻塞/超时时间等
      • 合理的临界资源  管理策略预防  --银行家算法

Cpython的GIL (global interpreter lock)
  • GIL定义
    • 在CPython中,有一个全局的解释器锁,它可以防止多个本机线程同时执行Python的编译器。这个锁是必需的,主要是因为Cpython的内存管理不是线程安全的。(然而,由于吉尔存在,其他的特性已经发展到依赖于它的保证。)   因此,才cpython中多线程不存在利用多核的情况,只能轮流等待cpu执行每一个线程,因此一个进程中的所有线程都不是并行执行的
  • cpython的GIL问题
    • 多个线程间需要操作共享的系统资源, 由于cpython的内存管理不是线程安全的, 所以所有的”线程”在执行时都需要先获取GIL
    • GIL类似于 Lock,所以cpython原理上不存在多线程的,多线程并不存在并行执行的情况
    • 越过GIL限制的方法
      • 用C/C++编写线程代码
        • void DeadLoop()
        • {    
          • while(1) ;
        • }
      • 编译为动态库让python调用gcc loop.c -shared -o libgil.so
      • Python程序中调用
        • from ctypes import * ; 
        • import threading
        • lib = cdll.LoadLibrary("./libgil.so")
        • t= threading.Thread(target=lib.Deadloop)
        • t.start()
        • lib.Deadloop()
Daemon线程(守护线程)
  • 线程退出步骤:
    • 主线程运行结束时,会判断子线程的daemon值,主线程会等待所有daemon为False的线程结束后才退出,如果剩下的线程daemon值都为True,那么会强制退出所有子线程,并结束进程。
  • Deamon线程:
    • 默认线程是普通类型的,当设置线程为守护线程时,当程序的最后一个普通线程退出时,整个进程都将会退处出
  • 将线程设置为Daemon的方法
    • 第一种写法:线程名.Daemon = True
    • 第二种写法:线程名.setDaemon(True)
在子进程中创建子线程应该注意的点
  • 需要注意:与主进程会默认等待子线程不同,子进程中不管创不创建子线程,如果子进程运行结束,那么子进程包括所有的子线程都会一起结束,并释放资源
  • 例如下面的代码,子进程如果不加入子线程的join()阻塞等待,主进程如果先运行完将会直接释放资源。
        
                    
线程池的使用方法
  • 使用步骤
    • 导入线程池模块
      • from multiprocessing.dummy import Pool as ThreadPool     ---- 
    • 创建参数列表
      • url_list = ['url1', 'url2', 'url3', ... ]
    • 创建一个线程池,20个线程数
      • pool = ThreadPool(20)  
    • 第一种向线程池中添加任务的方法(轮流执行情况)
      • pool.map(func, args_list)
      • pool.apply(func, args=( ))
    • 第二种向线程池中添加任务的方法(异步执行的情况)
      • pool.apply_async(func, args=( )) 
      • pool.map_async(func, args_list( ))
    • 关闭线程池,不能再添加任务
      • pool.close()
    • 阻塞线程池
      • pool.join()
拓展-threading的几个函数、/线程间的通信/线程池
  • 返回当前线程所在的主线程的Thread的实例对象
    • threading.main_thread()
  • 返回当前所在线程实例对象
    • threading.current_thread()
  • 返回存活线程的数量
    • threading.active_count()








刘小恺(Kyle) wechat
如有疑问可联系博主