python中的多线程与多进程

进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。

在Java中比较关注多线程,而在Python中,与多线程相比,可能更关注多进程。


1 GIL

在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在python中,无论有多少核,同时只能执行一个线程。究其原因,这就是由于GIL的存在导致的。

GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。

GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以它不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。而在pypy和jpython中是没有GIL的。通常使用的就是cpython。

根据python的这个特点,可以得出一个结论:python多线程适用于io密集型代码,而不适用于cpu密集型代码
原因是,io密集型代码大多数时间消耗在io等待上,而cpu使用率不高,因此可以开启多线程来使cpu得到最大化利用。但是对于cpu密集型代码,cpu本身的利用率就很高,再开启多线程来竞争cpu,提升的效率不足以抵消线程调度带来的资源消耗。

python下想要充分利用多核CPU,就用多进程。因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。

2 多线程

python通过threading.Thread类来创建线程对象。

import threading

# 线程任务,对全局变量num做+1操作
def run(n):
    global num
    num += 1


num = 0
t_obj = []

for i in range(20000):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.start()
    t_obj.append(t)

for t in t_obj:
    t.join()

print "num:", num

主要的线程方法说明如下:

  • t = threading.Thread(target=你写的函数名,args=(传入变量(如果只有一个变量就必须在后加上逗号),),name=随便取一个线程名):把一个线程实例化给t,这个线程负责执行target指定的线程方法
  • t.start():负责执行启动这个线程
  • t.join():必须要等待子线程执行完成后再执行主线程
  • t.setDeamon(True): 设置为守护线程。当主线程执行完毕后,不管子线程有没有执行完成都退出主程序,注意不能和t.join()一起使用。
  • threading.current_thread().name:打印出线程名

与Java一样,也有线程安全问题。在python2.x下执行,最终num结果可能为2000,也可能为19999、19998或者19997等等。也就是说,这段示例代码是线程不安全的。
解决线程安全的方法与Java类似,最常用的是阻塞式线程同步,即加锁。

# encoding: utf8
import threading


# 线程任务,对全局变量num做+1操作
def run(n):
    global num
    if lock.acquire():
        print "线程%s打印%s\n" % (n, num)
        num += 1
        lock.release()


num = 0
t_obj = []
lock = threading.Lock()

for i in range(20000):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.start()
    t_obj.append(t)

for t in t_obj:
    t.join()

print "num:", num

另一种创建多线程的方式是继承threading.Thread类,如下:

# encoding: utf8
import threading


class Task(threading.Thread):
    def __init__(self, n):
        super(Task, self).__init__()
        self.n = n

    # 线程任务,对全局变量num做+1操作
    def run(self):
        global num
        lock.acquire()
        print "线程%s打印%s\n" % (self.n, num)
        num += 1
        lock.release()


num = 0
t_obj = []
lock = threading.Lock()

for i in range(20000):
    t = Task(i)
    t.start()
    t_obj.append(t)

for t in t_obj:
    t.join()

2.1 主线程与子线程

在python中,当一个进程启动之后,默认会产生一个主线程。当使用多线程时,主线程会创建多个子线程。默认情况下子线程是以非守护线程方式启动的。当主线程执行完自己的任务后退出,此时子线程会继续执行自己的任务,直到子线程结束。

如果以守护线程的方式创建子线程,则当主线程结束时,子线程也随之结束。如下:

# encoding: utf8
import threading
import time


# 线程任务,对全局变量num做+1操作
def run(n):
    global num
    if lock.acquire():
        print "线程%s打印%s\n" % (n, num)
        time.sleep(0.1)
        num += 1
        lock.release()


num = 0
t_obj = []
lock = threading.Lock()

for i in range(2000):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.setDaemon(True)
    t.start()
    t_obj.append(t)


print "num:", num

运行结果:

线程t-0打印0

线程t-1打印1

线程t-2打印2

num: 2

2.2 线程通信

从上文中的例子中看到,我们使用多线程时用的是threading模块。
事实上,python提供了几个用于多线程编程的模块,包括thread,threading和queue。thread提供了基本的线程和锁的支持,但由于功能缺陷,基本不会使用。threading提供了更高级别,功能更强的线程管理的功能。

Threading模块不仅提供了Thread类,还提供了各种非常好用的同步机制。如下所示:

  • Lock:锁原语对象。
  • RLock:可重入锁对象。
  • Condition:条件变量能让一个线程停止执行,直到满足某个条件。
  • Event:通用的条件变量。多个线程可以等待某个事件的发生,在事件发生后,所有的线程会被激活。
  • Semaphore:为等待锁的线程,提供一个类似“等候室”的结构。
  • BoundedSemaphone:与Semaphore类似,只是它不允许超过初始值。

Lock与RLock
Lock在上文的例子中使用过。它是一个同步原语,状态是锁定或未锁定。通过acquire和release方法来加锁和释放锁。
RLock是一个类似于Lock对象的同步原语,只不过它是可重入的,同一个线程可以多次调用。

Event
事件是一个简单的线程同步对象,全局定义一个flag标记,当标记值为false时,event.wait()就会阻塞,否则不会阻塞。主要提供以下几个方法:

  • clear(): 将标记设置为false。
  • set():将标记设置为true。
  • is_set():判断是否设置了标记。
  • wait():监听标记,如果没有检测到标记就一直处于阻塞状态。

demo如下:

# encoding: utf-8
import threading
import time

event = threading.Event()


