线程

线程背景

进程

    之前我们已经了解了操作系统中进程的概念,程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。在多道编程中,我们允许多个程序同时加载到内存中,在操作系统的调度下,可以实现并发地执行。就是这样的设计,大大提高了CPU的利用率。进程的出现让每个用户感觉到自己独享CPU,因此,进程就是为了在CPU上实现多道编程而提出的。

有了进程为什么要有线程

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

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

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

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

线程的出现

    60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端:

  • 一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程
  • 二是由于对称多处理机(SMP)出现,可以满足多个运行单位,而多个进程并行开销过大
 因此在80年代,出现了能独立运行的基本单位——线程(Threads)
 注意:
  • 进程是资源分配的最小单位,线程是CPU调度的最小单位.
  • 每一个进程中至少有一个线程。 

 进程和线程的关系

    线程与进程的区别可以归纳为以下4点:

  • 地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
  • 通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
  • 调度和切换:线程上下文切换比进程上下文切换要快得多。
  • 在多线程操作系统中,进程不是一个可执行的实体。

线程的特点

  在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
  • 轻型实体
  线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
  线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
TCB
  • 独立调度和分派的基本单位。
  在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
  • 共享进程资源。
  在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
  • 可并发执行。
  在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

线程的实际应用场景

    开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。

内存中的线程

    多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。

 而对一台计算机上多个进程,则共享物理内存、磁盘、打印机等其他物理资源。多线程的运行和多进程的运行类似,是cpu在多个线程之间的快速切换。

 不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如迅雷会和QQ抢资源。而同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的,一个线程干死了另外一个线程的内存,那纯属程序员脑子有问题。

 类似于进程,每个线程也有自己的堆栈,不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。

 线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:

  • 父进程有多个线程,那么开启的子线程是否需要同样多的线程
  • 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?

 因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。

用户级线程和内核级线程

    线程的实现可以分为两类:用户级线程(User-Level Thread)和内核级线程(Kernel-Level Thread),后者又称为内核支持的线程或轻量级进程。在多线程操作系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。 

用户级线程

    内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu。

 

    在用户空间模拟操作系统对进程的调度,来调用一个进程中的线程,每个进程中都会有一个运行时系统,用来调度线程。此时当该进程获取cpu时,进程内再调度出一个线程去执行,同一时刻只有一个线程执行。

内核级线程

    切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程就是这样的。

用户级与内核级线程的对比

  1)内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
  2)用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
  3)用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
  4)在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
  5)用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。
用户级线程和内核级线程的区别
优点:当有多个处理机时,一个进程的多个线程可以同时执行。
缺点:由内核进行调度。
内核线程的优缺点
优点:
线程的调度不需要内核直接参与,控制简单。
可以在不支持线程的操作系统中实现。
创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
允许每个进程定制自己的调度算法,线程管理比较灵活。
线程能够利用的表空间和堆栈空间比内核级线程多。
同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。
缺点:
资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用
用户级线程的优缺点

混合实现

    用户级与内核级的多路复用,内核同一调度内核线程,每个内核线程对应n个用户线程

linux操作系统的NPTL

历史
在内核2.6以前的调度实体都是进程,内核并没有真正支持线程。它是能过一个系统调用clone()来实现的,这个调用创建了一份调用进程的拷贝,跟fork()不同的是,这份进程拷贝完全共享了调用进程的地址空间。LinuxThread就是通过这个系统调用来提供线程在内核级的支持的(许多以前的线程实现都完全是在用户态,内核根本不知道线程的存在)。非常不幸的是,这种方法有相当多的地方没有遵循POSIX标准,特别是在信号处理,调度,进程间通信原语等方面。

很显然,为了改进LinuxThread必须得到内核的支持,并且需要重写线程库。为了实现这个需求,开始有两个相互竞争的项目:IBM启动的NGTP(Next Generation POSIX Threads)项目,以及Redhat公司的NPTL。在2003年的年中,IBM放弃了NGTP,也就是大约那时,Redhat发布了最初的NPTL。

