python中的多线程和多进程

python中的多线程和多进程

@@ python中实现并发编程主要有三种方式

  • 多线程
  • 多进程
  • 多线程+多进程

python中的多进程

Unix和Linux系统提供的fork()函数来创建子进程,子进程是父进程的一个拷贝,但是子进程有自己的PID。

fork()函数会返回两次,父进程调用的时候得到的是子进程的PID,但是子进程调用的时候返回的永远是0。

python的os模块提供了fork函数,以创建子进程。但是,需要注意的是,这仅是Linux/Unix的情况下

Windows并没有提供fork()函数,因此,想要在Windows上使用python创建多进程,需要用到的模块是multiprocessing里面的Process类。

官方参考文档:Python multiprocessing references

from multiprocessing import Process

而且该模块提供了更佳高级的封装:

  • Pool - 一个进程池:用于批量启动进程
  • Queue - 用于进程间通信的队列
  • Pipe - 管道

以下是一个简单的创建进程的例子

# 加载需要创建进程的基本类
from multiprocessing import Process
from os import getpid	# os模块提供了一个getpid方法,用来获取当前运行的进程的id号
# 定义一个需要使用多进程来运行的函数
def out_num(num):
    print('pid: %-8d, num: %d'%(getpid(), num))


if __name__ == '__main__':
    nums = [i for i in range(1, 8)]
    proc = []	# 定义一个进程的集合
    for num in nums:
        p = Process(target=out_num, args=(num,))	# 传入的参数需要注意,args是一个元组
        p.start()		# 当进程被创建的时候,将其启动
        proc.append(p)	# 将创建的进程添加到进程的集合里面

    for p in proc:
        # 当进程运行结束之后,将子进程合并到主进程里面
        # 当然,如果不使用p.join()方法也没有出现什么问题(暂时没有发现)
        p.join()		

运行结果: 从下面的结果可以看到,每个进程都有有自己的pid,而且每个进程的执行开始时间是不一样的,因此输出的数字顺序也是不同的。

运行结果

注意点

如果想要多进程运行的话,不能够将进程start之后紧跟着join方法,因为join方法会阻塞子进程,让子进程运行完毕之后才能够继续运行,这样的话没有多大意义,如:

for num in nums:
    p = Process(target=out_num, args=(num,))
    p.start()
    p.join()

这样运行之后,结果是这样的:

运行结果

即相当于顺序执行了,没有发挥多进程的作用,这并不是我们想要的。


python 中的多线程

在python中,我们使用的多线程模块板是threading,-- from threading import Thread,这样就引入了我们需要的多线程类。

创建线程和启动线程的方法其实同Process进程的创建是类似的。

以下是一个简单的创建线程的例子

#!usr/bin/python
# coding : utf-8
from threading import Thread
from random import randint
from time import sleep, time

def walk(name):
	# 随机走动一段时间
    sec = randint(1, 3)
    print('>> %s is walking ...'% (name))
    sleep(sec)
    print('%s stoped. ' % (name))

if __name__ == "__main__":
    begin = time()
	# 创建两个线程并将其启动
    t1 = Thread(target=walk, args=('LiBai',))
    t2 = Thread(target=walk, args=('DuFu',))
    t1.start()
    t2.start()
    t1.join()
    t2.join()

    end = time()
	# 打印出两个线程云心完毕一共花费了多长时间
    print('Total time: %.3f' % (end - begin))
运行结果:

运行结果
从上述结果可以看出,第二个线程最先执行完毕。

除了直接使用Thread类来创建线程之外,我们还可以将Thread类进行继承封装,实现自定义的多线程对象。比如将上述的代码进行如下改写

#!usr/bin/python
# coding : utf-8
from threading import Thread
from random import randint
from time import sleep, time

class WalkTask(Thread):
    def __init__(self, name):
        super().__init__()
        self._name = name

    def run(self):
    	# 需要在继承的类中实现run方法,这样在调用start之后才会启动对应的业务流程
        sec = randint(1, 3)
        print('>> %s is walking ...'% (self.name))
        sleep(sec)
        print('%s stoped. ' % (self.name))


if __name__ == "__main__":
    begin = time()
    t1 = WalkTask('Libai')
    t1.start()
    t2 = WalkTask('DuFu')
    t2.start()

    t1.join()
    t2.join()

    end = time()
    print('Total time: %.3f' % (end - begin))

一般情况下,在使用多线程的时候还是推荐对其进行封装
因为在跑多线程的时候可以实现更多自定义的特性,更加灵活且方便。

