Python多线程与多进程浅析之一

简介:

多线程引言

多线程处理,是 Python 乃至很多编程语言中比较复杂的概念,随着 CPU 的多核心、计算速度越来越快、各类网络应用等的出现,对于多个线程的运用,可以有效的提高程序的处理性能和速度。

有很多讨论 Python 线程、进程和协程的书和资料,有的概念说的不太清楚,有的例子举得太复杂,因此在研究和实践之后,斗胆也讨论一下这个略有复杂的话题,希望不要误人子弟。

线程

线程的标准定义如下:

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

在多核或多CPU,或支持 Hyper-threading 的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单 CPU 单核的计算机上,使用多线程技术,也可以把进程中负责 IO 处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的 workhorse 线程执行密集计算,从而提高了程序的执行效率。

Python 是解释性语言

像 C/C++这样的语言是编译型语言,程序输入到编译器,编译器再根据语言的语法进行解析,然后翻译成语言独立的中间表示,最终链接成具有高度优化的机器码的可执行程序。编译器之所以可以深层次的对代码进行优化,是因为它可以看到整个程序(或者一大块独立的部分)。这使得它可以对不同的语言指令之间的交互进行推理,从而给出更有效的优化手段。

Python 程序的执行是解释型的,检查语法、翻译成中间状态等也会做,但是不会把整个程序翻译成机器码,可以理解为一行行去执行代码。目前的全栈语言 JavaScript 以及非常适合开发网站的 PHP 都是解释型语言。

现在的 CPU 4核、8核都是常规了,要想利用多核系统,Python必须支持多线程运行。作为解释型语言,Python的解释器必须做到既安全又高效。多线程编程会遇到的问题是解释器要避免在不同的线程操作内部共享的数据。同时它还要保证在管理用户线程时保证总是有最大化的计算资源

Python 线程切换机制

Python 支持多线程,有两种模式,一种是协作多任务(cooperative multitasking),另一种是抢占式多任务(preemptive multitasking)。

Python 的协作多任务机制是当一个线程开始 sleep 或者进行 I/O 操作时,另一个线程就有机会拿到GIL锁,开始执行它的代码。 Python 的抢占式多任务机制是每隔 15ms 进行监测,尝试收回 GIL。

由于多线程执行时,存在线程的切换,当多个线程同时运行时,如果能保证运行结果符合预期,就是线程安全的。

和操作系统进行进程调度类似,当进程执行一段时间之后,发生时钟中断,操作系统响应时钟中断,并在这时进行进程调度。而 Python 中也是通过软件模拟了这种时钟中断,来激活线程调度。

下面是一个重要的概念,关于线程安全,在说这个之前,先来看这个例子:

# 导入需要的库
>>> import threading

>>> count = 0

>>> def run():
...     global count
...     for i in range(1000000):
...         count = count + 1

>>> t1 = threading.Thread(target=run)
>>> t2 = threading.Thread(target=run)
>>> t1.start()
>>> t2.start()
>>> t1.join()
>>> t2.join()

>>> print(count)
1373573

两个线程轮流执行一个加法程序,感觉答案应该是2000000,可以,你会发现每次都小于2000000。这是为什么呢?

比如在 count 是 20 的时候,线程 t1 读取了 count,t1 读到的是 20,这时候 CPU 将控制权给了另一个线程 t2。 t2线程读到的 count 也是 20,然后 t2 加1,写回21。线程回到 t1的时候,t1 将前面读到的20也加1,还是21写回。本来应该连个线程各加1次,等于22的,现在成了21。

所以说在这个例子里,只要 CPU 从线程拿走控制权的时候正好是在读完值的时候,就会发生这样的情况。这就是多线程下对全局变量的写操作不是线程安全的现象和原因。

Python 线程安全

因为线程被切换时候,线程的写操作会被中断,所以我们要考虑线程安全这个问题,否则多线程的程序的运行结果就会出错。

天生线程安全

天生线程安全,就是线程代码中只对全局对象进行读操作,而不存在写操作。这种情况下,不论线程在何处中断,都不会影响各个线程本来的执行逻辑。

实现原子操作

在一个线程中,有时需要保证某一行或者某一段代码的逻辑是不可中断的,也就是说要保证这段代码执行的原子性。
Python 内建的数据类型(list,dict等)的共享变量进行操作,就是原子操作。

比如下面这些操作都是原子的,不用担心多线程切换时候的问题

  • list.append(x)
  • list1.extend(list2)
  • x = list[i]
  • x = list.pop()
  • list.sort()
  • x = y

执行代码的前后加互斥锁

我们修改一下刚才的两个进程的加法例子:
最简单的办法就是引入 threading 模块中的 Lock(),然后在 count 计算这里前面加上锁,后面加上释放。

>>> import threading

>>> lock = threading.Lock()

>>> count = 0

