多线程理论及操作

【一】什么是线程

  • 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程

  • 线程顾名思义,就是一条流水线工作的过程

    • 一条流水线必须属于一个车间,一个车间的工作过程是一个进程

    • 车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线

    • 流水线的工作需要电源,电源就相当于cpu

  • 所以进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。

  • 多线程(即多个控制线程)的概念是在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。

  • 例如

    • 北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。

【1】示例:

  • 进程

    • 资源单位
  • 线程

    • 执行单位
  • 将操作系统比喻成大的工厂

    • 进程相当于工厂里面的车间

    • 线程相当于车间里面的流水线

【2】小结

  • 每一个进程必定自带一个线程

  • 进程:资源单位

    • 起一个进程仅仅只是 在内存空间中开辟出一块独立的空间
  • 线程:执行单位

    • 真正被CPU执行的其实是进程里面的线程

    • 线程指的就是代码的执行过程,执行代码中所需要使用到的资源都找所在的进程索要

  • 进程和线程都是虚拟单位,只是为了我们更加方便的描述问题

【二】线程的创建开销

【1】创建进程的开销要远大于线程

  • 如果我们的软件是一个工厂

  • 该工厂有多条流水线

  • 流水线工作需要电源

  • 电源只有一个即cpu(单核cpu)

    • 一个车间就是一个进程
      • 一个车间至少一条流水线(一个进程至少一个线程)
    • 创建一个进程
      • 就是创建一个车间(申请空间,在该空间内建至少一条流水线)
    • 而建线程
      • 就只是在一个车间内造一条流水线

      • 无需申请空间,所以创建开销小

【2】进程之间是竞争关系,线程之间是协作关系

  • 车间直接是竞争/抢电源的关系,竞争

    • 不同的进程直接是竞争关系

    • 不同的程序员写的程序运行的迅雷抢占其他进程的网速

    • 360把其他进程当做病毒干死

  • 一个车间的不同流水线式协同工作的关系

    • 同一个进程的线程之间是合作关系,是同一个程序写的程序内开启动

    • 迅雷内的线程是合作关系,不会自己干自己

【三】线程和进程的区别

  • Threads share the address space of the process that created it; processes have their own address space.

    • 线程共享创建它的进程的地址空间; 进程具有自己的地址空间。
  • Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.

    • 线程可以直接访问其进程的数据段; 进程具有其父进程数据段的副本。
  • Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.

    • 线程可以直接与其进程中的其他线程通信; 进程必须使用进程间通信与同级进程进行通信。
  • New threads are easily created; new processes require duplication of the parent process.

    • 新线程很容易创建; 新进程需要复制父进程。
  • Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.

    • 线程可以对同一进程的线程行使相当大的控制权。 进程只能控制子进程。
  • Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.

    • 对主线程的更改(取消,优先级更改等)可能会影响该进程其他线程的行为; 对父进程的更改不会影响子进程。

【四】为何要有多线程

【1】开设进程

  • 申请内存空间 -- 耗资源

  • 拷贝代码 - 耗资源

【2】开设线程

  • 一个进程内可以开设多个线程

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

【3】总结线程的优点

  • 减少了资源的消耗

  • 同一个进程下的多个线程资源共享

【4】什么是多线程

  • 多线程指的是

    • 在一个进程中开启多个线程

    • 简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。

  • 多线程共享一个进程的地址空间

    • 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
  • 若多个线程都是cpu密集型的,那么并不能获得性能上的增强

    • 但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
  • 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于Python)

【五】开设多线程的两种方式

【1】方式一:直接调用Thread

复制代码
from multiprocessing import Process
from threading import Thread
import time
​
​
def task(name):
    print(f'当前任务:>>>{name} 正在运行')
    time.sleep(3)
    print(f'当前任务:>>>{name} 结束运行')
​
​
def Thread_main():
    t = Thread(target=task, args=("dream",))
    # 创建线程的开销非常小,几乎代码运行的一瞬间线程就已经创建了
    t.start()
    '''
    当前任务:>>>dream 正在运行this is main process!
    this is main process!
    当前任务:>>>dream 结束运行
    '''
​
​
def Process_main():
    p = Process(target=task, args=("dream",))
    p.start()
    '''
    this is main process!
    当前任务:>>>dream 正在运行
    当前任务:>>>dream 结束运行
    '''
