多线程

多线程理论

(1)什么是线程

  • 在 Python 中,线程(Thread)是执行单元的最小单位。线程是进程内的一条执行路径,每个线程都有自己的执行序列、执行环境和栈空间,但它们共享同一个进程的地址空间。

  • 在多线程编程中,可以同时运行多个线程,每个线程执行不同的任务,从而实现并发执行。相比于多进程,线程的开销较小,线程之间的切换更快,因为它们共享进程的内存空间。

  • 在 Python 中,线程可以通过标准库中的 threading 模块来创建和管理。通过创建 Thread 对象并将要执行的函数传递给该对象,就可以创建一个新的线程。Python 提供了丰富的线程控制和同步机制,如互斥锁、信号量等,用于协调和管理多个线程之间的并发访问。

  • 需要注意的是,在 Python 中由于全局解释器锁(GIL)的存在,多线程并不能真正实现多核并行计算,因为在任何给定时刻只有一个线程能够执行 Python 字节码。因此,在 CPU 密集型任务中,多线程并不能显著提高性能,但对于 I/O 密集型任务,多线程可以提高程序的响应速度和并发处理能力。

(2)线程的创建开销

  • 线程的创建开销包括多个方面,主要取决于操作系统和编程语言的实现。在 Python 中,线程的创建开销相对较高,主要原因是因为 Python 的全局解释器锁(Global Interpreter Lock,GIL)以及线程对象的额外开销。

  • 以下是影响线程创建开销的一些因素:

  1. 内核态和用户态切换: 线程是由操作系统内核来创建和管理的,因此线程的创建需要进行内核态和用户态之间的切换,这个切换过程会产生一定的开销。
  2. 内存分配和资源管理: 每个线程都需要分配一定的内存空间来存储线程的执行环境、栈空间以及其他相关信息。此外,操作系统需要为每个线程分配一些资源,如线程 ID、线程控制块等。
  3. GIL 的影响: 在 Python 中,由于全局解释器锁(GIL)的存在,多个线程不能同时执行 Python 字节码,因此线程的并行性受到限制。线程创建时,由于 GIL 的存在,可能会导致一定的额外开销。
  4. 线程对象的额外开销: 在 Python 中,线程对象(Thread)的创建也会带来一定的开销,包括线程对象本身的内存占用、管理开销等。
  5. 竞争和同步开销: 如果线程需要访问共享资源或进行同步操作,还会引入额外的竞争和同步开销,如锁的获取和释放等。
  • 总的来说,虽然线程的创建开销相对较低,但在 Python 中,由于 GIL 的存在以及额外的线程对象开销等因素,线程的创建开销可能会比较显著。因此,在设计多线程应用程序时,需要谨慎考虑线程的创建数量和资源管理,以避免不必要的开销和性能下降。

(3)线程和进程的区别

线程(Thread)和进程(Process)是操作系统中的两个核心概念,用于实现并发执行任务。它们之间的主要区别在于以下几个方面:

  1. 执行单位:

    • 进程是操作系统中的一个独立执行单元,拥有独立的地址空间、内存、文件描述符等资源。每个进程都有自己的代码段、数据段、堆栈等。
    • 线程是进程内的一个独立执行单元,多个线程共享同一个进程的地址空间和资源,包括内存、文件描述符、打开的文件等。因此,多个线程之间可以共享数据和通信。
  2. 资源消耗:

    • 进程之间的资源是相互独立的,因此进程的创建和销毁比较耗费系统资源,包括内存和 CPU 时间。
    • 线程之间共享相同的资源,因此线程的创建和销毁开销相对较小,但是线程之间的同步和通信会引入额外的开销。
  3. 通信与同步:

    • 进程之间的通信和同步相对复杂,通常需要使用进程间通信(Inter-Process Communication,IPC)机制,如管道、消息队列、共享内存等。
    • 线程之间共享同一个进程的地址空间和资源,因此线程之间的通信和同步相对简单,可以直接通过共享变量等方式进行通信和同步。
  4. 并发性和性能:

    • 由于进程之间拥有独立的资源,因此多进程可以实现真正的并行执行,适合处理 CPU 密集型任务。
    • 线程共享相同的资源,因此多线程更适合处理 I/O 密集型任务,但由于全局解释器锁(GIL)的存在,Python 中的多线程并不能真正实现多核并行,适合处理 I/O 密集型任务和简单的并发处理。