python怎样在线程之间进行通信

相信学过操作系统原理的同学都应该知道临界区这个概念

每个进程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源,还是软件临界资源,多个进程必须互斥地对它进行访问。
来自百度百科

python中的多线程如果想要通信,也需要使用到临界区。而使用到临界区便需要对进行访问的资源进行加锁操作:
  - 当使用资源时,先加上锁,此时别的线程无法访问
  - 使用完毕后,将锁进行释放,别的线程就又可以访问了
这样,才不会造成线程间的访问冲突,从而造成结果出错。

那如何给临界资源加锁呢?
python 的threading模块给我们提供了Lock类,这是一个资源锁。

这是实现原始锁对象的类。一旦一个线程获得一个锁,会阻塞随后尝试获得锁的线程,直到它被释放;任何线程都可以释放它。
来自 - Python threading Lock

如何使用?

# 先引入Lock类
from threading import Lock
_lock = Lock()   # 实例化
_lock.acquire()  # 加上这一句,只有获得锁 - 即锁是打开状态的时候,才能够访问这一句下方的资源

# .... 这里面是资源

_lock.release()  # 释放锁,让其他的线程也可以访问
接下来实现一个小实例
#!usr/bin/python
# coding : utf-8
from threading import Thread, Lock
from random import randint
from time import sleep, time

class Person:
	# 一个Person类
    def __init__(self):
        self._food = []		# 吃过的食物
        self._lock = Lock()	# 锁
        self._weight = 50	# 体重

    def eat(self, food):
    	# 吃东西 - 方法
    	# Person每次只能吃一个东西,因此,需要加上锁
        self._lock.acquire()
        try:
        	print('Is eating ', food[0], '...')
            self._food.append(food[0])
            self._weight += food[1]
            sleep(randint(1,3))		# 吃一个东西将随机花费1-3秒的时间
        finally:
            self._lock.release()	# 吃完东西后,可以将锁释放了,这样就可以吃其他东西了
            # pass

    @property
    def food(self):
    	# 返回吃过的食物
        return self._food

    def __str__(self):
    	# 返回任务的体重和吃过的食物列表
        s = 'weight: {}, food {}'.format(self._weight, self._food)
        return s


class EatTask(Thread):
	# 将吃东西采用多线程来处理
    def __init__(self, person:Person, food:str):
        super().__init__()
        self._person = person
        self._food = food

    def run(self):
        self._person.eat(self._food)


if __name__ == "__main__":
	# 每种食物包括食物的种类和食物的重量 -- 吃了后体重会上升
    foods = [('apple', 1), ('banana', 1), ('tomato', 1), 
        ('potato', 1), ('orange', 1), ('noodles', 1), 
        ('rice', 1), ('soup', 1)]
    threads = []
    # 创建一个Person对象 -- xiaoming
    xiaoming = Person()
    begin = time()

    for food in foods:
        t = EatTask(xiaoming, food)
        t.start()
        threads.append(t)

    for t in threads:
        t.join()

    end = time()

    print('Total time: %.3f' % (end - begin))
    print(xiaoming)
运行结果
  • 加上了互斥锁之后的运行结果
    运行结果
    从上述结果可以看到,因为吃一个食物需要花费1-3秒不等的时间,任务吃完所有的食物一共花费了14.01s的时间。这是因为,在线程执行吃东西的时候,如果当前的“Person”正在吃东西的话,会将当前的线程进行阻塞,因此,程序也就相当于顺序执行了

  • 去掉互斥锁的运行结果
    运行结果
    从结果可以看到,整个程序一共花费了3.002s的时间,这是由于没有给变量添加互斥锁,多个线程可以直接一起访问。而sleep的最大时间是3s,因此程序运行结束后的最大花费时间是3s。

因此,想要在线程间进行通信,可以定义一个全局变量,但是,这个变量最好需要添加一个互斥锁,这样能够避免由于多个线程同时访问而造成的冲突。(建议的做法是和上述操作一样,将需要访问的变量封装到类里面)。

我也是一边学习一边写博客,如果还有其他的方法,希望你可以在评论里告诉我

关于如何在多线程运行的过程中获得运行结果,还可以使用队列,当线程运行完毕后,将结果放入到Queue里面,(这个Queue是一个全局变量),然后在其他的线程里面调用结果。

采用多线程还是多进程

在Python语言中,单线程+异步I/O的编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。
协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销
协程的第二个优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不用加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
如果想要充分利用CPU的多核特性,最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
– 转自 《Python 100 Day》

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值