NPTL最开始在redhat linux 9里发布,现在从RHEL3起内核2.6起都支持NPTL,并且完全成了GNU C库的一部分。

 

设计
NPTL使用了跟LinuxThread相同的办法,在内核里面线程仍然被当作是一个进程,并且仍然使用了clone()系统调用(在NPTL库里调用)。但是,NPTL需要内核级的特殊支持来实现,比如需要挂起然后再唤醒线程的线程同步原语futex.

NPTL也是一个1*1的线程库,就是说,当你使用pthread_create()调用创建一个线程后,在内核里就相应创建了一个调度实体,在linux里就是一个新进程,这个方法最大可能的简化了线程的实现。

除NPTL的1*1模型外还有一个m*n模型,通常这种模型的用户线程数会比内核的调度实体多。在这种实现里,线程库本身必须去处理可能存在的调度,这样在线程库内部的上下文切换通常都会相当的快,因为它避免了系统调用转到内核态。然而这种模型增加了线程实现的复杂性,并可能出现诸如优先级反转的问题,此外,用户态的调度如何跟内核态的调度进行协调也是很难让人满意。
介绍

Python与线程

全局解释器锁GIL

    Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。
 对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。

 在多线程环境中,Python 虚拟机按以下方式执行:

  • 设置 GIL;
  • 切换到一个线程去运行;
  • 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
  • 把线程设置为睡眠状态;
  • 解锁 GIL;
  • 再次重复以上所有步骤。

 在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。

Python线程模块的选择

    Python提供了几个用于多线程编程的模块,包括thread、threading和Queue等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持,threading提供了更高级别、功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
 避免使用thread模块,因为更高级别的threading模块更为先进,对线程的支持更为完善,而且使用thread模块里的属性有可能会与threading出现冲突;其次低级别的thread模块的同步原语很少(实际上只有一个),而threading模块则有很多;再者,thread模块中当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作,至少threading模块能确保重要的子线程退出后进程才退出。 

 thread模块不支持守护线程,当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。而threading模块支持守护线程,守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求它就在那等着,如果设定一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。

threading模块

    multiprocessing模块完全模仿了threading模块的接口,二者在使用层面,有很大的相似性

    官网网址:https://docs.python.org/3/library/threading.html?highlight=threading#

线程的创建与方法

线程的创建 

    通过Thread创建:

from threading import Thread


def func(n):
    print("-----func<%s>-----" % n)


if __name__ == '__main__':
    t = Thread(target=func, args=(1,))
    t.start()
    print("主线程代码结束")
创建线程

    通过类创建:

from threading import Thread


class MyThread(Thread):
    def __init__(self, num):
        super().__init__()
        self.num = num

    def run(self):
        print("-----%s-----" % self.num)


if __name__ == '__main__':
    t = MyThread(1)
    t.start()
创建线程

Thread类的方法

join

    主线程执行到t.join(),会等待子线程结束,子线程结束后再执行后面的代码

其他方法

Thread实例对象的方法
  # isAlive(): 返回线程是否活动的。
  # getName(): 返回线程名。
  # setName(): 设置线程名。

threading模块提供的一些方法:
  # threading.currentThread(): 返回当前的线程变量。
  # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
方法
import threading
import time
from threading import current_thread, Thread


def func():
    time.sleep(1)

    print('我是子线程,名字是', current_thread().getName())  # 获取线程的名字
    print('我是子线程,id是', current_thread().ident)    # 打印id


if __name__ == '__main__':

    for i in range(2):
        t = Thread(target=func)
        t.start()

    print(threading.enumerate())  # 正在运行的线程的list
    print(threading.activeCount())  # 正在运行的线程数量

    print('主线程代码结束')
代码示例

 多线程的执行顺序

    默认情况下,主线程等待非守护线程结束后才结束,主线程代码结束并不代表主线程结束。因为主线程结束意味着进程结束,此时进程的整体资源都将被回收

