0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Python-多线程、多进程、协程

汽车电子技术 来源:程序猿知秋 作者:程序猿知秋 2023-02-16 15:46 次阅读

基本使用

**基本概念

**

  • 进程
    • 几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程
    • 进程是处于运行过程中的程序,并且具有一定的独立功能
    • 进程是系统进行资源分配调度的一个独立单位
  • 线程
    • 线程(Thread)也叫 轻量级进程 ,是操作系统能够进行运算调度的最小单位
    • 它被包涵在进程之中,是进程中的实际运作单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源
    • 进程中的多个线程之间可以并发执行

一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。

多线程的优点

  • 进程之间不能共享内存,但线程之间共享内存非常容易
  • 操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。因此,使用多线程来实现多任务并发执行比使用多进程的效率高
  • Python 语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了 Python 的多线程编程

示例

**方式一: **使用 threading.Thread(target=方法名) 的方式实现多线程

参数说明:threading.Thread(参数说明)

  • target: 指定该线程要调度的目标方法。只传函数名,不传函数,即不加()
  • args: 指定一个元组,以位置参数的形式为target指定的函数传入参数。元组的第一个参数传给target的第一个,以此类推
  • kwargs: 指定一个字典,以关键字参数的形式为target指定的函数传入参数
  • daemon:指定所构建的线程是否为后台线程
import time
import threading




def eat(num):
    for i in range(num):
        print("我正在吃饭......")
        time.sleep(1)




def drunk(num=10):
    for i in range(num):
        print("我正在喝水......")
        time.sleep(1)




if __name__ == '__main__':
    # 创建两个线程
    t1 = threading.Thread(target=eat, args=(10,))
    t2 = threading.Thread(target=drunk)
    # 启动两个线程
    t1.start()
    t2.start()


    while True:
        threadList = threading.enumerate()
        print("正在运行的线程是:", threadList)
        print("正在运行的线程数量是:", len(threadList))
        time.sleep(2)

方式二: 继承threading.Thread

import time
import threading




class MyThread(threading.Thread):


    def run(self):
        for i in range(10):
            print("线程启动了,我的名字叫:", self.name)
            time.sleep(1)




if __name__ == '__main__':
    # 创建两个线程
    t1 = MyThread()
    t2 = MyThread()
    # 启动两个线程, start() 方法内部会自动去调用 run方法,所以此处写 start() 就可以了
    t1.start()
    t2.start()


    while True:
        threadList = threading.enumerate()
        print("正在运行的线程是:", threadList)
        print("正在运行的线程数量是:", len(threadList))
        time.sleep(2)

全局变量、互斥锁、死锁

共享全局变量示例

import threading


g_num = 0


def fun_1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("------fun_1的g_num值:%d" % g_num)




def fun_2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("------fun_2的g_num值:%d" % g_num)




if __name__ == '__main__':
    t1 = threading.Thread(target=fun_1, args=(1000000,))
    t2 = threading.Thread(target=fun_2, args=(1000000,))
    t1.start()
    t2.start()

输出结果 :

图片

从以上结果可以看出,直接使用全局变量是有问题的,按理说,预期结果应该是2000000,实际结果相关很大,且每次执行结果都不一样

原因

  • 在g_num=0 时,t1取得g_num=0,此时系统把t1调度为 "sleeping" 状态,把t2转换为 "running" 状态,t2 这时也获得了 g_num=0
  • 然后 t2对得到的值进行加1,并赋给g_num,使得g_num=1
  • 然后系统又把 t2调度为"sleeing",把t1转为"running",线程t1又把它之前得到的0 加1后赋值给 g_num。
  • 这样就导致了 t1和t2都对 g_num加1,但结果仍然是 g_num=1

解决方案:互斥锁

  • 当多个线程几乎同时修改某个共享数据的时候,需要进行同步控制
  • 某个线程要更改共享数据时,先将其锁定,此时资源状态为**” 锁定 ,其他线程不能更改;直到该线程释放资源,将资源的状态改为 ”**非锁定 “, 其他的线程才能再次锁定该资源。互拆锁保证了 每次只有一个线程进行写入操作 ,从而保证了多线程数据的正确性
import threading


g_num = 0


