在python中,你可以启动一个线程,但却无法停止它。

对不起,你必须要等到它执行结束。

多线程编程适用于:

  1. 本质上是异步的;
  2. 需要多个并发活动;
  3. 每个活动的处理顺序是不确定的,或者说是随机不可预测的;
  4. 可以被组织或者划分成多个执行流,其中每个执行流都有一个指定要完成的任务;
  5. 这些子任务的结果最终合并成最终的输出的结果。

等待I/O、等待从数据库获取数据等

python异步的场景

1、 不涉及共享资源,获对共享资源只读,即非互斥操作 2、 没有时序上的严格关系 3、 不需要原子操作,或可以通过其他方式控制原子性 4、 常用于IO操作等耗时操作,因为比较影响客户体验和使用性能 5、 不影响主线程逻辑

1. 进程 线程 协程

进程是系统资源分配的最小单位,所有进程数据不共享,开销大。

线程Thread,又称作轻量级进程:

线程在同一个进程下执行,并享有相同的上下文。
线程是CPU调度的最小单位,依赖进程存在。一个进程至少有一个线程,叫主线程,而多个线程共享内存(数据共享,共享全局变量),从而极大地提高了程序的运行效率。

协程Coroutine,又称微线程:

是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操中栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行.

协程由于由程序主动控制切换,没有线程切换的开销,所以执行效率极高。对于IO密集型任务非常适用,如果是cpu密集型,推荐多进程+协程的方式。

线程包括开始、执行顺序和结束三部分。通过一个指令指针记录当前运行的上下文。当其他线程运行时,它可以被抢占(中断)和临时挂起(睡眠)——这种称之为yielding 让步。
线程和其主进程共享同一片数据空间。并发进行:每个线程运行一段时间,然后让步给其他线程(再次进入排队序列)。
如果两个或多个线程访问同一片数据,由于数据访问顺序不同,可能导致结果不一致。这种情况通常称为竞态条件(race condition)。幸运的是,大多数线程库都有一些同步原语,以允许线程管理器控制执行和访问。
另一个需要注意的问题是,线程无法给予公平的执行时间。这是因为一些函数会在完成前保持阻塞状态,如果没有专门为多线程情况进行修改,会导致CPU 的时间分配向这些贪婪的函数倾斜。

堆 和 栈

一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(私有的)栈(stack),Windows线程的缺省堆栈大小为1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,Windows进程还有所谓进程默认堆,用户也可以创建自己的堆。
**
堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

2. 全局解释锁GIL Global Interpreter Lock

python代码的执行是由python虚拟机进行控制的。对于python虚拟机的访问是由 全局解释器锁(GIL,Global Interpreter Lock )控制。 这个锁是用来保证同时只有一个线程运行的。
多线程执行顺序:

  1. 设置GIL 全局解释器锁
  2. 切换到一个线程去运行
  3. 执行:
  4. 指定数量的字节码指令
  5. 线程主动让出控制权(也可以是IO操作或者调用time.sleep()执行)
  6. 把线程设置为睡眠状态(切换出线程)
  7. 解锁GIL 全局解释器锁
  8. 重复上述步骤
  1. from threading import Thread
  2. '''
  3. GIL global Interpretor Lock
  4. python中的一个线程对应于C语言的一个线程。
  5. GIL保证python同一时刻只有一个线程在一个cpu上执行字节码
  6. GIl会根据执行的字节码行数或者时间片释放GIL.
  7. GIl还会在遇到io操作的时候主动释放
  8. '''
  9. total = 0
  10. def addd():
  11. global total
  12. for i in range(1000000):
  13. total +=1
  14. def desc():
  15. global total
  16. for i in range(1000000):
  17. total -=1
  18. t1 = Thread(target=addd)
  19. t2 = Thread(target=desc)
  20. t1.start()
  21. t2.start()
  22. t1.join()
  23. t2.join()
  24. print(total)

每次执行上述代码都会得到不一样的输出。这说明GIL并不是执行完一个线程后执行下一个线程。而是根据字节码行数、时间片或者IO操作时释放当前线程启动下一个线程。