多线程和多进程对比

进程和线程中内存空间存储内容

  • 进程:导入的模块、执行的python文件的文件所在位置、内置函数、.py文件里的代码和全局变量等
  • 线程:自己的堆栈(类似于一个列表,后进先出)和寄存器,里面存着自己的线程的变量,操作(add)等等,占用的空间很小

pid

    同一进程内的多个线程pid相同

from threading import Thread
import os
import time


def func():
    time.sleep(1)
    print("我是子线程,我的pid是", os.getpid())


if __name__ == '__main__':
    t = Thread(target=func)
    t.start()

    print("我是主线程,我的pid是", os.getpid())
    t.join()
    print("主线程代码结束")
pid

多进程与多线程的开启效率比较

   创建进程所需时间远大于创建线程所需时间,所以同一情况下,多线程的开启效率远高于多进程。

import time
from threading import Thread
from multiprocessing import Process


def func():
    time.sleep(0.1)
    print("hello")


if __name__ == '__main__':
    # 线程
    t_lst = []
    t_s_t = time.time()  # 开始时间
    for i in range(100):
        t = Thread(target=func)  # 创建线程
        t_lst.append(t)
        t.start()

    [el.join() for el in t_lst]  # 等待子线程结束

    t_e_t = time.time()  # 结束时间
    t_diff_t = t_e_t - t_s_t

    # 进程
    p_lst = []
    
    p_s_t = time.time()  # 开始时间
    for i in range(100):
        p = Process(target=func)  # 创建进程
        p_lst.append(p)
        p.start()
        
    [el.join() for el in p_lst]  # 等待子进程结束
    p_e_t = time.time()  # 结束时间

    p_diff_t = p_e_t - p_s_t  

    print("进程时间>>>", p_diff_t)
    print("线程时间>>>", t_diff_t)
多进程和多线程开启效率比较

多线程与多进程数据共享比较

  • 线程:多个线程内部有自己的数据栈,数据不共享,而全局变量在多个线程间是共享的
  • 进程:进程间数据不共享。
from threading import Thread

num = 100


def func():
    global num
    num -= 1


if __name__ == '__main__':
    t = Thread(target=func)
    t.start()

    t.join()
    print("主线程num:", num)
多线程间共享全局变量

 总结:

  • 进程是最小的内存分配单位,线程是操作系统调度的最小单位,线程被CPU执行
  • 进程内至少含有一个线程,一个进程内可以开启多个线程
  • 开启一个线程所需要的时间要远小于开启一个进程所需的时间
  • 多个线程内部有自己的数据栈,里面数据不共享。全局变量在多个线程之间是共享的

守护线程

    守护线程随着主线程结束而结束

import time
from threading import Thread


def func1():
    time.sleep(3)
    print("func1结束")


def func2():
    time.sleep(2)
    print("func2结束")


if __name__ == '__main__':
    t1 = Thread(target=func1)
    t2 = Thread(target=func2)
    t1.daemon = True   # 设置t1为守护线程

    t1.start()
    t2.start()

    print("主线程代码结束")  # func1结束不会打印,因为运行完func2结束,此时非守护线程运行完了,主线程结束,而此时守护进程还未运行完,
                           # 被迫结束,所以func1不会打印
代码示例

GIL锁

  • 在Cpython解释器中,同一进程下开启的多个线程,同一时刻只能有一个线程执行,无法利用多核优势。
  • GIL并不是python的特性,而是实现Cpython解释器时所引入的一个概念。不是每个解释器都有GIL锁。
  • 剖析GIL对python多线程的影响:http://www.dabeaz.com/python/UnderstandingGIL.pdf
"""
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple 
native threads from executing Python bytecodes at once. This lock is necessary mainly 
because CPython’s memory management is not thread-safe. (However, since the GIL 
exists, other features have grown to depend on the guarantees that it enforces.)
"""
官方说明

 

