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知识库 -> 13.进阶---多线程 和 多进程 -> 正文阅读

[Python知识库]13.进阶---多线程 和 多进程

Python代码中创建新线程

python3 将 系统调用创建线程 的功能封装在 标准库 threading 中。

大家来看下面的一段代码

print('主线程执行代码') 

# 从 threading 库中导入Thread类
from threading import Thread
from time import sleep

# 定义一个函数,作为新线程执行的入口函数
def threadFunc(arg1,arg2):
    print('子线程 开始')
    print(f'线程函数参数是:{arg1}, {arg2}')
    sleep(5)
    print('子线程 结束')


# 创建 Thread 类的实例对象
thread = Thread(
    # target 参数 指定 新线程要执行的函数
    # 注意,这里指定的函数对象只能写一个名字,不能后面加括号,
    # 如果加括号就是直接在当前线程调用执行,而不是在新线程中执行了
    target=threadFunc, 

    # 如果 新线程函数需要参数,在 args里面填入参数
    # 注意参数是元组, 如果只有一个参数,后面要有逗号,像这样 args=('参数1',)
    args=('参数1', '参数2')
)

# 执行start 方法,就会创建新线程,
# 并且新线程会去执行入口函数里面的代码。
# 这时候 这个进程 有两个线程了。
thread.start()

# 主线程的代码执行 子线程对象的join方法,
# 就会等待子线程结束,才继续执行下面的代码
thread.join()
print('主线程结束')

运行该程序,解释器执行到下面代码时

thread = Thread(target=threadFunc,
                args=('参数1', '参数2')
                )

创建了一个Thread实例对象,其中,Thread类的初始化参数 有两个

target参数 是指定新线程的 入口函数, 新线程创建后就会 执行该入口函数里面的代码,

args 指定了 传给 入口函数threadFunc 的参数。 线程入口函数 参数,必须放在一个元组里面,里面的元素依次作为入口函数的参数。

注意,上面的代码只是创建了一个Thread实例对象, 但这时,新的线程还没有创建。

要创建线程,必须要调用 Thread 实例对象的 start方法 。也就是执行完下面代码的时候

thread.start()

新的线程才创建成功,并开始执行 入口函数threadFunc 里面的代码。

有的时候, 一个线程需要等待其它的线程结束,比如需要根据其他线程运行结束后的结果进行处理。

这时可以使用 Thread对象的 join 方法

thread.join()

如果一个线程A的代码调用了 对应线程B的Thread对象的 join 方法,线程A就会停止继续执行代码,等待线程B结束。 线程B结束后,线程A才继续执行后续的代码。

所以主线程在执行上面的代码时,就暂停在此处, 一直要等到 新线程执行完毕,退出后,才会继续执行后续的代码。

join通常用于 主线程把任务分配给几个子线程,等待子线程完成工作后,需要对他们任务处理结果进行再处理。

就好像一个领导把任务分给几个员工,等几个员工完成工作后,他需要收集他们提交的报告,进行后续处理。

这种情况,主线程必须子线程完成才能进行后续操作,所以join就是 等待参数对应的线程完成,才返回。

共享数据的访问控制

做多线程开发,经常遇到这样的情况:多个线程里面的代码 需要访问 同一个 公共的数据对象。

这个公共的数据对象可以是任何类型, 比如一个 列表、字典、或者自定义类的对象。

有的时候,程序 需要 防止线程的代码 同时操作 公共数据对象。 否则,就有可能导致 数据的访问互相冲突影响。

请看一个例子。

我们用一个简单的程序模拟一个银行系统,用户可以往自己的帐号上存钱。

对应代码如下:

from threading import Thread
from time import sleep

bank = {
    'byhy' : 0
}

# 定义一个函数,作为新线程执行的入口函数
def deposit(theadidx,amount):
    balance =  bank['byhy']
    # 执行一些任务,耗费了0.1秒
    sleep(0.1)
    bank['byhy']  = balance + amount
    print(f'子线程 {theadidx} 结束')

theadlist = []
for idx in range(10):
    thread = Thread(target = deposit,
                    args = (idx,1)
                    )
    thread.start()
    # 把线程对象都存储到 threadlist中
    theadlist.append(thread)

for thread in theadlist:
    thread.join()

print('主线程结束')
print(f'最后我们的账号余额为 {bank["byhy"]}')

上面的代码中,一起执行

开始的时候, 该帐号的余额为0,随后我们启动了10个线程, 每个线程都deposit函数,往帐号byhy上存1元钱。

可以预期,执行完程序后,该帐号的余额应该为 10。

然而,我们运行程序后,发现结果如下

子线程 0 结束
子线程 3 结束
子线程 2 结束
子线程 4 结束
子线程 1 结束
子线程 7 结束
子线程 5 结束
子线程 9 结束
子线程 6 结束
子线程 8 结束
主线程结束
最后我们的账号余额为 1

为什么是 1 呢? 而不是 10 呢?
如果在我们程序代码中,只有一个线程,如下所示

from time import sleep

bank = {
    'byhy' : 0
}

# 定义一个函数,作为新线程执行的入口函数
def deposit(theadidx,amount):
    balance =  bank['byhy']
    # 执行一些任务,耗费了0.1秒
    sleep(0.1)
    bank['byhy']  = balance + amount

for idx in range(10):
    deposit (idx,1)

print(f'最后我们的账号余额为 {bank["byhy"]}')

代码都是 串行 执行的。 不存在多线程同时访问 bank对象 的问题,运行结果一切都是正常的。

现在我们程序代码中,有多个线程,并且在这个几个线程中都会去调用 deposit,就有可能同时操作这个bank对象,就有可能出一个线程覆盖另外一个线程的结果的问题。