def fun_1(num):
    global g_num
    for i in range(num):
        # 上锁,如果之前没上锁,此时调用就会上锁;如果之前上锁了,此时调用就会阻塞,直接锁被别人释放
        lock.acquire()
        g_num += 1
        # 释放锁
        lock.release()
    print("------fun_1的g_num值:%d" % g_num)




def fun_2(num):
    global g_num
    for i in range(num):
        # 上锁,如果之前没上锁,此时调用就会上锁;如果之前上锁了,此时调用就会阻塞,直接锁被别人释放
        lock.acquire()
        g_num += 1
        # 释放锁
        lock.release()
    print("------fun_2的g_num值:%d" % g_num)




if __name__ == '__main__':
    # 创建一个互斥锁,默认是没上锁的
    lock=threading.Lock()


    t1 = threading.Thread(target=fun_1, args=(1000000,))
    t2 = threading.Thread(target=fun_2, args=(1000000,))
    t1.start()
    t2.start()

死锁

  • 在线程共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁
  • 尽管死锁很少发生,但一旦发生就会造成程序停止响应
import threading
import time




def fun_1():
    # 1号锁,上锁
    lock1.acquire()
    print("fun_1..do some thing.........1")
    time.sleep(1)


    # 2号锁,上锁,如果被别人占用了,则会阻塞
    lock2.acquire()
    print("fun_1..do some thing..........2")
    # 释放2号锁
    lock2.release()


    # 释放1号锁
    lock1.release()




def fun_2():
    # 2号锁,上锁
    lock2.acquire()
    print("fun_2..do some thing.........2")
    time.sleep(1)


    # 1号锁,上锁,如果被别人占用了,则会阻塞
    lock1.acquire()
    print("fun_2..do some thing..........1")
    # 释放1号锁
    lock1.release()


    # 释放2号锁
    lock2.release()


if __name__ == '__main__':
    # 创建两个互斥锁
    lock1=threading.Lock()
    lock2=threading.Lock()


    t1 = threading.Thread(target=fun_1)
    t2 = threading.Thread(target=fun_2)
    t1.start()
    t2.start()

输出结果:会一直停止阻塞

图片

死锁解决方案

  • 程序设计时避免

  • **添加超时等待

    **

进程

进程的状态

  • 新建:操作系统调度启动一个新进程
  • 就绪态:运行的条件都已经满足,等待cpu执行
  • 执行态:cpu 正在执行
  • 等待态:等待某些条件满足,例如一个程序sleep了,此时就处于等待态

图片

进程的创建

  • 在python中,提供了 multiprocessing 模块,就是跨平台的多进程模块,模块提供了 Process类来代表一人进程对象,这个对象可以理解为一个独立的进程
  • Process 参数说明
    • target:传递函数的引用,子进程将执行这个函数
    • args:给target指定的函数,传递参数,以元组的方式传递,非必填
    • kwargs:给target指定的函数传递命名参数,非必填
    • name:给进程设定一个名称,非必填
    • group:指定进程组,非必填
  • Process 对象的常用方法
    • start():启动子进程
    • is_alive():判断子进程是否还活着
    • join(timeout):是否等待子进程执行结束,或等待多少秒
    • terminate():不管任务是否完成,立即终止子进程
  • Process 对象的常用属性
    • name:当前进程的别名,默认为Process-x, x为从1开始递增的整数
    • pid:当前进程的pid (进程号)

示例

import time
import os
import multiprocessing




def eat(num):
    for i in range(num):
        print("我正在吃饭......,我的进程号是:%d,父进程的进程号是:%d" % (os.getpid(),os.getppid()))
        time.sleep(1)




def drunk(num):
    for i in range(num):
        print("我正在喝水......我的进程号是:%d,父进程的进程号是:%d" % (os.getpid(),os.getppid()))
        time.sleep(1)




if __name__ == '__main__':
    # 创建两个进程
    p1 = multiprocessing.Process(target=eat, args=(10,))
    p2 = multiprocessing.Process(target=drunk, args=(10,))
    # 启动两个进程
    p1.start()
    p2.start()


    print("主进程的进程号是:%d" %os.getpid())

输出结果

图片