​
​
if __name__ == '__main__':
    Thread_main()
    # Process_main()
    print('this is main process!')

【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('dream')
    t.start()
    print(f'this is a main process')
​
    """
    dream is running
    this is a main process
    dream is ending
    """
​
​
if __name__ == '__main__':
    main()

【三】一个进程下开启多个线程和多个子进程的区别

【1】线程比进程速度快

复制代码
from threading import Thread
from multiprocessing import Process
import time
​
​
def work():
    print('hello')
​
​
def timer(func):
    def inner(*args, **kwargs):
        start_time = time.time()
        res = func(*args, **kwargs)
        print(f'函数 {func.__name__} 运行时间为:{time.time() - start_time}')
        return res
​
    return inner
​
​
@timer
def work_process():
    # 在主进程下开启子进程
    t = Process(target=work)
    t.start()
    print('主线程/主进程')
    '''
    主线程/主进程
    函数 work_process 运行时间为:0.0043752193450927734
    hello
    '''
​
​
@timer
def work_thread():
    # 在主进程下开启线程
    t = Thread(target=work)
    t.start()
    print('主线程/主进程')
    '''
    打印结果:
    hello
    主线程/主进程
    函数 work_thread 运行时间为:0.0001499652862548828
    '''
​
​
if __name__ == '__main__':
    # part1 : 多线程
    work_thread()
    # part2 : 多进程
    work_process()

【2】查看pid

复制代码
from threading import Thread
from multiprocessing import Process
import os
​
​
def work():
    print('hello', os.getpid())
​
​
def work_thread():
    # part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1 = Thread(target=work)
    t2 = Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/主进程pid', os.getpid())
​
    # hello 5022
    # hello 5022
    # 主线程/主进程pid 5022
​
​
def work_process():
    # part2:开多个进程,每个进程都有不同的pid
    p1 = Process(target=work)
    p2 = Process(target=work)
    p1.start()
    p2.start()
    print('主线程/主进程pid', os.getpid())
​
    # 主线程/主进程pid 5032
    # hello 5034
    # hello 5035
​
​
if __name__ == '__main__':
    # part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    work_thread()
    # part2:开多个进程,每个进程都有不同的pid
    work_process()

【3】同一进程内的线程共享进程内的数据

复制代码
from threading import Thread
from multiprocessing import Process
​
​
def work():
    global n
    n = 0
​
​
def work_process():
    n = 100
    p = Process(target=work)
    p.start()
    p.join()
    print('主', n)  # 毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100
​
    # 主 100
​
​
def work_thread():
    n = 1
    t = Thread(target=work)
    t.start()
    t.join()
    print('主', n)  # 查看结果为1,因为同一进程内的线程之间共享进程内的数据
​
​
if __name__ == '__main__':
    # part1 多进程 : 子进程只改自己的
    work_process()
    # part2 多线程: 数据发生错乱,同一进程内的线程之间共享数据
    work_thread()

【四】守护线程

【1】主线程死亡,子线程未死亡

  • 主线程结束运行后不会马上结束,而是等待其他非守护子线程结束之后才会结束

  • 如果主线程死亡就代表者主进程也死亡,随之而来的是所有子线程的死亡

复制代码
from threading import Thread
import time
​
​
def work(name):
    print(f"当前{name} 是开始\n")
    time.sleep(2)
    print(f"当前{name} 是结束")
​
​
def main():
    print(f'这是主函数main开始')
    task = Thread(target=work,args=('knight',))
    task.start()
    print(f'这是主函数main结束')
​
​
if __name__ == '__main__':
    main()
​
# 这是主函数main开始
# 当前knight 是开始
# 这是主函数main结束
​
# 当前knight 是结束

【2】主线程死亡,子线程也死亡

复制代码
from threading import Thread
import time
​
​
def work(name):
    print(f"当前{name} 是开始\n")
    time.sleep(2)
    print(f"当前{name} 是结束")
​
​
def main():
    print(f'这是主函数main开始')
    task = Thread(target=work,args=('knight',))
    task.daemon = True  # 开启守护进程,主线程结束,子线程也随之结束
    task.start()
    print(f'这是主函数main结束')
​
​
if __name__ == '__main__':
    main()
​
# 这是主函数main开始
# 当前knight 是开始
# 这是主函数main结束

示例:对比是否被守护进程的区别