def lighter():
    count = 0
    event.set()   # 设置标记
    while True:
        if 5 < count < 10:
            # 红灯,清除标记位
            event.clear()
            print "red light is on..."
        elif count > 10:
            # 绿灯,设置标记位
            event.set()
            count = 0
        else:
            print "green light is on..."

        time.sleep(1)
        count += 1


def car(name):
    while True:
        if event.is_set():
            print("%s running..." % name)
            time.sleep(3)
        else:
            print("%s sees red light, waiting..." % name)
            event.wait()
            print("%s sees green light is on, starting going..." % name)


light = threading.Thread(target=lighter,)
light.start()

car = threading.Thread(target=car, args=('MINI',))
car.start()

Condition
Condition称为条件锁,也是一个同步原语,当需要线程关注特定的状态变化或事件的发生时使用。通过acquire和release来加锁和释放锁。主要方法是:

  • wait([timeout]):使线程进入condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。
  • notify():从等待池挑选一个线程通知,收到通知的线程自动调用acquire方法来尝试获得锁定;其他线程仍然在等待池中。使用前线程必须已获得锁定,否则将抛出异常。
  • notifyAll():通知等待池中的所有线程。使用前线程必须已获得锁定,否则将抛出异常。

示例如下:

# encoding: utf-8
import time
from threading import Condition, current_thread, Thread

"""
通过两个线程依次打印0-99
"""
con = Condition()
i = 0


def tc1():
    global i
    with con:
        while i < 100:
            print current_thread().name, i
            time.sleep(0.3)
            i += 1
            if i % 2 == 1:
                con.notify()
                con.wait()
        con.notify()


def tc2():
    global i
    with con:
        while i < 100:
            print current_thread().name, i
            time.sleep(0.3)
            i += 1
            if i % 2 == 0:
                con.notify()
                con.wait()
        con.notify()


Thread(target=tc1).start()
Thread(target=tc2).start()

从示例可以看出,Condition的这几个方法使用其实与Java里面的线程同步方法一样。

public class ThreadTest {
   private static final Object obj = new Object();
   private static int num = 0;  
   public static void main(String[] args) {
       new Thread(() -> {
           synchronized (obj) {
               while (num < 100) {
                   System.out.println(Thread.currentThread().getName() + "打印" + num);
                   num += 1;
                   if (num % 2 == 1) {
                       obj.notify();
                       try {
                           obj.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                   }
               }
           }
       }).start();

       new Thread(() -> {
           synchronized (obj) {
               while (num < 100) {
                   System.out.println(Thread.currentThread().getName() + "打印" + num);
                   num += 1;
                   if (num % 2 == 0) {
                       obj.notify();
                       try {
                           obj.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                   }
               }
           }
       }).start();
   } }

2.3 线程池

在python2.x中,threading模块并没有系统线程池。但在multiprocessing.dummy模块中,可以通过from multiprocessing.dummy import Pool这样的方式引入线程池。使用方法与multiprocessing模块中的进程池基本相同。示例如下:

# encoding: utf8
from multiprocessing.dummy import Pool as ThreadPool


def run(n):
    return n**2


if __name__ == '__main__':
    pool = ThreadPool(5)
    futures = []
    for i in range(10):
        future = pool.apply_async(run, (i,))
        futures.append(future)

    for future in futures:
        future.wait()

    print [future.get() for future in futures]

在python3.x版本中,concurrent.futures模块实现了系统线程池。示例如下:

# encoding: utf8
from concurrent.futures import ThreadPoolExecutor

pool = ThreadPoolExecutor(3)
l = []


def run(a, b):
    return a * b


for i in range(10):
    future = pool.submit(run, i, i + 1)
    l.append(future)


print([future.result() for future in l])

3 多进程

3.1 demo示例

看一个例子如下:

import os, time
from multiprocessing import Pool


# 返回n的平方
def work(n):
    print('%s run' % os.getpid())
    time.sleep(3)
    return n**2


if __name__ == '__main__':
    p = Pool(processes=10)
    res, data = [], []
    for i in range(20):
        result = p.apply_async(work, args=(i,))
        res.append(result)
    p.close()
    p.join()  # 主进程等待所有子进程全部结束 
    for r in res:
        data.append(r.get())
    print data

执行结果如下:
在这里插入图片描述
20次work调用,一次有10个进程在同时执行,因此总共只需要执行两轮,耗费6秒多一点时间。如果使用20个进程,则只需要3秒多一点时间。

3.2 进程池

进程池Pool中常用方法:

  • apply(): 同步(串行)执行
  • apply_async():异步(并行)执行
  • terminate():立刻关闭进程池
  • join():主进程等待所有子进程执行完毕。必须在关闭进程池(close或terminate)之后。
  • close():等待所有进程结束后,才关闭进程池

在python3.x中,进程池与java中的线程池非常类似。如下所示:

# encoding: utf8
import os
import time
from concurrent.futures import ProcessPoolExecutor


def task(n):
    print("%s is running" % os.getpid())
    time.sleep(2)
    return n**2


if __name__ == '__main__':
    p = ProcessPoolExecutor()  # 默认为cpu个数
    l = []
    start = time.time()
    for i in range(10):
        # submit方法返回的是一个future实例
        future = p.submit(task, i)
        l.append(future)
    # 类似Pool线程池的close和join一起使用的效果
    p.shutdown()
    print('='*30)
    print([future.result() for future in l])

4 参考资料

[1]https://www.cnblogs.com/whatisfantasy/p/6440585.html
[2]https://blog.csdn.net/lzy98/article/details/88819425

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值