Python进阶05-多线程

135 阅读12分钟

Python进阶05-多线程

1、进程

(1)单任务

  • 单任务:指在同一时间内只执行单个任务。
import time

# 定义一个函数,用于实现听音乐
def music():
    for i in range(3):
        print('正在听音乐...')
        time.sleep(0.2)

# 定义一个函数,用于实现写代码
def coding():
    for i in range(3):
        print('正在写代码...')
        time.sleep(0.2)

# 定义一个程序执行的入口
if __name__ == '__main__':
    music()
    coding()

(2)多任务

  • 多任务:指在同一时间内执行多个任务。
  • 例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。

image-20240821114918460

  • 多任务的两种表现形式

    • 并发:在一段时间内交替去执行多个任务。
    • 并行:在一段时间内真正的同时一起执行多个任务。
  • 并发案例

    • 对于单核cpu处理多任务,操作系统轮流让各个任务交替执行,假如:软件1执行0.01秒,切换到软件2,软件2执行0.01秒,再切换到软件3,执行0.01秒……这样反复执行下去 , 实际上每个软件都是交替执行的 . 但是,由于CPU的执行速度实在是太快了,表面上我们感觉就像这些软件都在同时执行一样 . 这里需要注意单核cpu是并发的执行多任务的。

    image-20240821145715583

  • 并行案例

    • 对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的任务,多个内核是真正的一起同时执行多个任务。这里需要注意多核cpu是并行的执行多任务,始终有多个任务一起执行。

      image-20240821153338822

(3)进程

  • 进程(Process)是资源分配的最小单位,它是操作系统进行资源分配和调度运行的基本单位。一个程序运行后至少有一个进程。
  • 通俗理解:一个正在运行的程序就是一个进程 . 例如:正在运行的qq , 微信等 他们都是一个进程。

(4)多进程完成多任务

  • 多进程是Python程序中实现多任务的一种方式,使用多进程可以大大提高程序的执行效率 。

image-20240822222658006