复制代码
# 导入所需模块
from threading import Thread
import time
​
​
# 定义函数foo,模拟一个耗时操作
def foo():
    # 打印开始信息
    print(f' this is foo begin')
    # 模拟耗时操作,暂停3秒
    time.sleep(3)
    # 打印结束信息
    print(f' this is foo end')
​
​
# 定义另一个函数func,同样模拟耗时操作
def func():
    # 打印开始信息
    print(f' this is func begin')
    # 模拟耗时操作,暂停1秒
    time.sleep(1)
    # 打印结束信息
    print(f' this is func end')
​
​
# 主函数
def main():
    # 创建线程 task_foo ,目标函数为foo
    task_foo = Thread(target=foo)
    # 设置 task_foo 为守护线程
    # 意味着当主线程结束时,不论 task_foo 是否执行完毕都会被强制终止
    task_foo.daemon = True
    # 创建线程 task_func ,目标函数为func
    task_func = Thread(target=func)
​
    # 启动线程 task_foo
    task_foo.start()
    # 启动线程 task_func
    task_func.start()
​
    # 主线程继续执行,打印以下信息
    print(f' this is main')
​
​
# 程序入口
if __name__ == '__main__':
    main()
    #  this is main begin 
    #  this is foo begin
    #  this is func begin
    #  this is main end
    #  this is func end

执行过程

(1) 初始化阶段
  • 程序开始执行时,首先会导入所需的模块,并定义两个函数foo()func()

  • 这两个函数分别代表了两个需要并发执行的任务。

(2)线程创建与启动
  • main()函数中

  • 首先通过Thread类创建了两个线程实例t1t2

  • 其中t1的目标函数是foot2的目标函数是func

  • 然后将t1设置为守护线程(daemon=True),这意味着当主线程结束时,即使t1尚未执行完毕也会被系统终止。

  • 之后,两个线程通过start()方法启动,这意味着它们将异步地执行各自的目标函数。

原理分析

(1)并发执行
    • t1开始执行,打印出"this is foo begin",随后进入3秒的等待状态。

    • 几乎同时,t2也开始执行,打印出"this is func begin",并进入1秒的等待状态。

    • 由于线程调度机制,实际的打印顺序可能会略有不同,但通常情况下func()会先于foo()结束,因为它的等待时间较短。

(2)主线程执行
  • 主线程继续执行,打印出"this is main"。

  • 由于t1被设置为守护线程,即便它还在睡眠中,当主线程执行结束后,整个程序也会直接终止,此时t1不论是否完成都会被系统停止。

  • t2作为一个非守护线程,如果在主线程结束前已完成,则正常结束,否则也会随程序终止。

【五】线程的互斥锁

  • 所有子线程都会进行阻塞操作,导致最后的改变只是改了一次
复制代码
from threading import Thread
import time

money = 100


def work():
    global money

    # 模拟获取到车票信息
    temp = money

    # 模拟网络延迟
    time.sleep(2)

    # 模拟购票
    money = temp - 1


def main():
    task_list = [Thread(target=work) for i in range(100)]
    [task.start() for task in task_list]
    [task.join() for task in task_list]
    print(money)


if __name__ == '__main__':
    main()

    # 99

解决方法

  • 在数据发生改变的地方进行加锁处理
复制代码
from threading import Thread,Lock
import time

money = 100
mutex = Lock()


def work():
    global money

    # 数据发生改变之前加锁
    mutex.acquire()

    # 模拟获取到车票信息
    temp = money

    # 模拟网络延迟
    time.sleep(1)

    # 模拟购票
    money = temp - 1

    # 数据改变之后解锁
    mutex.release()


def main():
    task_list = [Thread(target=work) for i in range(100)]
    [task.start() for task in task_list]
    [task.join() for task in task_list]
    print(money)


if __name__ == '__main__':
    main()


    # 0
相关推荐
databook17 小时前
Manim实现闪光轨迹特效
后端·python·动效
Juchecar18 小时前
解惑:NumPy 中 ndarray.ndim 到底是什么?
python
用户83562907805118 小时前
Python 删除 Excel 工作表中的空白行列
后端·python
Json_18 小时前
使用python-fastApi框架开发一个学校宿舍管理系统-前后端分离项目
后端·python·fastapi
数据智能老司机1 天前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机1 天前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机1 天前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机1 天前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i1 天前
drf初步梳理
python·django
每日AI新事件1 天前
python的异步函数
python