• python多线程是如何工作


    一、进程、线程、协程的相关概念
    1、进程、线程、协程定义
    (1)进程是系统进行资源分配和调度的独立单位
    (2)线程是进程的实体,是CPU调度和分派的基本单位
    (3)协程也是线程,称微线程,自带CPU上下文,是比线程更小的执行单元
    2、进程和线程的区别
    一个程序至少有一个进程,一个进程至少有一个线程。线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率;线程不能够独立执行,必须依存在进程中。
    3、进程和线程的优缺点
    线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
    4、协程相关
    将把一个进程比作生活中的饭店,则保证饭店正常运行的服务员就是线程,服务好每桌客人就是线程要完成的任务。
      当用多线程完成任务时,采取以下模式:每来一桌的客人,该桌子就安排一个服务员,即有多少桌客人就得对应多少个服务员。
      而当我们用协程来完成任务时,模式如下:就安排一个服务员,来吃饭得有一个点餐和等菜的过程,当A在点菜,就去B服务,B叫了菜在等待,我就去C,当C也在等菜并且A点菜点完了,赶紧到A来服务… …依次类推
      从上面的例子可以看出,想要使用协程,那么我们的任务必须有等待。当我们要完成的任务有耗时任务,属于IO密集型任务时,我们使用协程来执行任务会节省很多的资源(一个服务员和多个服务员的区别), 并且可以极大的利用到系统的资源。
    二、多进程例子
    1、Unix/Linux操作系统->fork()系统调用
    fork()非常特殊。普通的函数调用,调用一次,返回一次。但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
    Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程

    import os
    print('Process (%s) start...' % os.getpid())
    pid = os.fork()
    if pid == 0:
    	print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
    else:
    	print('I (%s) just created a child process (%s).' % (os.getpid(), pid))	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    输出结果为:

    Process (163320) start...
    I (163320) just created a child process (163337).
    I am child process (163337) and my parent is 163320.
    
    • 1
    • 2
    • 3

    2、multiprocessing模块
    由于fork()只支持Unix/Linux系统,无法在Windows系统下运行。所以fork()不支持跨平台操作。由于python是跨平台的,提供了multiprocessing模块,该模块可以支持跨平台的多进程操作。multiprocessing模块提供了一个Process类来代表一个进程对象。

    import os
    from multiprocessing import Process
    # 子进程要执行的代码
    def run_proc(name):
        print('Run child process %s (%s)...' % (name, os.getpid()))
    if __name__=='__main__':
    	print('Parent process %s.' % os.getpid())
        p = Process(target=run_proc, args=('test',))
        print('Child process will start.')
        p.start() # 启动子进程
        p.join() #等待子进程结束
        print('Child process end.')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输出为:

    Parent process 163928.
    Child process will start.
    Run child process test (163945)...
    Child process end.
    
    • 1
    • 2
    • 3
    • 4

    创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
    3、Pool模块
    如果需要启动大量的子进程,可以用进程池的方式批量创建子进程,multiprocessing中的Pool提供了创建进程池的方法。

    from multiprocessing import Pool
    import os, time, random
    def long_time_task(name):
        print('Run task %s (%s)...' % (name, os.getpid()))
        start = time.time()
        time.sleep(random.random() * 3)
        end = time.time()
        print('Task %s runs %0.2f seconds.' % (name, (end - start)))
    if __name__=='__main__':
        print('Parent process %s.' % os.getpid())
        p = Pool(4)
        for i in range(5):
            p.apply_async(long_time_task, args=(i,))
        print('Waiting for all subprocesses done...')
        p.close()
        p.join()
        print('All subprocesses done.')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    输出为:

    Parent process 165248.
    Waiting for all subprocesses done...
    Run task 0 (165265)...
    Run task 1 (165266)...
    Run task 2 (165267)...
    Run task 3 (165268)...
    Task 3 runs 0.01 seconds.
    Run task 4 (165268)...
    Task 2 runs 0.50 seconds.
    Task 0 runs 1.19 seconds.
    Task 4 runs 1.20 seconds.
    Task 1 runs 2.50 seconds.
    All subprocesses done.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。由于Pool的默认大小是CPU的核数,至少提交大于核数的子进程才能看到上面的等待效果。
    4、子进程
    子进程往往并不是自身,而是一个外部进程。在创建子进程后,还需要控制子进程的输入和输出。subprocess模块可以非常方便地启动一个子进程,然后控制起输入和输出。

    import subprocess
    print('$ nslookup www.python.org')
    r = subprocess.call(['nslookup', 'www.python.org'])
    print('Exit code:', r)
    
    • 1
    • 2
    • 3
    • 4

    输出结果为:

    $ nslookup www.python.org
    Server:		127.0.0.53
    Address:	127.0.0.53#53
    
    Non-authoritative answer:
    www.python.org	canonical name = dualstack.python.map.fastly.net.
    Name:	dualstack.python.map.fastly.net
    Address: 146.75.112.223
    Name:	dualstack.python.map.fastly.net
    Address: 2a04:4e42:1a::223
    
    exit code: 0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如果子进程还需要输入,则可以通过communicate()方法输入。
    5、进程间通信
    Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。以Queue为例,见下方代码

    from multiprocessing import Process, Queue
    import os, time, random
    def write(q):
        print('Process to write: %s' % os.getpid())
        for value in ['a','b','c']:
            print('put %s to queue...' % value)
            q.put(value)
            time.sleep(random.random())
    
    def read(q):
        print('Process to read: %s' % os.getpid())
        while True:
            value = q.get(True)
            print('get %s from Queue.' % value)
    if __name__ == "__main__" :
     q = Queue()
     pw = Process(target=write,args=(q,))
     pr = Process(target=read,args=(q,))
     pw.start()
     pr.start()
     pw.join()
     pr.join()
     pr.terminate()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    三、多线程例子
    1、threading模块
    当我们面临多任务的时候,可以使用多个进程完成,但同时也可以在一个进程内由多个线程完成。线程是操作系统直接支持的执行单元。Python中的线程是真正的Posix Thread,而不是模拟出来的线程
    Python的标准库中提供了两个模块:_thread和threading,_thread是低级模块,threading是对_thread封装的高级模块。因此我们在使用时只需用threading模块。
    启动一个线程和启动一个进程的过程基本一样,均是将函数传入进程(Process)模块或者线程(Thread)模块,并通过传入函数的方式对线程或进程进行实例化,最后调用start()开始执行,以下方代码为例

    import time, threading
    def loop():
        print('thread %s is running...'% threading.current_thread().name)
        n = 0
        while n < 5:
            n = n + 1
            print('thread %s >>> %s' % (threading.current_thread().name,n))
            time.sleep(1)
        print('thread %s ended.' % threading.current_thread().name)
    if __name__ == "__main__" :
      	print('thread %s is running...' % threading.current_thread().name)
      	t = threading.Thread(target=loop,name='LoopThread')
      	t.start()
      	t.join()
      	print('thread %s ended.' % threading.current_thread().name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    输出结果为:

    thread MainThread is running...
    thread LoopThread is running...
    thread LoopThread >>> 1
    thread LoopThread >>> 2
    thread LoopThread >>> 3
    thread LoopThread >>> 4
    thread LoopThread >>> 5
    thread LoopThread ended.
    thread MainThread ended.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    从上方代码可以看出,任何进程都会默认启动一个线程,将该线程成为主线程,主线程中又可启动新的线程。threading.current_thread().name永远返回当前线程的实例。
    2、Lock
    多线程和多进程最大的区别就是多线程共享一个进程的内存,所有变量由所有线程共享,任何一个变量都有可能被任何一个线程修改,因此在多进程编程中,往往要注意多个线程对同一个变量修改的问题(原因为线程之间是交替运行,一个线程运行过程中可能中断,但在另一个线程中却在修改)。而多进程中,即使执行同一个代码,同一个变量也会拷贝在各自进程中的内存,互不影响。
    为了解决线程之间可能会修改同一变量的问题,确保多线程程序可以正常运行,应该给线程中运行的函数加锁(Lock),即在运行加锁的线程时,其他的进程不可执行进程中的函数,只有当锁被释放之后,获得该锁的线程才能修改。无论有多少线程,锁只有一个,且同一时刻只有一个线程持有该锁,所以不会造成修改的冲突。python中创建锁是通过threading.Lock()实现。以下为代码示例:

    import threading
    balance = 0
    lock = threading.Lock()
    def change_it(n):
        global balance
        balance = balance + n
        balance = balance - n
    
    def run_thread(n):
    
        for i in range(2000000):
            lock.acquire()
            try:
                change_it(n)
            finally:
                lock.release()
    t1 = threading.Thread(target=run_thread, args=(5,))
    t2 = threading.Thread(target=run_thread, args=(8,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print(balance)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    lock.acquire()为线程获得锁,当线程用完之后一定要释放锁,通过lock.release()释放,否则线程将是死线程。可以用try…finally来确保锁一定会被释放。在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。
    3、

  • 相关阅读:
    主数据管理是数字化转型成功的基石——江淮汽车案例分享
    大学生个人网页模板 简单网页制作作业成品 极简风格个人介绍HTML网页设计(舞蹈培训网页)
    幸福消费成酒店投资趋势红利,荟语酒店凭何打造品牌核心优势
    靶场战神为何会陨落?
    实体类属性名与数据库列名不一致解决方案
    存档&改造【03】Apex-Fancy-Tree-Select花式树的导入及学习
    Ultipa Transporter V4.3.22 即将发布,解锁更多易用功能!
    Web3D虚拟人制作简明指南
    C# 中的“智能枚举”:如何在枚举中增加行为
    Selenium IDE 工具
  • 原文地址:https://blog.csdn.net/qq_31511955/article/details/126864867