IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Python知识库 -> 【multithreading】Python 多线程的概念和使用方法 -> 正文阅读

[Python知识库]【multithreading】Python 多线程的概念和使用方法


前情提要

在计算中,进程是正在执行的计算机程序的一个实例。任何流程都有 3 个基本组成部分:

  • 一个可执行程序。
  • 程序所需的相关数据(变量、工作空间、缓冲区等)
  • 程序的执行上下文(进程状态)
  • 线程是进程中可以调度执行的实体。此外,它是可以在 OS(操作系统)中执行的最小处理单元。

简而言之,线程是程序中的一系列此类指令,可以独立于其他代码执行。为简单起见,可以假设线程只是进程的子集。

线程在线程控制块 (TCB) 中包含所有这些信息:

  • 线程标识符:为每个新线程分配唯一 id (TID)
  • 堆栈指针:指向进程中线程的堆栈。堆栈包含线程范围内的局部变量。
  • 程序计数器:存放线程当前正在执行的指令地址的寄存器。
  • 线程状态:可以是running、ready、waiting、start或done。
  • 线程的寄存器集:分配给线程进行计算的寄存器。
  • 父进程指针:指向线程所在进程的进程控制块 (PCB) 的指针。

在这里插入图片描述

多线程

一个进程中可以存在多个线程,其中:

  • 每个线程都包含自己的寄存器集和局部变量(存储在堆栈中)。
  • 一个进程的所有线程共享全局变量(存储在堆中)和程序代码

思考一下下图,内存中多个线程是如何存在的:
在这里插入图片描述
多线程被定义为处理器同时执行多个线程的能力。


实现

一、线程基础

1. 简易demo

# 导入包
import threading

# 打印立方
def print_cube(num):
	print("Cube: {}".format(num * num * num))

# 打印平方
def print_square(num):
	print("Square: {}".format(num * num))

if __name__ == "__main__":
	# 创建进程
	t1 = threading.Thread(target=print_square, args=(10,))
	t2 = threading.Thread(target=print_cube, args=(10,))

	# 启动t1和t2
	t1.start()
	t2.start()

	# 主进程等待t1和t2执行完
	t1.join()
	t2.join()

	# t1和t2执行完后才能执行下面的代码
	print("Done!")

输出:

Square: 100
Cube: 1000
Done!

解释上面的demo:

  • 要导入线程模块:

    import threading
    
  • 要创建一个新线程,必须创建一个Thread类的对象。它的参数有:

    • target : 线程要执行的函数
    • args:要传递给目标函数的参数

    在上面的示例中,我们创建了 2 个具有不同目标函数的线程:

    t1 = threading.Thread(target=print_square, args=(10,)) 
    t2 = threading.Thread(target=print_cube, args=(10,))
    
  • 要启动一个线程,我们使用Thread类的start方法。

    t1.start() 
    t2.start()
    
  • 一旦线程启动,当前程序(你可以把它想象成一个主线程)也会继续执行。使用join方法,让主进程等待子进程运行结束后再运行。

    t1.join() 
    t2.join()
    

思考下图,以便更好地理解上述程序的工作原理:
在这里插入图片描述

线程运行

2. 如何更改线程的名称

import threading
import os

def task1():
	print("Task 1 线程名称: {}".format(threading.current_thread().name))
	print("task 1 所属进程的ID: {}".format(os.getpid()))

def task2():
	print("Task 2 线程名称: {}".format(threading.current_thread().name))
	print("task 2 所属进程的ID: {}".format(os.getpid()))

if __name__ == "__main__":

	# 打印当前进程的ID
	print("主进程ID: {}".format(os.getpid()))

	# 打印主线程的名称
	print("主线程名称: {}".format(threading.main_thread().name))

	# 创建子线程
	t1 = threading.Thread(target=task1, name='t1')
	t2 = threading.Thread(target=task2, name='t2')
	
	# 运行子线程
	t1.start()
	t2.start()
	
	print("主进程结束")

输出:

主进程ID: 11758
主线程名称: MainThread
Task 1 线程名称: t1
task 1 所属的进程ID: 11758
Task 2 线程名称: t2
task 2 所属的进程ID: 11758
主进程结束

上述demo解析:

  • os.getpid() 函数来获取当前进程的 ID
    print("主进程ID: {}".format(os.getpid()))
    
    可以看出子线程运行时的主进程ID是不变的。
  • threading.main_thread() 函数来获取主线程对象。在正常情况下,主线程是启动 Python 解释器的线程。线程对象的name属性用于获取线程的名称。
    print("主线程名称: {}".format(threading.main_thread().name))
    
  • threading.current_thread() 函数来获取当前线程对象。
    print("Task 1 线程名称: {}".format(threading.current_thread().name))
    