总的来说,进程是系统中的独立执行单元,拥有独立的资源,适合处理 CPU 密集型任务;而线程是进程内的独立执行单元,共享进程的资源,适合处理 I/O 密集型任务和简单的并发处理。

(4)为何要有多线程

(1)开设进程

  • 申请内存空间 -- 耗资源
  • 拷贝代码 - 耗资源

(2)开设线程

  • 一个进程内可以开设多个线程
  • 在一个进程内开设多个线程无需再次申请内存空间及拷贝代码操作

(3)总结线程的优点

  • 减少了资源的消耗
  • 同一个进程下的多个线程资源共享

(4)什么是多线程

  • 多线程指的是
    • 在一个进程中开启多个线程
    • 简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。
  • 多线程共享一个进程的地址空间
    • 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
  • 若多个线程都是cpu密集型的,那么并不能获得性能上的增强
    • 但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
  • 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于Python)

多线程开设

(1)threading模块

import time
from threading import Thread
def run(i):
print(f'这是线程{i}')
time.sleep(2)
def main_first():
task_list = []
for i in range(1, 5):
thread = Thread(target=run, args=(i,))
task_list.append(thread)
task_list_new = []
for i in task_list:
i.start()
task_list_new.append(i)
for p in task_list_new:
p.join()
if __name__ == '__main__':
start_time = time.time()
main_first()
print(f'总耗时:>>> {time.time() - start_time}')
# 这是线程1
# 这是线程2
# 这是线程3
# 这是线程4
# 总耗时:>>> 2.012131929397583

(2)继承Thread父类

from threading import Thread
import time
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
# 定义 run 函数
def run(self):
print(f'{self.name} is running')
time.sleep(3)
print(f'{self.name} is ending')
def main():
t = MyThread('heart')
t.start()
print(f'this is a main process')
if __name__ == '__main__':
main()
# heart is running
# this is a main process
# heart is ending

(3)查看pid

  • 多个线程其实是开设在一个进程下的,子线程的进程id和主线程是一样的