>>> def run():
...     global count
...     for i in range(1000000):
...         # 加锁
...         lock.acquire()
...         count += 1
...         lock.release()

>>> t1 = threading.Thread(target=run)
>>> t2 = threading.Thread(target=run)
>>> t1.start()
>>> t2.start()
>>> t1.join()
>>> t2.join()

>>> print(count)    
2000000

也可以使用with语句来实现同样功能。在使用锁的时候,with语句会在进入语句块之前自动的获取到该锁对象,然后在语句块执行完成后自动释放掉锁。如同在打开文件时候的 with 语句一样,这样比较简洁也安全。

>>> import threading

>>> lock = threading.Lock()

>>> count = 0

>>> def run():
...     global count
...     for i in range(1000000):
...        # 使用 with 来进行加锁
...         with lock:
...             count += 1
    
>>> t1 = threading.Thread(target=run)
>>> t2 = threading.Thread(target=run)
>>> t1.start()
>>> t2.start()
>>> t1.join()
>>> t2.join()

>>> print(count)        
2000000

锁的操作还是略复杂的,除了简单的直接锁以外,还有RLock,简单锁即便是线程本身也会发生阻塞,RLock 只有在其他线程访问时才会发生阻塞。

信号量 (Semaphores) 是一个更高级的锁机制。信号量内部有一个计数器而不像锁对象内部有锁标识,而且只有当占用信号量的线程数超过信号量时线程才阻塞。这允许了多个线程可以同时访问相同的代码区。

当信号量被获取的时候,计数器减小;当信号量被释放的时候,计数器增大。当获取信号量的时候,如果计数器值为0,则该进程将阻塞。当某一信号量被释放,counter值增加为1时,被阻塞的线程(如果有的话)中会有一个得以继续运行。

信号量通常被用来限制对容量有限的资源的访问,比如一个网络连接或者数据库服务器。在这类场景中,只需要将计数器初始化为最大值,信号量的实现将为你完成剩下的事情。

用 Semaphores 可以实现类似线程池的功能。当然我们其实有更简单的办法来实现线程池,后面会说到。

实现线程同步

线程同步是在锁的基础来实现的。通过锁来对各个线程的执行顺序进行控制。一个线程需要等待其它线程完成特定任务之后,才能执行。多个线程之间有依赖关系。比如抓取网站数据,然后分析处理,写入数据库,就可以通过线程同步来实现。

待续

摘自本人与同事所著《Python 机器学习实战》一书

目录
相关文章
|
20天前
|
调度 开发者 Python
深入浅出操作系统:进程与线程的奥秘
在数字世界的底层,操作系统扮演着不可或缺的角色。它如同一位高效的管家,协调和控制着计算机硬件与软件资源。本文将拨开迷雾,深入探索操作系统中两个核心概念——进程与线程。我们将从它们的诞生谈起,逐步剖析它们的本质、区别以及如何影响我们日常使用的应用程序性能。通过简单的比喻,我们将理解这些看似抽象的概念,并学会如何在编程实践中高效利用进程与线程。准备好跟随我一起,揭开操作系统的神秘面纱,让我们的代码运行得更加流畅吧!
|
20天前
|
消息中间件 Unix Linux
【C语言】进程和线程详解
在现代操作系统中,进程和线程是实现并发执行的两种主要方式。理解它们的区别和各自的应用场景对于编写高效的并发程序至关重要。
46 6
|
20天前
|
调度 开发者
深入理解:进程与线程的本质差异
在操作系统和计算机编程领域,进程和线程是两个核心概念。它们在程序执行和资源管理中扮演着至关重要的角色。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
44 5
|
18天前
|
算法 调度 开发者
深入理解操作系统:进程与线程的管理
在数字世界的复杂编织中,操作系统如同一位精明的指挥家,协调着每一个音符的奏响。本篇文章将带领读者穿越操作系统的幕后,探索进程与线程管理的奥秘。从进程的诞生到线程的舞蹈,我们将一起见证这场微观世界的华丽变奏。通过深入浅出的解释和生动的比喻,本文旨在揭示操作系统如何高效地处理多任务,确保系统的稳定性和效率。让我们一起跟随代码的步伐,走进操作系统的内心世界。
|
20天前
|
调度 开发者
核心概念解析:进程与线程的对比分析
在操作系统和计算机编程领域,进程和线程是两个基本而核心的概念。它们是程序执行和资源管理的基础,但它们之间存在显著的差异。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
38 4
|
25天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
1月前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
1月前
|
监控 JavaScript 前端开发
python中的线程和进程(一文带你了解)
欢迎来到瑞雨溪的博客,这里是一位热爱JavaScript和Vue的大一学生分享技术心得的地方。如果你从我的文章中有所收获,欢迎关注我,我将持续更新更多优质内容,你的支持是我前进的动力!🎉🎉🎉
23 0
|
1月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
62 0
|
7月前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
下一篇
DataWorks