进程线程区别

  • 功能区别
    • 进程:能够完成多任务,比如,在一台电脑上运行多个qq
    • 线程:能够完成多任务,比如,在一个qq中开多个聊天窗口
  • 调度
    • 进程是资源分配的基本单位
    • 线程是cpu调度和执行的最小单位
  • 拥有资源
    • 进程拥有资源的一个独立单位
    • 线程不拥有系统资源,但可以访问隶属于进程的资源
  • 稳定性
  • 进程有独立的地址空间,多进程较稳定,因为其中一个出现状况不影 响另外一个

  • 同一个进程的多个线程,共用地址空间,多线程相比于多进程,稳定性要差,因为一个线程出现问题会严重影响其他线程

  • 依赖关系
    • 一个线程只能属性一个进程
    • 一个进程至少有一个线程
  • 针对全局变量的共享
    • 多进程间不共享全局变量(进程间都是独立的)
    • 多线程间共享全局变量

进程间的通信

  • 不同的进程间有时也需要进行数据传递,在Python中,可以使用 muitiprocessiong模块的 Queue ,来实现进程间的数据传递
  • Queue 是一个消息队列,数据是先进先出的原则

示例

import time
import multiprocessing




def set_data(queue):
    numList=[1,2,3,4]
    for i in numList:
        # 给队列中放数据,如果队列已经满了,则会阻塞,直到能放数据为止
        queue.put(i)


    time.sleep(1)




def get_data(queue):
    while True:
        # 判断队列中如果没有数据了,则退出循环
        if queue.empty():
            break


        # 从队列中取数据
        data=queue.get()
        print("从队列中取出的数据是:",data)


if __name__ == '__main__':
    # 创建一个消息列表,容量是3(表示只能装3个数据)
    queue=multiprocessing.Queue(3)


    # 创建两个进程
    p1 = multiprocessing.Process(target=set_data, args=(queue,))
    p2 = multiprocessing.Process(target=get_data, args=(queue,))
    # 启动两个进程
    p1.start()
    p2.start()

**输出结果 **

图片

进程池

  • 需要创建成百上千个进程时,可以使用 muitiprocessing模块提供的 Pool 方法
  • 初始化Pool时 ,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池里面没有满,就会创建一个新的进程来执行该请求,如果已经满了,那么就会等待,直到池中有空闲进程,会调用空闲进程来执行新任务

示例

import time
import os
import multiprocessing


def work(msg):
    print("开始执行工作,当前进程是:",os.getpid())
    time.sleep(2)
    print("接收到的消息数据是:%s"%msg)




if __name__ == '__main__':
    # 创建进程池,容量为3
    pool=multiprocessing.Pool(3)


    for i in range(10):
        # pool.apply_async(要调用的目标,(传递给目标的参数元组,))
        # 每次循环将会用空闲的子进程去执行任务
        pool.apply_async(work,("传递参数:%d"%i,))


    # 关闭进程池,关闭后进程池不再接收新请求
    pool.close()
    # 等待进程池中所有的子进程执行完成 ,必须放在close 语句之后
    pool.join()

输出结果

图片

协程

迭代器

  • 迭代是访问集合元素的一种方式
  • 迭代器是一个可以记住遍历位置的对象。
  • 迭代器对象从集合的第一个元素开始访问,直到所有的元素都访问完成
  • 迭代器只会前进,不会后退
  • 我们把对 list、set、str、tuple 等类型的数据使用 for...in ... 的方式从中获取数据,这样的过程称为 遍历(循环)也叫迭代

判断一个数据类型是否可迭代,使用 isinstance(xxx,Iterable)

from collections.abc import Iterable


list1=[1,2]
str1="123"
tuple1=(1,2)
dict1={1:"a",2:"b"}
num=122


print(isinstance(list1,Iterable))
print(isinstance(str1,Iterable))
print(isinstance(tuple1,Iterable))
print(isinstance(dict1,Iterable))
print(isinstance(num,Iterable))

输出结果

图片

自已实现迭代器示例

class MyIterator:
    """自己实现一个迭代器"""


    def __init__(self):
        self.name = list()
        self.currentIndex = 0


    def add(self, arg):
        self.name.append(arg)


    def __iter__(self):
        # 如果想要一个对象成为可以被迭代的对象,即可以使用 for ... in ... ,那么必须实现 __iter__ 方法
        return self


    def __len__(self):
        return len(self.name)


    def __next__(self):
        # 当使用for...in... 迭代时,会先调用 __iter__ 方法,然后调用其返回对象中的 __next__ 方法(即本方法)
        if self.currentIndex < len(self.name):
            result = self.name[self.currentIndex]
            self.currentIndex += 1
            return result
        else:
            # 抛出一个 停止迭代的异常
            raise StopIteration




