多任务:线程

未匹配的标注

简介#

什么叫 “多任务” 呢?简单地说,就是操作系统可以同时运行多个任务。操作系统轮流让各个任务交替执行,表面上看,每个任务都是交替执行的,但是,由于 CPU 的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。真正的并行执行多任务只能在多核 CPU 上实现,但是,由于任务数量远远多于 CPU 的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

并发:指的是任务数多于 cpu 核数,通过操作系统的各种任务调度算法,实现用多个任务 “一起” 执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)。
并行:指的是任务数小于等于 cpu 核数,即任务真的是一起执行的。

线程#

简介#

线程是程序的最小执行流单元,是程序中一个单一的顺序控制流程

threading 模块#

python 的 thread 模块是比较底层的模块,python 的 threading 模块是对 thread 做了一些包装的,可以更加方便的被使用

语法#

import threading
t = threading.Thread(target="函数名")
t.start()

当调用 start() 时,才会真正的创建线程,并且开始执行。主线程会等待所有的子线程结束后才结束

查看线程数量#

可以通过 len(threading.enumerate()) 查看当前线程数量

注意点#

线程执行代码的封装#

通过使用 threading 模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用 threading 模块时,往往会定义一个新的子类 class,只要继承 threading.Thread 就可以了,然后重写 run 方法。

python 的 threading.Thread 类有一个 run 方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过 Thread 类的 start 方法,可以启动该线程,交给 python 虚拟机进行调度,当该线程获得执行的机会时,就会调用 run 方法执行线程。

线程的执行顺序#

多线程程序的执行顺序是不确定的。当执行到 sleep 语句时,线程将被阻塞(Blocked),到 sleep 结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个 run 函数,但是线程的启动顺序、run 函数中每次循环的执行顺序都不能确定。

  1. 每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的 name,但是 python 会自动为线程指定一个名字。
  2. 当线程的 run () 方法结束时该线程完成。
  3. 无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。

共享全局变量#

在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据,缺点就是线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)。

多线程开发可能遇到的问题#

假设两个线程 t1 和 t2 都要对全局变量 g_num (默认是 0) 进行加 1 运算,t1 和 t2 都各对 g_num 加 10 次,g_num 的最终的结果应该为 20。

但是由于是多线程同时操作,有可能出现下面情况:

  1. 在 g_num=0 时,t1 取得 g_num=0。此时系统把 t1 调度为”sleeping” 状态,把 t2 转换为”running” 状态,t2 也获得 g_num=0
  2. 然后 t2 对得到的值进行加 1 并赋给 g_num,使得 g_num=1
  3. 然后系统又把 t2 调度为”sleeping”,把 t1 转为”running”。线程 t1 又把它之前得到的 0 加 1 后赋值给 g_num。
  4. 这样导致虽然 t1 和 t2 都对 g_num 加 1,但结果仍然是 g_num=1

如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确

互斥锁#

简介#

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制,线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

互斥锁为资源引入一个状态:锁定 / 非锁定

某个线程要更改共享数据时,先将其锁定,此时资源的状态为 “锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成 “非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

threading 模块中定义了 Lock 类,可以方便的处理锁定:

# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()
  1. 如果这个锁之前是没有上锁的,那么 acquire 不会堵塞
  2. 如果在调用 acquire 对这个锁上锁之前 它已经被 其他线程上了锁,那么此时 acquire 会堵塞,直到这个锁被解锁为止
上锁解锁过程#

当一个线程调用锁的 acquire () 方法获得锁时,锁就进入 “locked” 状态。

每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为 “blocked” 状态,称为 “阻塞”,直到拥有锁的线程调用锁的 release () 方法释放锁之后,锁进入 “unlocked” 状态。

线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

优缺点#

优点:确保了某段关键代码只能由一个线程从头到尾完整地执行

缺点:阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了;由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁

死锁#

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。尽管死锁很少发生,但一旦发生就会造成应用的停止响应。

如何避免死锁#
  • 程序设计时要尽量避免
  • 添加超时时间等

GIL#

GIL 为全局解释器锁。每个线程在执行的过程都需要先获取 GIL,保证同一时刻只有一个线程可以执行代码。

Python 语言和 GIL 没有半毛钱关系。仅仅是由于历史原因在 Cpython 虚拟机 (解释器),难以移除 GIL。在 IO 操作等可能会引起阻塞的 system call 之前,可以暂时释放 GIL, 但在执行完毕后,必须重新获取 GIL Python 3.x 使用计时器(执行时间达到阈值后,当前线程释放 GIL)或 Python 2.x,tickets 计数达到 100。Python 使用多进程是可以利用多核的 CPU 资源的。多线程爬取比单线程性能有提升,因为遇到 IO 阻塞会自动释放 GIL 锁

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~