这时,可以使用 threading库里面的锁对象 Lock 去保护。

我们修改多线程代码,如下:

from threading import Thread,Lock
from time import sleep

bank = {
    'byhy' : 0
}

bankLock = Lock()

# 定义一个函数,作为新线程执行的入口函数
def deposit(theadidx,amount):
    # 操作共享数据前,申请获取锁
    bankLock.acquire()
    
    balance =  bank['byhy']
    # 执行一些任务,耗费了0.1秒
    sleep(0.1)
    bank['byhy']  = balance + amount
    print(f'子线程 {theadidx} 结束')
    
    # 操作完共享数据后,申请释放锁
    bankLock.release()

theadlist = []
for idx in range(10):
    thread = Thread(target = deposit,
                    args = (idx,1)
                    )
    thread.start()
    # 把线程对象都存储到 threadlist中
    theadlist.append(thread)

for thread in theadlist:
    thread.join()

print('主线程结束')
print(f'最后我们的账号余额为 {bank["byhy"]}')

执行一下,结果如下

子线程 0 结束
子线程 1 结束
子线程 2 结束
子线程 3 结束
子线程 4 结束
子线程 5 结束
子线程 6 结束
子线程 7 结束
子线程 8 结束
子线程 9 结束
主线程结束
最后我们的账号余额为 10

正确了。

Lock 对象的acquire方法 是申请锁。

每个线程在 操作共享数据对象之前,都应该 申请获取操作权,也就是 调用该 共享数据对象对应的锁对象的acquire方法。

如果线程A 执行如下代码,调用acquire方法的时候,

bankLock.acquire()

别的线程B 已经申请到了这个锁, 并且还没有释放,那么 线程A的代码就在此处 等待 线程B 释放锁,不去执行后面的代码。

直到线程B 执行了锁的 release 方法释放了这个锁, 线程A 才可以获取这个锁,就可以执行下面的代码了。

如果这时线程B 又执行 这个锁的acquire方法, 就需要等待线程A 执行该锁对象的release方法释放锁, 否则也会等待,不去执行后面的代码。

daemon线程

大家执行下面的代码

from threading import Thread
from time import sleep

def threadFunc():
    sleep(2)
    print('子线程 结束')

thread = Thread(target=threadFunc)
thread.start()
print('主线程结束')

可以发现,主线程先结束,要过个2秒钟,等子线程运行完,整个程序才会结束退出。

因为:

Python程序中当所有的 非daemon线程 结束了,整个程序才会结束
主线程是非daemon线程,启动的子线程缺省也是 非daemon 线程。

所以,要等到 主线程和子线程 都结束,程序才会结束。

我们可以在创建线程的时候,设置daemon参数值为True,如下

from threading import Thread
from time import sleep

def threadFunc():
    sleep(2)
    print('子线程 结束')

thread = Thread(target=threadFunc,
                daemon=True # 设置新线程为daemon线程
                )
thread.start()
print('主线程结束')

再次运行,可以发现,只要主线程结束了,整个程序就结束了。因为只有主线程是非daemon线程。

多进程

Python 官方解释器 的每个线程要获得执行权限,必须获取一个叫 GIL (全局解释器锁) 的东西。

这就导致了 Python 的多个线程 其实 并不能同时使用 多个CPU核心。

所以如果是计算密集型的任务,不能采用多线程的方式。

大家可以运行一下如下代码

from threading import Thread

def f():
    while True:
        b = 53*53

if __name__ == '__main__':
    plist = []
    # 启动10个线程
    for i in range(10):
        p = Thread(target=f)
        p.start()
        plist.append(p)

    for p in plist:
        p.join()

运行后,打开任务管理器,可以发现 即使是启动了10个线程,依然只能占用一个CPU核心的运算能力。

如果需要利用电脑多个CPU核心的运算能力,可以使用Python的多进程库,如下

from multiprocessing import Process

def f():
    while True:
        b = 53*53

if __name__ == '__main__':
    plist = []
    for i in range(2):
        p = Process(target=f)
        p.start()
        plist.append(p)

    for p in plist:
        p.join()

运行后,打开任务管理器,可以发现 有3个Python进程,其中主进程CPU占用率为0,两个子进程CPU各占满了一个核心的运算能力。

仔细看上面的代码,可以发现和多线程的使用方式非常类似。

还有一个问题,主进程如何获取 子进程的 运算结果呢?

可以使用多进程库 里面的 Manage 对象,如下

from multiprocessing import Process,Manager
from time import sleep

def f(taskno,return_dict):
    sleep(2)
    # 存放计算结果到共享对象中
    return_dict[taskno] = taskno

if __name__ == '__main__':
    manager = Manager()
    # 创建 类似字典的 跨进程 共享对象
    return_dict = manager.dict()
    plist = []
    for i in range(10):
        p = Process(target=f, args=(i,return_dict))
        p.start()
        plist.append(p)

    for p in plist:
        p.join()

    print('get result...')
    # 从共享对象中取出其他进程的计算结果
    for k,v in return_dict.items():
        print (k,v)

  Python知识库 最新文章
Python中String模块
【Python】 14-CVS文件操作
python的panda库读写文件
使用Nordic的nrf52840实现蓝牙DFU过程
【Python学习记录】numpy数组用法整理
Python学习笔记
python字符串和列表
python如何从txt文件中解析出有效的数据
Python编程从入门到实践自学/3.1-3.2
python变量
上一篇文章      下一篇文章      查看所有文章
加:2021-09-04 17:28:29  更:2021-09-04 17:30:51 
 
开发: 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年11日历 -2024/11/15 13:00:38-

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