myIter = MyIterator()
myIter.add("张三")
myIter.add("李四")
myIter.add("王五")


for i in myIter:
    print(i)


# 获取集合长度
print(len(myIter))

**生成器

**

  • 生成器是一种特殊的迭代器
  • 创建生成器有两种方式
    • 方式一:把一个列表生成式的[] 改成 ()
    • # 原始列表
      l=[x*2 for x in range(10)]
      # 构建生成器
      g=(x*2 for x in range(10))
      
      
      # 迭代创建的生成器
      for i in g:
          print(i)
      
  • 方式二:使用 yield 关键字
  • def create_age(num):
        currentAge=0
        while currentAge# 创建生成器中的 值,并打印 send 过来的参数值
            sendMsg=yield currentAge
            print("%s 的年龄是:%d"%(sendMsg,currentAge))
            currentAge+=1
    # 创建生成器,并初始化10个值
    obj=create_age(10)
    
    
    # 迭代一次 生成器中的值
    n=next(obj)
    ss=obj.send("name"+str(0))
    print("send的结果:",ss)
    
    
    # 迭代后续,生成器中的值
    for i in obj:
        print(i)
    

协程

  • 实现方式一:采用yield 实现
import time


def eat():
    while True:
        print("我在吃饭.....")
        time.sleep(1)
        yield




def drunk():
    while True:
        print("我在喝水.....")
        time.sleep(1)
        yield


if __name__ == '__main__':
    # 创建两个生成器
    eat=eat()
    drunk=drunk()


    while True:
        next(eat)
        next(drunk)
  • 实现方式二:采用 greenlet 实现
    • 先安装 greenlet 模块:pip install greenlet
import time
from greenlet import greenlet




def eat():
    while True:
        print("我在吃饭.....")
        # 切换到g2中运行
        g2.switch()
        time.sleep(1)




def drunk():
    while True:
        print("我在喝水.....")
        # 切换到g1中运行
        g1.switch()
        time.sleep(1)




if __name__ == '__main__':
    # 创建两个生成器
    g1 = greenlet(eat)
    g2 = greenlet(drunk)


    # 切换到g1中运行
    g1.switch()
  • 实现方式三:采用 gevent实现 推荐使用
    • 由于 greenlet 需要人手动切换,比较占用IO资源,并且会出现,一旦中间某个程序处于线程等待的话,会一直等待很长时间的问题。所以gevent就应运而生,gevent 遇到延时阻塞会自动切换
    • 先安装 gevent模块:pip install gevent
import time
import gevent
from gevent import monkey


# 有耗时操作时需要, 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
monkey.patch_all()




def work(num):
    for i in range(num):
        print(gevent.getcurrent(), i)
        # 使用 mokey.patch_all() 之后,程序会自动替换成 gevent里面的 gevent.sleep() 方法
        time.sleep(1)




if __name__ == '__main__':
    # 创建并启动协程
    gevent.joinall({
        gevent.spawn(work, 10),
        gevent.spawn(work, 10)
    })

图片下载器实现

import gevent
from gevent import monkey
from urllib import request


# 有耗时操作时需要, 将程序中用到的耗时操作的代码,换为gevent中自己实现的模块
monkey.patch_all()


def down_pic(filename,url):
    resp=request.urlopen(url)
    data=resp.read()


    # 写入文件
    with open(filename,"wb") as f:
        f.write(data)




if __name__ == '__main__':
    # 创建并启动协程
    gevent.joinall({
        gevent.spawn(down_pic, "1.jpg","https://himg3.qunarzz.com/imgs/201812/14/C._M0DCiiigrWCy4LQi1024.jpg"),
        gevent.spawn(down_pic, "2.jpg","https://source.qunarzz.com/site/images/zhuanti/huodong/shangwu.jpg")
    })

总结

  • 进程是资源分配的单位
  • 线程是操作系统调度的单位
  • 进程切换需要的资源最大,效率很低
  • 线程切换需要的资源一般,效率一般(不考虑GIL的情况下)
  • 协程切换任务资源很小,效率高
  • 多进程、多线程根据cpu核数不一样可能是并行的,但是协程是在一个线程中,所以是并发
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 操作系统
    +关注

    关注

    37

    文章

    6293

    浏览量

    121906
  • 程序
    +关注

    关注

    114

    文章

    3631

    浏览量

    79570
  • 任务
    +关注

    关注

    1

    文章

    20

    浏览量

    8504
  • 进程
    +关注

    关注

    0

    文章

    194

    浏览量

    13876