3. 线程中使用Logging模块

我们能够通过打印线程名来识别当前运行的是哪个线程。然而,我们真正需要的是Logging模块的支持,该模块将使用的 %(threadName)s可以将线程名嵌入到每个日志消息中。在日志消息中包含线程名可以更容易地将这些消息追溯到其源。注意,Logging是线程安全的,因此来自不同线程的消息在输出中保持不同。

import threading
import time
import logging

logging.basicConfig(level=logging.DEBUG,
                      format='[%(levelname)s] (%(threadName)-9s) %(message)s',)

def f1():
    logging.debug('开始')
    time.sleep(1)
    logging.debug('结束')

def f2():
    logging.debug('开始')
    time.sleep(2)
    logging.debug('结束')

def f3():
    logging.debug('开始')
    time.sleep(3)
    logging.debug('结束')

t1 = threading.Thread(target=f1) # 使用默认线程名
t2 = threading.Thread(name='f2', target=f2)
t3 = threading.Thread(name='f3', target=f3)

t1.start()
t2.start()
t3.start()

输出

[DEBUG] (Thread-1 ) 开始
[DEBUG] (f2       ) 开始
[DEBUG] (f3       ) 开始
[DEBUG] (Thread-1 ) 结束
[DEBUG] (f2       ) 结束
[DEBUG] (f3       ) 结束

4. 守护线程 setDaemon

守护线程是在后台运行并为主线程或非守护线程提供支持,一般那些后台执行的线程被视为守护线程。守护线程不会阻止主线程退出。
它的最佳示例之一是垃圾收集器,因为我们假设主线程正在执行或运行,此时如果发生任何内存问题, python 虚拟机(PVM)将立即执行垃圾收集器垃圾收集器将在后台执行并销毁所有无用的对象,然后释放出空闲内存,一旦有空闲内存可用,主线程会将毫无问题地执行。

(1) 非守护线程不会在主线程结束后停止运行
from threading
import time

def thread_1():				
	for i in range(5):
		print('这是非守护进程')
		time.sleep(2)

T = Thread(target=thread_1)
T.start()	

time.sleep(5)				
print('主线程结束')

输出:

这是非守护进程
这是非守护进程
这是非守护进程
主线程结束
这是非守护进程
这是非守护进程
(2) 将子线程设置为守护线程
  • obj.setDaemon():参数 True/False
from threading
import time

def thread_1():				
	for i in range(5):
		print('这是非守护进程')
		time.sleep(2)

T = Thread(target=thread_1)
T.setDaemon(True) 
T.start()	

time.sleep(5)				
print('主线程结束')

输出

这是非守护进程
这是非守护进程
这是非守护进程
主线程结束
(3) 否是守护线程
  • obj.isDaemon()
  • obj.daemon
注意:

obj.setDaemon() 设置线程为守护线程时,一定要在 obj.start() 前。因为活动的线程无法设置为守护线程。否则会报错:RuntimeError: cannot set daemon status of active thread

5. 加入线程 join

此方法可以理解为:在主线程的执行顺序里加入一个子线程,这时,子线程就占用了主线程的执行时间,必须等待子线程结束后才能继续执行主线程的后续代码。
在调用 join() 方法时,调用线程被阻塞,直到线程对象停止运行。线程对象可以在以下任何一种情况下终止:

  • 正常结束。
  • 异常退出。
  • 直到超时。

语法:

object_name.join()
# 只等待timeout秒,超时将强行结束
object_name.join(timeout) 

6. 主线程与子线程的关系

下图介绍了主线程与子线程的运行关系:
在这里插入图片描述


二、线程同步

线程同步被定义为一种机制,它确保多个并发线程不会同时执行临界区的代码段。

临界区是指访问共享资源的程序部分。
在这里插入图片描述

1. 为什么需要同步

import threading

x = 0

def increment():
    global x
    x += 1
  
def thread_task():
    for _ in range(100000):
        increment()

def main_task():
	global x
	x = 0
	
	for _ in range(2):
		t = threading.Thread(target=thread_task)
		t.start()
		t.join()

if __name__ == "__main__":
	for i in range(10):
		main_task()
		print("迭代 {0}: x = {1}".format(i,x))

输出

迭代 0: x = 175005
迭代 1: x = 200000
迭代 2: x = 200000
迭代 3: x = 169432
迭代 4: x = 153316
迭代 5: x = 200000
迭代 6: x = 167322
迭代 7: x = 200000
迭代 8: x = 169917
迭代 9: x = 153589