(5)进程创建

  • 进程的创建步骤

    • 导入进程包:import multiprocessing
    • 通过进程类创建进程对象:进程对象 = multiprocessing.Process()
    • 启动进程执行任务:进程对象.start()
  • 通过进程类创建进程对象参数说明:进程对象 = multiprocessing.Process(target=任务名)

    • image-20240822222718690

    • import time
      # 第一步:导入多进程包
      import multiprocessing
      
      # 定义一个music()函数,用于实现听音乐功能
      def music():
          for i in range(3):
              print('正在听音乐...')
              time.sleep(0.2)
      
      # 定义一个coding()函数,用于实现写代码功能
      def coding():
          for i in range(3):
              print('正在写代码...')
              time.sleep(0.2)
      
      # 定义一个程序的执行入口
      if __name__ == '__main__':
          # ... 主进程
          # 第二步:在主进程中创建两个子进程
          music_process = multiprocessing.Process(target=music)
          coding_process = multiprocessing.Process(target=coding)
          # 第三步:启动刚才创建的子进程
          music_process.start()
          coding_process.start()
      
  • 进程执行带有参数的任务

    • image-20240822224858178

    • args参数的使用

      • # target: 进程执行的函数名
        # args: 表示以元组的方式给函数传参
        sing_process = multiprocessing.Process(target=music, args=(3,))
        sing_process.start()
        
    • kwargs参数的使用

      • # target: 进程执行的函数名
        # kwargs: 表示以字典的方式给函数传参
        dance_process = multiprocessing.Process (target=coding, kwargs={“t": 0.2})
        # 启动进程
        dance_process.start()
        
    • 注意点:

      • 元组方式传参 :元组方式传参一定要和参数的顺序保持一致
      • 字典方式传参:字典方式传参字典中的key一定要和参数名保持一致
    • import time
      # 第一步:导入多任务包
      import multiprocessing
      
      # 定义一个music()函数,用于实现听音乐功能
      # 参数n代表循环次数
      def music(n):
          for i in range(n):
              print('正在听音乐...')
              time.sleep(0.2)
      
      # 定义一个coding()函数,用于实现写代码功能
      # 参数t代表休眠时间
      def coding(t):
          for i in range(3):
              print('正在写代码...')
              time.sleep(t)
      
      # 定义一个程序的执行入口
      if __name__ == '__main__':
          # 第二步:创建子进程对象
          music_process = multiprocessing.Process(target=music, args=(3,))
          coding_process = multiprocessing.Process(target=coding, kwargs={'t':0.2})
      
          # 第三步:启动子进程
          music_process.start()
          coding_process.start()
      

(6)获取进程编号

  • 进程编号的作用:当程序中进程的数量越来越多时 , 如果没有办法区分进程就无法进行有效的进程管理 , 为了方便管理使用进程编号。

  • 获取进程编号的两种方式 :

    • os.getpid():获取进程编号

      • import os
        pid = os.getpid()
        print(pid)
        # 或者
        import multiprocessing
        pid = multiprocessing.current_process().pid
        print(pid)
        
    • os.getppid():获取父进程编号

      • def work():
            # 查看当前进程
            current_process = multiprocessing.current_process()
            # 获取当前进程的编号
            print(“work进程编号:”, current_process.pid, os.getpid())
            # 获取父进程的编号
            print(“work父进程的编号:”, os.getppid())    
        
  • os.kill():杀死进程

    • '''
      import os
      
      os.kill(进程PID编号, 传递的信号)
      信号:
      9 : 强制杀掉PID进程
      15 :通知PID进程,正常结束
      '''
      import os
      
      os.kill(12752, 9)
      

(7)多进程之间不共享全局变量

image-20240822225825704

  • 单任务中进程之间是可以共享变量的
'''
在单任务中,多个函数之间是可以共享全局变量的。
'''
import time

my_list = []

def write_data():
    for i in range(3):
        my_list.append(i)
        print('add:', i)
    print(my_list)

def read_data():
    print(my_list)

if __name__ == '__main__':
    write_data()# [0, 1, 2]
    time.sleep(1)
    read_data()# [0, 1, 2]
  • 多进程之间不共享全局变量
'''
在单任务中,多个函数之间是可以共享全局变量的。
在多进程多任务中,多个进程之间无法共享全局变量。
'''
import time
import multiprocessing

my_list = []

def write_data():
    for i in range(3):
        my_list.append(i)
        print('add:', i)
    print(my_list)

def read_data():
    print(my_list)

if __name__ == '__main__':
    # 创建子进程
    write_process = multiprocessing.Process(target=write_data)
    read_process = multiprocessing.Process(target=read_data)

    # 启动子进程
    write_process.start()#[0, 1, 2]
    time.sleep(1)
    read_process.start()#[]

(8)主进程和子进程的结束顺序

  • 默认情况:主进程会等待所有的子进程执行结束再结束

    • import time
      import multiprocessing
      
      # 定义一个work任务
      def work():
          for i in range(10):
              print('working...')
              time.sleep(0.2)
      
      # 定义一个入口程序
      if __name__ == '__main__':
          # 创建一个子进程(大约需要2s执行完毕)
          sub_process = multiprocessing.Process(target=work)
          sub_process.start()
      
          # 休眠1s
          time.sleep(1)
          print('主进程代码已经执行结束!')
      
      # 主进程代码执行结束后,整个程序并不会立即结束,而是等待子进程执行结束,当子进程执行结束后,整个主进程才能真正结束!
      # 结论:主进程默认会等待子进程的结束而结束
      
  • 设置守护进程:主进程退出后直接销毁子进程,不再等待执行子进程

    • import time
      import multiprocessing
      
      # 定义一个work任务
      def work():
          for i in range(10):
              print('working...')
              time.sleep(0.2)
      
      # 定义一个入口程序
      if __name__ == '__main__':
          # 创建一个子进程(大约需要2s执行完毕)
          sub_process = multiprocessing.Process(target=work)
          # 第一种解决方案:守护进程
          sub_process.daemon = True
          sub_process.start()
      
          # 休眠1s
          time.sleep(1)
          print('主进程代码已经执行结束!')
      
  • 手工销毁子进程:主进程退出后不再等待执行子进程

    • import time
      import multiprocessing
      
      # 定义一个work任务
      def work():
          for i in range(10):
              print('working...')
              time.sleep(0.2)
      
      # 定义一个入口程序
      if __name__ == '__main__':
          # 创建一个子进程(大约需要2s执行完毕)
          sub_process = multiprocessing.Process(target=work)
          sub_process.start()
      
          # 休眠1s
          time.sleep(1)
      
          # 在主程序结束之前,强制销毁子进程
          sub_process.terminate()
          print('主进程代码已经执行结束!')
      

2、线程

(1)线程

  • 线程:程序执行的最小单位 , 实际上进程只负责分配资源 , 而利用这些资源执行程序的是线程 , 也就说进程是线程的容器 , 一个进程中最少有一个线程来负责执行程序。
  • 线程自己不拥有系统资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。这就像通过一个QQ软件(一个进程)打开两个窗口(两个线程)跟两个人聊天一样 , 实现多任务的同时也节省了资源。

(2)多线程完成多任务

  • 多线程是Python程序中实现多任务的另一种方式。
    • image-20240823144212797

(3)线程创建

  • 线程的创建步骤

    • 导入线程模块:import threading
    • 通过线程类创建线程对象:线程对象 = threading.Thread(target=任务名)
    • 启动线程执行任务:线程对象.start()
  • 通过线程类创建线程对象

    • image-20240823144635148

    • '''
      在Python代码中,一个Python文件可以创建一个进程 => 创建多线程
      线程包含在进程内部,一个进程理论上最少有一个线程
      ① 进程资源分配最小单元 => 申请资源
      ② 线程专门用于执行程序(干活的)
      '''
      import time
      # 第一步:导入多线程模块
      import threading
      
      def music():
          for i in range(3):
              print('正在听音乐...')
              time.sleep(0.2)
      
      def coding():
          for i in range(3):
              print('正在写代码...')
              time.sleep(0.2)
      
      if __name__ == '__main__':
          # 第二步:创建多线程对象
          music_thread = threading.Thread(target=music)
          coding_thread = threading.Thread(target=coding)
      
          # 第三步:启动多线程
          music_thread.start()
          coding_thread.start()
      
  • 线程执行带有参数的任务

    • image-20240823160138826

    • args参数的使用

      • # target: 线程执行的函数名
        # args: 表示以元组的方式给函数传参
        coding_thread = threading.Thread(target=coding, args=(3,))
        coding_thread.start()
        
    • kwargs参数的使用

      • # target: 线程执行的函数名
        # kwargs: 表示以字典的方式给函数传参
        music_thread = threading.Thread(target=music, kwargs={”count": 3})
        # 开启线程
        music_thread.start()
        
    • 注意点

      • 元组方式传参 :元组方式传参一定要和参数的顺序保持一致
      • 字典方式传参:字典方式传参字典中的key一定要和参数名保持一致
    • '''
      在Python代码中,我们可以在多任务中使用args或kwargs进行传参
      args:以元组方式传递参数
      kwargs:以字典方式传递参数
      '''
      import time
      # 第一步:导入多线程模块
      import threading
      
      # 参数n代表循环次数
      def music(n):
          for i in range(n):
              print('正在听音乐...')
              time.sleep(0.2)
      
      # 参数t代表休眠时间
      def coding(t):
          for i in range(3):
              print('正在写代码...')
              time.sleep(t)
      
      if __name__ == '__main__':
          # 第二步:创建多线程对象
          music_thread = threading.Thread(target=music, args=(3,))
          coding_thread = threading.Thread(target=coding, kwargs={'t':0.2})
      
          # 第三步:启动多线程
          music_thread.start()
          coding_thread.start()
      

(4)线程间共享全局变量

  • 多个线程都是在同一个进程中 , 多个线程使用的资源都是同一个进程中的资源 , 因此多线程间是共享全局变量

    • image-20240825175850827

    • '''
      多进程实现多任务:无法共享全局变量
      多线程实现多任务:因为所有线程都位于同一个进程中,所以不仅资源共享,而且全局变量也是共享的
      '''
      import time
      import threading
      
      my_list = []
      
      # 定义一个write_data()任务
      def write_data():
          for i in range(3):
              my_list.append(i)
              print('add:', i)
          print(my_list)
      
      # 定义一个read_data()任务
      def read_data():
          print(my_list)
      
      if __name__ == '__main__':
          # 创建两个子线程
          write_thread = threading.Thread(target=write_data)
          read_thread = threading.Thread(target=read_data)
      
          write_thread.start() # [0, 1, 2]
          time.sleep(1)
          read_thread.start()  # [0, 1, 2]
      

(5)主线程和子线程的结束顺序

  • 默认情况:主线程会等待所有的子线程执行结束后主线程再结束

    • import time
      import threading
      
      def work():
          for i in range(10):
              print('working...')
              time.sleep(0.2)
      
      if __name__ == '__main__':
          # 创建一个子线程
          sub_thread = threading.Thread(target=work)
          # 启动子线程
          sub_thread.start()
      
          time.sleep(1)
          print('主线程代码执行结束!')
      
  • 设置守护进程:主进程退出后直接销毁子线程,不再等待执行子进程

    • import time
      import threading
      
      def work():
          for i in range(10):
              print('working...')
              time.sleep(0.2)
      
      if __name__ == '__main__':
          # 创建一个子线程
          # 方案一:守护主线程
          # sub_thread = threading.Thread(target=work, daemon=True)
          # 方案二:通过方法设置守护主线程
          sub_thread = threading.Thread(target=work)
          sub_thread.setDaemon(True)
      
          # 启动子线程
          sub_thread.start()
      
          time.sleep(1)
          print('主线程代码执行结束!')
      

(6)线程间的执行顺序

  • 线程之间执行是无序的,是由CPU调度决定某个线程先执行的

image-20240825182711319

  • 同时启动多个线程
'''
要用到的知识点:获取进程的信息
# 通过current_thread方法获取线程对象
current_thread = threading.current_thread()
# 通过current_thread对象可以知道线程的相关信息,例如被创建的顺序
print(current_thread)
'''
import time
import threading

def get_info():
    time.sleep(0.2)
    current_thread = threading.current_thread()
    print(current_thread)


if __name__ == '__main__':
    # 创建10个子线程 => 按顺序创建
    for i in range(10):  # 0 1 2 3 4 5 6 7 8 9
        sub_thread = threading.Thread(target=get_info)
        sub_thread.start()
  • 输出是乱序的
<Thread(Thread-3 (get_info), started 10936)>
<Thread(Thread-2 (get_info), started 4516)>
<Thread(Thread-1 (get_info), started 15400)>
<Thread(Thread-8 (get_info), started 10928)>
<Thread(Thread-7 (get_info), started 7676)>
<Thread(Thread-6 (get_info), started 13376)>
<Thread(Thread-4 (get_info), started 3344)>
<Thread(Thread-5 (get_info), started 4912)>
<Thread(Thread-10 (get_info), started 12508)>
<Thread(Thread-9 (get_info), started 7908)>

3、进程VS线程

(1)关系对比

  • ① 线程是依附在进程里面的,没有进程就没有线程。

  • ② 一个进程默认提供一条线程,进程可以创建多个线程。

(2)区别对比

  • ① 进程之间不共享全局变量

  • ② 线程之间共享全局变量

  • ③ 创建进程的资源开销要比创建线程的资源开销要大

  • ④ 进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位

(3)优缺点对比

  • ① 进程优缺点

    • 优点:可以用多核,适合CPU密集型应用

    • 缺点:资源开销大

  • ② 线程优缺点

    • 优点:资源开销小,适合IO密集型应用(文件、网络)

    • 缺点:不能使用多核

4、TCP服务器端开发七步走(多任务版本)

'''
TCP服务器端开发七步走 => ① 创建套接字对象 ② 绑定IP和端口 ③ 设置监听 ④ 接收客户端连接请求 ⑤ 接收消息
⑥ 发送消息 ⑦ 关闭套接字对象
'''
import socket
import threading

class WebServer(object):
    # 3、定义一个__init__()魔术方法
    def __init__(self):
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 设置端口复用(让服务器端占用的端口在执行结束可以立即释放,不影响后续程序的使用)
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
        self.tcp_server_socket.bind(('', 9000))  # 注意:参数必须是一个元组
        self.tcp_server_socket.listen(128)

    # 5、定义一个handle_request()方法,用于接收消息与发送消息
    def handle_request(self, new_socket, ip_port):
        # 接收某个客户端发送过来的消息
        content = new_socket.recv(1024).decode('gbk')  # 1024代表什么意思 => 1024字节 => 1kb = 实际工作中,一条数据大小在1~1.5k之间
        print(f'{ip_port}客户端发送消息:{content}')
        # 返回数据给客户端
        new_socket.send('信息已收到,over!'.encode('gbk'))
        # 关闭套接字对象
        # new_socket.close()

    # 4、定义一个start()方法
    def start(self):
        while True:
            new_socket, ip_port = self.tcp_server_socket.accept()
            # 来一个客户,我们就为其创建一个线程,调用自身的handle_request()方法,用于接收消息与发送消息
            sub_thread = threading.Thread(target=self.handle_request, args = (new_socket, ip_port))
            # 启动线程
            sub_thread.start()

# 定义一个程序的执行入口
if __name__ == '__main__':
    # 1、实例化对象
    ws = WebServer()
    # 2、调用对象中的相关方法 => 启动TCP服务
    ws.start()