收藏 人收藏

    评论

    相关推荐

    Python多线程编程原理

    Python多线程类似于同时执行多个不同程序,但其执行过程中和进程还是有区别的,每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线
    发表于 11-22 14:01

    多线程多进程的区别

    6.你的数据库一会又500个连接数,一会有10个,你分析一下情况7.udp和tcp的区别8.多线程多进程的区别9.有一台web服务器,你选择用多线程还是多进程,...
    发表于 07-19 07:21

    浅谈多进程多线程的选择

    鱼还是熊掌:浅谈多进程多线程的选择关于多进程多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,
    发表于 08-24 07:38

    线程有什么区别

    线程的区别线程的共同目的之一是实现系统资源的上下文调用,不过它们的实现层级不同;
    发表于 12-10 06:23

    python多线程多进程对比

    段可以干多件事,譬如可以边吃饭边看电视;在Python中,多线程 虽然是严格上来说是串行,但却比一般的串行程序执行效率高得很。 一般的串行程序,在程序阻塞的时候,只能干等着,
    发表于 03-15 16:42

    LINUX系统下多线程多进程性能分析

    采用多进程处理多个任务,会占用很多系统资源(主要是CPU 和内存的使用)。在LINUX 中,则对这种弊端进行了改进,在用户态实现了多线程处理多任务。本文系统论述了多线程
    发表于 08-13 08:31 20次下载

    什么是多线程编程?多线程编程基础知识

    摘要:多线程编程是现代软件技术中很重要的一个环节。要弄懂多线程,这就要牵涉到多进程。本文主要以多线程编程以及多线程编程相关知识而做出的一些结
    发表于 12-08 16:30 1.2w次阅读

    如何选好多线程多进程

    关于多进程多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有这么简单了,选
    的头像 发表于 05-11 16:16 2780次阅读
    如何选好<b class='flag-5'>多线程</b>和<b class='flag-5'>多进程</b>

    多进程多线程的深度比较

    嵌入式Linux中文站,关于多进程多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”。这句话应付考试基本上够了,但如果在工作中遇
    发表于 04-02 14:42 371次阅读

    使用Python多进程的理由

    Python 是运行在解释器中的语言,查找资料知道, python 中有一个全局锁( GI),在使用多进程( Threa)的情况下,不能发挥多核的优势。而使用多进程( Multipro
    的头像 发表于 04-04 16:50 1363次阅读
    使用<b class='flag-5'>Python</b><b class='flag-5'>多进程</b>的理由

    Python多进程学习

    Python 多进程 (Multiprocessing) 是一种同时利用计算机多个处理器核心 (CPU cores) 进行并行处理的技术,它与 Python多线程 (Multith
    的头像 发表于 04-26 11:04 360次阅读

    浅谈Linux网络编程中的多进程多线程

    在Linux网络编程中,我们应该见过很多网络框架或者server,有多进程的处理方式,也有多线程处理方式,孰好孰坏并没有可比性,首先选择多进程还是多线程我们需要考虑业务场景,其次结合当
    发表于 08-08 16:56 408次阅读
    浅谈Linux网络编程中的<b class='flag-5'>多进程</b>和<b class='flag-5'>多线程</b>

    关于Python多进程多线程详解

    进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。关于多进程多线程,教科书上最经典的一句话是“进程
    的头像 发表于 11-06 14:46 313次阅读
    关于<b class='flag-5'>Python</b><b class='flag-5'>多进程</b>和<b class='flag-5'>多线程</b>详解

    Linux系统上多线程多进程的运行效率

    关于多进程多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有这么简单了,选
    的头像 发表于 11-10 10:54 440次阅读
    Linux系统上<b class='flag-5'>多线程</b>和<b class='flag-5'>多进程</b>的运行效率

    你还是分不清多进程多线程吗?一文搞懂!

    你还是分不清多进程多线程吗?一文搞懂! 多进程多线程是并发编程中常见的两个概念,它们都可以用于提高程序的性能和效率。但是它们的实现方式和使用场景略有不同。 1.
    的头像 发表于 12-19 16:07 257次阅读