3. 什么是多线程竞争

线程是非独立的,同一个进程里线程是数据共享的,当各个线程访问数据资源时会出现竞争状态:即数据几乎同步会被多个线程占用,造成数据混乱,所谓的线程不安全。
而解决线程竞争的一个办法就是——Lock锁

锁的好处:

确保了某段关键代码(共享数据资源)只能由一个线程从头到尾完整地执行能解决多线程资源竞争下的原子操作问题。

锁的坏处:

阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率大大地降低。

4. 锁的致命问题——死锁

若干子线程在系统资源竞争时,都在等待对方对某部分资源解除占用状态,结果是谁也不愿先解锁,互相干等着,程序无法执行下去,这就是死锁。


14 python 多线程编程 - 图1

死锁的产生原因

a. 竞争资源

系统中的资源可以分为两类:

  1. 可剥夺资源。是指某进程再获得这类资源后,该资源可以再被其他进程或系统剥夺,CPU和主存均属于可剥夺资源;
  2. 不可剥夺资源。当系统把这类资源分配给某进程后,再不能强制收回,只能等进程用完后自行释放,如磁带机、打印机等
  • 产生死锁中的竞争资源之一指的是竞争不可剥夺资源(例如:系统中只有一台打印机,可供进程P1使用,假定P1已占用了打印机,若P2继续要求打印机打印将阻塞)
  • 产生死锁中的竞争资源另外一种资源指的是竞争临时资源(临时资源包括硬件中断、信号、消息、缓冲区内的消息等),通常消息通信顺序进行不当,则会产生死锁

    b. 进程间推进顺序非法
  • 若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁

  • 例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁

死锁的四个必要条件

  1. 互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一进程占用;
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放;
  3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放;
  4. 环路等待条件:在发生死锁时,必然存在一个进程——资源的环形链。

解决死锁的基本方法

预防死锁:
  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:破坏请保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
  • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

**

避免死锁
  • 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
  • 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

检测死锁
  1. 首先为每个进程和每个资源指定一个唯一的号码;
  2. 然后建立资源分配表和进程等待表

解除死锁

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

  • 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
  • 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

    5. 什么是线程安全?什么是互斥锁?

    每个对象都对应于一个可称为’互斥锁‘的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

    同一进程中的多线程之间是共享系统资源的,多个线程同时对一个对象进行操作,一个线程操作尚未结束,另一线程已经对其进行操作,导致最终结果出现错误,此时需要对被操作对象添加
    互斥锁,保证每个线程对该对象的操作都得到正确的结果。

    6. 计算机中的同步和异步

    同步(Synchronous)

    是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果。是一种线性执行的方式,执行的流程不能跨越。一般用于流程性比较强的程序,比如用户登录,需要对用户验证完成后才能登录系统。

    异步(Asynchronous)

    是发送了调用的指令,调用者无需等待被调用的方法完全执行完毕;而是继续执行下面的流程。是一种并行处理的方式,不必等待一个程序执行完,可以执行其它的任务,比如页面数据加载过程,不需要等所有数据获取后再显示页面。

他们的区别就在于一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式,比如日志记录就可以使用异步方式进行保存。

如果主线程需要等待子线程全部执行完成再结束,那么主线程在子线程执行过程中需要阻塞;如果主线程不需要等待子线程执行完成就可以提前结束,那么主线程在子线程执行过程中不需要阻塞,也就会出现主线程已跑完,子线程还在跑。 需要等待就要阻塞线程执行,不需要等待就不用了阻塞线程执行,就是非阻塞。
同步异步相对于多任务而言,阻塞非阻塞相对于代码执行而言。

7. 线程、进程的使用场景

什么是并发和并行

并发(concurrency):不会在同一时刻同时运行,存在交替执行的情况

Threading库
线程是并发的,多线程适合在IO密集型操作(多读写操作如爬虫)

并行(parallel):同一时刻多个任务同时运行

multiprocessing库
进程是并行的,多进程适合在CPU密集型操作(CPU指令较多,如位多的浮点数运算)

8. 协程 asyncio