1) GIL

    GIL本质是一把互斥锁,都是将并发运行变成串行,以此来控制同一时内共享数据只能被一个任务所修改,进而保证数据安全。GIL保证的是解释器级别的数据安全

    对于Cpython解释器,GIL锁加在python代码进解释器接口处

 


 

2) python程序的执行流程

    在一个python的进程内,不仅有主线程及由该主线程开启的其他子线程,还有解释器开启的垃圾回收等解释器级别的线程。python文件执行过程为:

  • 先将python解释器代码加载至内存,然后将python代码加载至内存,两者在同一进程内
  • Cpython解释器分两部分:编译和虚拟机。线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去编译成.pyc文件(c语言字节文件),再经虚拟机转换成二进制供cpu计算

    解释器的代码是所有线程共享的,所以所有线程都能访问到解释器的代码去执行。如果无GIL锁,可能出现一个线程正在执行x=100的同时,而拉圾回收线程执行的是回收100的操作,此时就会导致数据不安全。

因为Python解释器会自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它做一次全局轮询看看哪些内存数据是可以被清空的,此时你程序里的线程和py解释器的线程是并发运行的,假设你的线程删除了一个变量,而py解释器的垃圾回收线程在清空这个变量的过程中,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,  这可以说是Python早期版本的遗留问题。
垃圾回收解释

 


 

3) GIL与多线程

    有了GIL的存在,对于多线程,同一时刻同一进程中只有一个线程被执行

    在线程执行时,如遇到I/O操作,线程会被切换,同时GIL锁被回收,其他线程抢占GIL锁获取解释器代码权限进而被执行。所以对于计算密集型,多线程并不能提高效率,而对于I/O密集型,多线程可提高效率

    应用:

  • 多线程用于I/O密集型,如socket,爬虫,Web
  • 多进程用于计算密集型,如金融分析

 

同步锁——Lock

    多线程间共享全局变量,如果并发执行,可能造成数据错乱,采用同步锁,可保证数据安全

    特点:牺牲了执行效率,保证了数据安全

1) 同步锁的使用

from threading import Lock

lock = Lock()  # 创建锁

lock.acquire()  # 上锁

lock.release()  # 释放锁
同步锁的使用

 


 

2) 同步锁 VS GIL锁,join

  • 同步锁:锁住修改共享数据的部分代码
  • GIL锁:线程抢的是GIL锁,GIL锁相当于线程的执行权限,只有拿到了GIL锁,才有机会拿到同步锁
  • join:等待指定线程执行完毕,如果所有子线程采用join方式,即变成整体串行,相当于锁住各线程所有代码,相比于同步锁锁住共享数据代码,执必降低了效率
import time
from threading import Thread


num = 100


def func():
    global num
    tep = num
    time.sleep(0.001)
    tep = tep - 1
    num = tep


if __name__ == '__main__':
    t_lst = []
    for i in range(100):
        t = Thread(target=func)
        t_lst.append(t)
        t.start()

    [el.join() for el in t_lst]

    print("主线程的>>>", num)
共享数据不安全现象
import time
from threading import Thread, Lock


num = 100


def func(lock):
    global num
    lock.acquire()    # 上锁
    tep = num
    time.sleep(0.001)
    tep = tep - 1
    num = tep
    lock.release()   # 释放锁


if __name__ == '__main__':
    lock = Lock()   # 创建锁
    t_lst = []
    for i in range(100):
        t = Thread(target=func, args=(lock,))
        t_lst.append(t)
        t.start()

    [el.join() for el in t_lst]

    print("主线程的>>>", num)
加锁解决共享数据不安全问题

 


 

3) 死锁

  • 定义:指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种相互等待的现象
  • 特点:如无外力作用,它们将一直阻塞下去。
import time
from threading import Thread, Lock


