Python的多线程

1、进程和线程

现代操作系统(Windows,macOS,Linux)都可以执行多任务,多任务就是同时运行多个任务。

现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定,程序自己不能决定什么时候执行,执行多长时间。

 

如果要同时执行多个任务,有三种解决方案:

一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。还有一种方法是启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。当然还有第三种方法,就是启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。

总结一下就是,多任务的实现有3种方式:

  • 多进程模式;
  • 多线程模式;
  • 多进程+多线程模式。

 

多线程类似于同时执行多个不同程序,多线程运行有如下优点:

  • 使用线程可以把占据长时间的程序中的任务放到后台去处理。
  • 用户界面可以更加吸引人,比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
  • 程序的运行速度可能加快。
  • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。

 

2、多进程

2.1、创建新进程(Process)

通过 OS 模块的 fork 调用可以在 Linux 和 Unix 系统上 生成多个进程,但只能在 Linux、Unix系统上运行,详情可参考:https://www.liaoxuefeng.com/wiki/1016959663602400/1017628290184064

multiprocessing模块是跨平台版本的多进程模块,通过模块我们可以实现跨平台使用多进程。

multiprocessing模块提供了一个进程类Process来创建子进程。创建子进程时,只需要传入一个执行函数和函数的参数。进程实例用start()方法启动,join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

from multiprocessing import Process
import os

# 子进程要执行的代码
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.')

#上面将输出:
Parent process 9204.
Child process will start.
Run child process test (2172)...
Child process end.

 

2.2、进程池

如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

from multiprocessing import Pool
import os, time, random

def long_time_task(name):
    print('第 %s (%s) 个进程正在执行...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('第 %s 个进程的执行时间为: %0.2f 秒.' % (name, (end - start)))

if __name__=='__main__':
    print('父进程 %s.' % os.getpid())
    p = Pool(4)   #Pool(4)表示最多同时执行4个进程
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('等待所有的进程执行完毕')
    p.close()
    p.join()   #对进程池实例对象调用join()方法会阻塞父进程,知道所有的子进程都执行完毕
    print('所有的进程都已经执行完毕')

上述代码的执行结果可能如下:

父进程 3016.
等待所有的进程执行完毕
第 0 (10420) 个进程正在执行...
第 1 (10224) 个进程正在执行...
第 2 (9020) 个进程正在执行...
第 3 (8672) 个进程正在执行...
第 0 个进程的执行时间为: 0.92 秒.
第 4 (10420) 个进程正在执行...
第 2 个进程的执行时间为: 1.79 秒.
第 4 个进程的执行时间为: 1.28 秒.
第 3 个进程的执行时间为: 2.72 秒.
第 1 个进程的执行时间为: 2.96 秒.
所有的进程都已经执行完毕

执行结果当中,第几个进程先执行完毕是不确定的。可以看到,有多个进程是同时执行的。

Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。Pool(n)表示最多同时执行n个进程,所以从上述执行结果来看,可以看到执行到第3个进程时,刚好有4个进程在执行,此时第4个进程必须等到前面的进程有一个执行完毕后才能执行。Pool的默认大小是CPU的核数。

 

2.3、进程间的通信

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print('进程开始写操作。。 %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('插入值 %s 到 queue 当中...' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    print('进程开始读操作。。 %s' % os.getpid())
    while True:
        value = q.get(True)
        print('从queue当中读取值 %s from.' % value)

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
# 启动子进程pw,进行写入操作 pw.start() # 启动子进程pr,进行读取操作 pr.start()
# 等待pw结束 pw.join()
# pr进程里是死循环,无法等待其结束,只能强行终止: pr.terminate()

上述代码的执行结果如下:

进程开始写操作。。 1940
插入值 A 到 queue 当中...
进程开始读操作。。 5892
从queue当中读取值 A from.
插入值 B 到 queue 当中...
从queue当中读取值 B from.
插入值 C 到 queue 当中...
从queue当中读取值 C from.

 

2、多线程

多任务可以由多进程完成,也可以由一个进程内的多线程完成。

Python的标准库提供了两个模块:_threadthreading_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

 

2.1、使用threading模块创建线程

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

import time, threading

# 新线程执行的代码:
def loop():
    print('线程 %s 正在执行...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('线程 %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('线程 %s 执行完毕.' % threading.current_thread().name)

print('线程 %s 正在执行...' % threading.current_thread().name)

t = threading.Thread(target=loop, name='新线程AA')  #给线程传递函数参数且命名
t.start()
t.join()
print('线程 %s 执行完毕.' % threading.current_thread().name)

上述代码的执行结果如下:

线程 MainThread 正在执行...
线程 新线程AA 正在执行...
线程 新线程AA >>> 1
线程 新线程AA >>> 2
线程 新线程AA >>> 3
线程 新线程AA >>> 4
线程 新线程AA >>> 5
线程 新线程AA 执行完毕.
线程 MainThread 执行完毕.

由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。

主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们可以随意给子线程命名。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1Thread-2……

 

通过继承来创建线程:

我们可以通过直接从 threading.Thread 继承创建一个新的子类,并实例化后调用 start() 方法启动新线程,即它调用了线程的 run() 方法:

#!/usr/bin/python3

import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, counter):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.counter = counter
    def run(self):
        print ("开始线程:" + self.name)
        print_time(self.name, self.counter, 5)
        print ("退出线程:" + self.name)

def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.exit()
        time.sleep(delay)
        print ("%s: %s" % (threadName, time.ctime(time.time())))
        counter -= 1

# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)

# 开启新线程
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print ("退出主线程")

上述代码的执行结果:

开始线程:Thread-1
开始线程:Thread-2
Thread-1: Tue Jan 26 23:26:17 2021
Thread-2: Tue Jan 26 23:26:18 2021
Thread-1: Tue Jan 26 23:26:18 2021
Thread-1: Tue Jan 26 23:26:19 2021
Thread-2: Tue Jan 26 23:26:20 2021
Thread-1: Tue Jan 26 23:26:20 2021
Thread-1: Tue Jan 26 23:26:21 2021
退出线程:Thread-1
Thread-2: Tue Jan 26 23:26:22 2021
Thread-2: Tue Jan 26 23:26:24 2021
Thread-2: Tue Jan 26 23:26:26 2021
退出线程:Thread-2
退出主线程

 

2.1.1、threading模块的常见方法

threading模块的常见方法方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

线程类实例的常见方法:除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

  • run(): 用以表示线程活动的方法。
  • start():启动线程活动
  • join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
  • isAlive(): 返回线程是否活动的。
  • getName(): 返回线程名。
  • setName(): 设置线程名。

 

2.2、给线程加锁

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

要想避免多线程执行时,导致共享变量发生数据混乱的问题,可以给多个线程共享的修改变量的方法加锁。python中创建一个锁通过threading.Lock()来实现。

代码实例:

import time, threading

# 假定这是你的银行存款:
balance = 0
lock = threading.Lock()

def change_it(n):
    # 先存后取,结果应该为0:
    global balance
    balance = balance + n
    balance = balance - n


def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        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()

上述代码,当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。  

参考:https://www.liaoxuefeng.com/wiki/1016959663602400/1017629247922688

 

posted @ 2021-01-25 23:21  wenxuehai  阅读(593)  评论(0编辑  收藏  举报
//右下角添加目录