在上面的程序中:

  • main_task函数中创建了两个线程t1t2 ,并将全局变量x设置为 0。
  • 每个线程都有一个目标函数thread_task函数,每个threat_task函数被调用100000次。
  • thread_task函数将在每次调用中将全局变量x增加 1。

x的预期结果应该是200000,实际结果在10 次迭代中的值并不是200000。
这是由于线程对共享变量x并发访问而发生的。x值的这种不可预测性是由于线程竞争造成的,这种情况也被称为线程不安全
在这里插入图片描述

线程竞争

因此,我们需要一个工具来在多个线程之间进行适当的同步。

2. Lock 线程锁

threading模块提供了一个Lock类来处理竞争条件。Lock是使用操作系统提供的Semaphore(信号量)对象实现的。

Semaphore信号量是一个同步对象,它控制并行编程环境中多个进程/线程对公共资源的访问。它只是操作系统(或内核)存储中指定位置的一个值,每个进程/线程都可以检查并更改它。根据找到的值,进程/线程可以使用该资源,如果检查它已经被使用,就必须等待一段时间才能再次尝试。信号量可以是二进制的(0或1),也可以有额外的值。通常,使用信号量的进程/线程会检查该值,然后,如果它使用该资源,就会更改该这个值,以便后续信号量的使用者知道要等待。

Lock类提供以下方法:

  • acquire([blocking]):获取锁。锁可以是阻塞的,也可以是非阻塞的。

    • 当blocking参数设置为True(默认值)时,线程执行将被阻塞,直到锁被解锁,然后锁被设置为locked并返回True。
    • 当blocking参数设置为False时,线程执行不会被阻塞。如果锁被解锁,则将其设置为locked,并返回True,否则立即返回False。
  • release():释放锁。

    • 如果是Lock是acquire,会释放锁,然后返回。其他线程阻塞等待锁,只有使用锁的线程解锁后,其他线程会竞争锁,最终只有一个线程会获取到锁并执行,其它线程继续阻塞等待。
    • 如果已经解锁,则会引发ThreadError。

小demo:

import threading

x = 0

def increment():
	global x
	x += 1

def thread_task(lock):
	for _ in range(100000):
		lock.acquire()  # 执行关键代码前,获取锁
		increment()
		lock.release()  # 执行完后,解锁

def main_task():
	global x
	x = 0

	# 创建Lock对象
	lock = threading.Lock()

	# 将lock通过线程参数传递进去
	t1 = threading.Thread(target=thread_task, args=(lock,))
	t2 = threading.Thread(target=thread_task, args=(lock,))

	t1.start()
	t2.start()

	t1.join()
	t2.join()

if __name__ == "__main__":
	for i in range(10):
		main_task()
		print("迭代 {0}: x = {1}".format(i,x))

结果

迭代 0:x = 200000
迭代 1:x = 200000
迭代 2:x = 200000
迭代 3:x = 200000
迭代 4:x = 200000
迭代 5:x = 200000
迭代 6:x = 200000
迭代 7:x = 200000
迭代 8:x = 200000
迭代 9:x = 200000
  • 解释上面的demo:
    首先,使用以下命令创建一个Lock对象:
    lock = threading.Lock()
    
  • 然后,lock对象作为参数传入函数:
     t1 = threading.Thread(target=thread_task, args=(lock,))
     t2 = threading.Thread(target=thread_task, args=(lock,))
    
  • 在目标函数的关键部分,使用lock.acquire()方法应用锁。一旦获得锁,在使用lock.release()方法释放锁之前,没有其他线程可以访问临界区(这里是increment函数) 。
    lock.acquire()
    increment()
    lock.release()
    

结果正是我们想要的,每次x的值都是 200000。
流程如下:
![在这里插入图片描述](https://img-blog.csdnimg.cn/86d3810962fe4568a386abcdeb346e01.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5aSn5L2s5qmZMTIxNQ==,size_16,color_FFFFFF,t_70,g_se,x_16,#pic_center在这里插入图片描述


三、线程通信

1. 队列Queue

#TODO 待完善

2. 条件Condition

#TODO 待完善


四、多线程的优点及缺点

优点缺点
1、线程之间彼此独立。
2、由于线程并行执行任务,因此可以更好地利用系统资源。
3、增强单核多处理器机器的性能。
1、随着线程数的增加,程序复杂性也会增加。
2、操作共享资源(对象、数据)时,必须进行同步。
3、调试难度高,结果有时不可预测。
4、程序设计糟糕的话会导致死锁。
5、只适用于IO密集型
  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2022-04-07 22:39:24  更:2022-04-07 22:39:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/28 23:38:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计