def func1(lock_A, lock_B):
    lock_A.acquire()
    time.sleep(0.5)
    print("func1拿到了A锁")
    lock_B.acquire()
    print("func1拿到了B锁")
    lock_B.release()
    lock_A.release()


def func2(lock_A, lock_B):
    lock_B.acquire()
    print("func2拿到了B锁")
    lock_A.acquire()
    print("func2拿到了A锁")
    lock_A.release()
    lock_B.release()


if __name__ == '__main__':
    lock_A = Lock()
    lock_B = Lock()
    t1 = Thread(target=func1, args=(lock_A, lock_B))
    t2 = Thread(target=func2, args=(lock_A, lock_B))
    t1.start()
    t2.start()
死锁现象代码

 


 

4) 递归锁

    在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock即递归锁

    RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

    递归锁可以解决上面死锁问题。

import time
from threading import Thread, RLock


def func1(lock_A, lock_B):
    lock_A.acquire()
    time.sleep(0.5)
    print("func1拿到了A锁")
    lock_B.acquire()
    print("func1拿到了B锁")
    lock_B.release()
    lock_A.release()


def func2(lock_A, lock_B):
    lock_B.acquire()
    print("func2拿到了B锁")
    lock_A.acquire()
    print("func2拿到了A锁")
    lock_A.release()
    lock_B.release()


if __name__ == '__main__':
    lock_A = lock_B = RLock()
    t1 = Thread(target=func1, args=(lock_A, lock_B))
    t2 = Thread(target=func2, args=(lock_A, lock_B))
    t1.start()
    t2.start()
递归锁解决死锁代码

 信号量

原理

  • Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;
  • 调用release()时内置计数器+1;
  • 计数器不能小于0,当计数器为0时,acquire()将阻塞线程调用release()。
import time
import random
from threading import Thread, Semaphore


def func1(i, s):
    time.sleep(1)
    s.acquire()
    print('客官%s里边请~~' % i)
    time.sleep(random.randint(1, 3))
    s.release()


if __name__ == '__main__':
    s = Semaphore(4)
    for i in range(10):
        t = Thread(target=func1, args=(i, s))
        t.start()
代码示例

 信号量与进程池比较

    信号量与进程池是完全不同的概念,进程池Pool(4),是指创建4个进程,从头到尾都是这四个进程在执行,进程结束,进程不会销毁,而是被 扔进池中。而信号量为产生了一堆进程,都在等待抢占同步锁acquire()。

事件

    线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在初始情况下,Event对象中的信号标志被设置为False。如果有线程等待一个Event对象, 而这个Event对象的标志为False,那么这个线程将会被一直阻塞直至该标志为True。一个线程如果将一个Event对象的信号标志设置为True,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为True的Event对象,那么它将忽略这个事件, 继续执行

event.isSet():返回event的状态值;
event.wait():如果 event.isSet()==False将阻塞线程;
event.set(): 设置event的状态值为True,所有阻塞的线程激活进入就绪状态, 等待操作系统调度;
event.clear():恢复event的状态值为False。
方法

    例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作

import threading
import time
import random
from threading import Thread, Event


def conn_mysql():
    count = 1
    while not event.is_set():
        if count > 3:
            raise TimeoutError('链接超时')
        print('<%s>第%s次尝试链接' % (threading.current_thread().getName(), count))
        event.wait(0.5)
        count += 1
    print('<%s>链接成功' % threading.current_thread().getName())


def check_mysql():
    print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())
    time.sleep(random.randint(1, 2))
    event.set()


if __name__ == '__main__':
    event = Event()  # 创建事件
    # 创建线程
    conn1 = Thread(target=conn_mysql)
    conn2 = Thread(target=conn_mysql)
    check = Thread(target=check_mysql)

    conn1.start()
    conn2.start()
    check.start()
代码示例

 线程队列

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

queue.Queue(maxsize=0)

  • 特点:先进先出
import queue

