多线程

进程和线程的概念

程序员面试的时候,经常会问 进程和线程的区别。

我们写的python程序(或者其他应用程序比如画笔、qq等),运行起来,就称之为一个进程

在windows下面有 任务管理器,里面显示了当前系统上运行着的进程。

image

可以看到,我们系统中有很多的进程运行着,比如qq等。

这些程序还没有运行的时候,它们的执行代码存储在磁盘文件中,就是那些 .exe 文件。

双击它们,就被os加载到内存中,运行起来,成为进程

简单的说:进程就是一个个运行着的程序


而系统中每个进程里面至少包含一个 线程

线程是操作系统创建的,用来控制代码执行的数据结构,保存了代码执行过程中的重要的状态信息。

没有线程,操作系统没法管理和维护 代码运行的状态信息。

没有创建线程之前,操作系统是不会执行我们的代码的。

我们大家的写的Python程序,大部分里面虽然没有创建线程的代码,

但是实际上,当Python解释器程序运行起来(成为一个进程),OS就自动的创建一个线程,通常称为主线程,在这个主线程里面执行代码指令。

当解释器执行我们python程序代码的时候。 我们的代码就在这个主线程中解释执行。


下面这个程序,运行起来后,只有一个线程,就是主线程,在主线程里面,代码按照流程一直执行到结束, 主线程就退出了。 同时进程也结束了。

fee = input('请输入午餐费用:')
members = input('请输入聚餐人姓名,以英文逗号,分隔:')

# 将人员放入一个列表
memberlist = members.split(',') 
# 得到人数
headcount = len(memberlist) 

# 计算人均费用
avgfee = int (fee) / headcount
print(avgfee)


现代计算机上面,CPU是多核心的。 CPU执行一个线程的代码,就会分配一个核心去执行该代码。

有的时候,我们的程序希望,能够让更多的CPU核心同时执行我们的程序里面的一些代码。

比如,我们程序里面有个函数,执行压缩文件的任务,现在有4个大文件,需要压缩。

假如如果是一个CPU核心执行这个函数,压缩一个文件要10秒钟,压缩4个,就要40秒。

而如果我们能够让4个CPU核心能同时去执行压缩函数, 理论上就只要 10秒。

但是如果执行的程序里面只有一个线程,执行过程中,只会有一个CPU核心去执行,那么就需要40秒。

而要同时让多个CPU去执行任务,必须创建新的线程。

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=threadFunc,
                args=('参数1', '参数2')
                )

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

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

解释器执行上面的代码, 在这一句的时候

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

只是创建了一个Thread实例对象, 并没有创建新的线程。

要创建线程,必须要调用 该Thread 实例对象的start方法。

上面的Thread类的初始化参数args,是传给 入口函数threadFunc的参数。 所有的参数,必须放在一个元组里面,里面的元素依次对应入口函数的参数。


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

共享数据的访问控制

我们做多线程开发的时候,经常要面临的一个问题就是: 多个线程里面执行的代码 需要访问 公共的数据对象。

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

这时候,我们应该有相应的措施保证: 同时只能有一个线程的代码操作公共的数据对象, 不然就有可能会导致 数据的访问互相冲突影响。

请看一个例子。 我有一个银行账号,并且我会在这个银行账号存款和取款。

对应代码如下:

from threading import Thread,Lock
from random import randint
from time import sleep

# 银行账号类
class BankAccount:

    def __init__(self):
        # 账户余额,初识值为0
        self.balance = 0  

        # 创建锁对象,保护共享数据 balance
        self.balanceLock = Lock()

    #  存款
    def  deposit(self,amount):
        print('存款操作开始')
        # 访问共享数据前必须申请锁
        self.balanceLock.acquire()

        #为了演示效果,随机等待一段时间,
        sleep(randint(1,3))
        # 操作共享数据
        self.balance += amount

        # 访问共享数据后必须释放锁
        self.balanceLock.release() 
        print('存款操作结束')

    # 取款
    def withdrawal(self,amount):
        print('取款操作开始')
        # 访问共享数据前必须申请锁
        self.balanceLock.acquire()

        #为了演示效果,随机等待一段时间,
        sleep(randint(1,3))
        
        # 操作共享数据 
        self.balance -= amount

        # 访问共享数据后必须释放锁
        self.balanceLock.release()
        print('取款操作结束')
        
# 我的银行账号
myaccount = BankAccount()


# 创建一个新线程,执行存款2000操作
thread1 = Thread(target=myaccount.deposit,    
                 args=(2000,))

# 再创建一个新线程,执行取款500操作
thread2 = Thread(target=myaccount.withdrawal,  
                 args=(500,))

# 启动上面的两个子线程,
# 在两个子线程里面,分别执行存款 和取款 方法
thread1.start()
thread2.start()

# 等待两个子线程执行结束
thread1.join()
thread2.join()

print (f'最后我们的账号余额为 {myaccount.balance}')

上面的代码中, 银行账号类 的余额属性 balance 是 该类的方法代码要访问的数据。

如果在我们程序代码中,始终只有一个线程,那么 该类的方法访问这个balance属性是不需要什么特别的保护措施的。

如果在我们程序代码中,可能会有多线程,并且在这个几个线程中可能都会去调用 该类的方法。 那么为了程序的健壮性,访问这个balance属性通常需要用 去保护。

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

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

self.balanceLock.acquire()

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

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

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




课后练习

去做练习