import time
from multiprocessing import Process
from threading import Thread
import os
def run(i):
print(f'这是参数:>>> {i}')
print(f'这是子的ID:>>> {os.getpid()}')
time.sleep(2)
def main_process():
task_list = [Process(target=run, args=(i,)) for i in range(1, 5)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(f'这是主进程的ID :>>> {os.getpid()}')
def main_thread():
task_list = [Thread(target=run, args=(i,)) for i in range(1, 5)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(f'这是主线程的ID :>>> {os.getpid()}')
if __name__ == '__main__':
main_process()
# 这是参数:>>> 2
# 这是子的ID:>>> 5644
# 这是参数:>>> 1
# 这是子的ID:>>> 1904
# 这是参数:>>> 3
# 这是子的ID:>>> 13532
# 这是参数:>>> 4
# 这是子的ID:>>> 11968
# 这是主进程的ID :>>> 10196
main_thread()
# 多个线程其实是开设在一个进程下的,子线程的进程id和主线程是一样的
# 这是参数:>>> 1
# 这是子的ID:>>> 16416
# 这是参数:>>> 2
# 这是子的ID:>>> 16416
# 这是参数:>>> 3
# 这是子的ID:>>> 16416
# 这是参数:>>> 4
# 这是子的ID:>>> 16416
# 这是主线程的ID :>>> 16416

(4)多线程共享数据

  • 同一个进程下的所有线程共享同一个进程的资源
  • 多进程是不共享的,每一块内存空间都是独立的
  • 多进程的代码和结果如下
import time
from multiprocessing import Process
import os
number = 0
def run(i):
global number
number += 1
print(f'这是run函数内的number:>>> {number}')
print(f'这是参数:>>> {i}')
print(f'这是子的ID:>>> {os.getpid()}')
time.sleep(2)
def main_process():
task_list = [Process(target=run, args=(i,)) for i in range(1, 5)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(f'这是主进程的ID :>>> {os.getpid()}')
if __name__ == '__main__':
main_process()
# 这是run函数内的number:>>> 1
# 这是参数:>>> 3
# 这是子的ID:>>> 15116
# 这是run函数内的number:>>> 1
# 这是参数:>>> 2
# 这是子的ID:>>> 17212
# 这是run函数内的number:>>> 1
# 这是参数:>>> 4
# 这是子的ID:>>> 1756
# 这是run函数内的number:>>> 1
# 这是参数:>>> 1
# 这是子的ID:>>> 6716
# 这是主进程的ID :>>> 15800
  • 然而多线程是共享的,代码如下
import time
from threading import Thread
import os
number = 0
def run(i):
global number
number += 1
print(f'这是run函数内的number:>>> {number}')
print(f'这是参数:>>> {i}')
print(f'这是子的ID:>>> {os.getpid()}')
time.sleep(2)
def main_thread():
task_list = [Thread(target=run, args=(i,)) for i in range(1, 5)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(f'这是主线程的ID :>>> {os.getpid()}')
if __name__ == '__main__':
main_thread()
# 这是run函数内的number:>>> 1
# 这是参数:>>> 1
# 这是子的ID:>>> 13888
# 这是run函数内的number:>>> 2
# 这是参数:>>> 2
# 这是子的ID:>>> 13888
# 这是run函数内的number:>>> 3
# 这是参数:>>> 3
# 这是子的ID:>>> 13888
# 这是run函数内的number:>>> 4
# 这是参数:>>> 4
# 这是子的ID:>>> 13888
# 这是主线程的ID :>>> 13888

(5)查看当前进程的名字 current_thread

  • current_thread().name
import time
from threading import Thread, current_thread
import os
number = 0
def run(i):
global number
number += 1
print(f'这是run函数内的number:>>> {number}')
print(f'这是参数:>>> {i}')
print(f'这是子的ID:>>> {os.getpid()}')
time.sleep(2)
def main_thread():
task_list = [Thread(target=run, args=(i,)) for i in range(1, 5)]
[task.start() for task in task_list]
[task.join() for task in task_list]
print(f'这是主线程的ID :>>> {os.getpid()}')
print(f'这是主线程的名字 :>>> {current_thread().name}')
if __name__ == '__main__':
main_thread()
# 这是run函数内的number:>>> 1
# 这是参数:>>> 1
# 这是子的ID:>>> 10552
# 这是run函数内的number:>>> 2
# 这是参数:>>> 2
# 这是子的ID:>>> 10552
# 这是run函数内的number:>>> 3
# 这是参数:>>> 3
# 这是子的ID:>>> 10552
# 这是run函数内的number:>>> 4
# 这是参数:>>> 4
# 这是子的ID:>>> 10552
# 这是主线程的ID :>>> 10552
# 这是主线程的名字 :>>> MainThread

(6)守护线程 daemon

from threading import Thread
import time
def task(name):
print(f'当前 {name} is beginning')
time.sleep(2)
print(f'当前 {name} is ending')
def main():
t = Thread(target=task, args=('heart',))
# 开启守护线程
t.daemon = True
t.start()
print(f' this is main process')
if __name__ == '__main__':
main()
# 当前 heart is beginning
# this is main process

(7)线程的互斥锁

(1)未加锁

from threading import Thread,Lock
import time
money = 100
def task():
global money
# 模拟获取到车票信息
temp = money
# 模拟网络延迟
time.sleep(2)
# 模拟购票
money = temp - 1
def main():
t_list = []
for i in range(5):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
# 所有子线程结束后打印 money
print(money)
if __name__ == '__main__':
main()
# 99

(2)加锁后

from threading import Thread,Lock
import time
money = 100
lock = Lock()
def task():
global money
lock.acquire()
# 模拟获取到车票信息
temp = money
# 模拟网络延迟
time.sleep(2)
# 模拟购票
money = temp - 1
lock.release()
def main():
t_list = []
for i in range(5):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
# 所有子线程结束后打印 money
print(money)
if __name__ == '__main__':
main()
# 95
posted @   ssrheart  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示