q = queue.Queue(3)  # 创建一个长度为3的队列,也就是说,最多只能放3个数据

q.put(2)
q.put(5)
print(">>>", q.qsize())  # 返回当前队列中数据的长度
q.put(10)

print(q.get())
print(q.get())
print(q.get())
代码示例

queue.LifoQueue(maxsize=0)

  • 特点:先进后出
import queue

q = queue.LifoQueue(3)  # 创建一个长度为3的队列,也就是说,最多只能放3个数据

q.put(2)
q.put(5)
print(">>>", q.qsize())  # 返回当前队列中数据的长度
q.put(10)

print(q.get())
print(q.get())
print(q.get())
代码示例

queue.PriorityQueue(maxsize=0)

  • 特点:按优先级取值,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小,优先级越高
  • put()方法中参数为元组,元组第一个元素为优先值,第二个元素为值。对于同等优先级,则从值的第一位开始比较,小的优先级高。不同数据类型不能比较。
import queue

q = queue.PriorityQueue(3)  # 创建一个长度为3的队列,也就是说,最多只能放3个数据

q.put((20, "a"))
q.put((-1, "b"))
q.put((-10, "c"))

print(q.get())
print(q.get())
print(q.get())


"""
结果:
(-10, 'c')
(-1, 'b')
(20, 'a')
"""
代码示例

 线程池

1. 模块:
# ThreadPoolExecutor:线程池,提供异步调用
# ProcessPoolExecutor:进程池,提供异步调用

2. 基本方法:
# submit(fn, *args, **kwargs):
1)异步提交任务
2)返回结果为Future对象,需用result()方法获取结果

# map(func, *iterables, timeout=None, chunksize=1):
1) 异步提交任务
2) 返回值为生成器

# shutdown(wait=True):
1)相当于进程池的pool.close() + pool.join()操作
2)wait=True,主线程在此阻塞,等待池内所有任务执行完毕回收完资源后才继续
3)wait=False,主线程不会在此阻塞,继续往下执行
4)不管wait参数为何值,整个程序都会等待池内所有任务执行完毕才结束

# result(timeout=None):取得结果

# add_done_callback(fn):回调函数
模块基本使用

 

异步

  • submit(fn, *args, **kwargs)

    返回值为Future对象,获取结果需用result()方法

   该结果对象如果没有执行完,则会阻塞在这里

from concurrent.futures import ThreadPoolExecutor


def func(n):
    return n*n


if __name__ == '__main__':
    p = ThreadPoolExecutor(4)    # 创建线程池
    t_lst = []
    for i in range(10):
        res = p.submit(func, i)      # 异步提交任务
        t_lst.append(res)

    p.shutdown()

    for el in t_lst:
        print(el.result())               # 获取结果,用result()方法

    print("主线程代码结束")
代码示例

 

  • map(func, *iterables, timeout=None, chunksize=1)

    返回结果为生成器,获取结果不用result()方法

from concurrent.futures import ThreadPoolExecutor


def func(n):
    return n*n


if __name__ == '__main__':
    p = ThreadPoolExecutor(4)     # 创建线程池
    res = p.map(func, range(10))  # 异步提交任务

    p.shutdown()

    for el in res:
        print(el)                 # 获取结果

    print("主线程代码结束")
代码示例

 

回调函数

    submit异步提交任务返回的是Future对象,类Future中有回调函数方法,所以可以调用。

    map异步提交任务返回的是生成器,不能调用回调函数

import time
from concurrent.futures import ThreadPoolExecutor


def func(n):
    time.sleep(1)
    return n*n


def call_back(m):
    print(">>>>>", m.result())


if __name__ == '__main__':
    t_p = ThreadPoolExecutor()
    t_p.submit(func, 10).add_done_callback(call_back)

    t_p.shutdown()
    print("主线程代码结束")
回调函数简单使用

 

posted @ 2018-12-03 20:20  Ethan_Y  阅读(220)  评论(0编辑  收藏  举报