1、python多线程

使用一个例子来学习多线程。建议自己敲一遍。
python多线程是通过threading模块的Thread实现。
创建线程对象 t = thread.Thread() 启动线程 t.start()

import threading
import time

def say(name):
	print("你好%s at %s"%(name, time.ctime()))
	time.sleep(2)
	print("结束%s at %s"%(name, time.ctime()))


def listen(name):
	print("你好%s at %s"%(name, time.ctime()))
	time.sleep(4)
	print("结束%s at %s"%(name, time.ctime()))


if __name__=='__main__':
	t1 = threading.Thread(target=say, args = ('tony',))
	t1.start()
	t2 = threading.Thread(target = listen, args = ('simon',))
	t2.start()
	print("程序结束=============")
你好tony at Wed Mar 24 17:58:32 2021
你好simon at Wed Mar 24 17:58:32 2021
程序结束=============
结束tony at Wed Mar 24 17:58:34 2021
结束simon at Wed Mar 24 17:58:36 2021

可以看出主线程不是在两个子线程运行结束后退出的。这是因为主线程和两个子线程是同时跑的,但是子线程跑完后,主线程才会退出。

但是有时候我们想要子线程跑完后,再继续跑主线程,这个时候就引入了join

import threading
import time

def say(name):
	print("你好%s at %s"%(name, time.ctime()))
	time.sleep(2)
	print("结束%s at %s"%(name, time.ctime()))


def listen(name):
	print("你好%s at %s"%(name, time.ctime()))
	time.sleep(4)
	print("结束%s at %s"%(name, time.ctime()))


if __name__=='__main__':
	t1 = threading.Thread(target=say, args = ('tony',))
	t1.start()
	t2 = threading.Thread(target = listen, args = ('simon',))
	t2.start()
	t1.join()
	t2.join()
	print("程序结束=============")
你好tony at Wed Mar 24 18:01:51 2021
你好simon at Wed Mar 24 18:01:51 2021
结束tony at Wed Mar 24 18:01:53 2021
结束simon at Wed Mar 24 18:01:55 2021
程序结束=============

主线程最后执行print操作,并退出。但是如果不加join,主线程执行打印,但是主线程还没有结束,还需要等待子线程结束后,主线程才结束。

上面的子线程是非守护子线程,默认的子线程都是主线程的非守护子线程。有时候我们需要当主线程结束,不管主线程有没有结束,子线程都要跟随主线程一起退出,这个时候就需要守护线程

如果某个线程是守护线程,那么主线程就不需要等待这个子线程,当其他非守护线程运行结束后,主线程就退出。
下面我们看个例子,设置t2为守护进程。

import threading
import time

def say(name):
	print("你好%s at %s"%(name, time.ctime()))
	time.sleep(2)
	print("结束%s at %s"%(name, time.ctime()))


def listen(name):
	print("你好%s at %s"%(name, time.ctime()))
	time.sleep(4)
	print("结束%s at %s"%(name, time.ctime()))


if __name__=='__main__':
	t1 = threading.Thread(target=say, args = ('tony',))
	t1.start()
	t2 = threading.Thread(target = listen, args = ('simon',))
	t2.setDaemon(True)
	t2.start()
	print("程序结束=============")
你好tony at Wed Mar 24 18:07:47 2021
你好simon at Wed Mar 24 18:07:47 2021
程序结束=============
结束tony at Wed Mar 24 18:07:49 2021

t2进程是sleep 4s,主线程不等待t2进程结束,就退出了。
thread的一些方法:

  • join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
  • setDaemon(True) 在start()之前设置,这个方法和join是相仿的。
  • run():线程被cpu调度后自动执行线程对象的run方法
  • start() 启动线程活动
  • isAlibe() 返回线程是否活动的。

上面的进程,如果分开跑,需要6s,但是实际上用了4s,说明性能提升了,但是这种提升是cpu并发实现的提升,也就是cpu线程切换(多道技术)带来的,而不是cpu的并行执行。

并发:指一个系统具有处理多个任务的能力(cpu切换,多道技术)
并行:指一个系统有同时处理多个任务的能力(cpu同时处理)
并行是并发的一种情况,子集。

python 多线程不能实现真正的并行操作,是因为GIL(全局解释器锁

我们对任务进行分类:

  • IO密集型(各个线程都会各种的等待,如果有等待,切换线程是比较核是的),也可以采用多线程+协程
  • 计算密集型(线程在计算过程中没有等待,这时候没必要做切换)

2.多线程同步锁、死锁和递归锁

如果python多个线程要用到相同的数据,就会存在资源争用和锁的问题。

2.1 同步锁

通常用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象。如果需要方位该资源,调用acquire方法来湖区对象(如果其他线程已经获取了该锁,则当前线程需要等待其被释放),使用release方法释放锁。
先看下面的案例

num = 100

def fun_sub():
	global num
	num2 = num
	time.sleep(0.001)
	num = num2 - 1


if __name__ == '__main__':
	print('开始测试同步锁 at %s' % time.ctime())
	thread_list = []
	for thread in range(100):
		t = threading.Thread(target = fun_sub)
		t.start()
		thread_list.append(t)
	for t in thread_list:
		t.join()
	print('num is %d'%num)
	print('结束测试同步锁 at %s'%time.ctime())
num is 72
结束测试同步锁 at Wed Mar 24 22:06:38 2021

上面的例子,我们每个线程从公共资源num变量执行减1操作,正常情况下,等到代码执行结束,得到的num为0。但是结果却是72,分析下代码执行流程。

  1. 因为GIL,只有一个线程(假设线程1)拿到了num资源,然后把变量赋值给num2,sleep 0.001秒,这时候num=100。
  2. 第一个线程sleep的时候,这个线程会做yield操作,就是cpu切换给别的线程(假设线程2拿到GIL,获得cpu使用权),线程2拿到和线程1一样的num,返回赋值给这时候num有可能还是100,然后sleep,这个时候num还是100.
  3. 线程2 sleep的时候,又要yield操作,假设线程3拿到num,执行上面的操作,num还有可能是100.
  4. 等到后面cpu重新切换给线程1,线程2,线程3执行时,他们执行减1操作后,其实得到的num都是99,不是顺序递减的。
  5. 其他线程操作如上

所以实际的运行过程并不是我们想象的按顺序减,这个时候就需要python的同步锁了,同一时间只能放一个线程来操作num变量,减1后,后面的线程操作来操作num。
使用同步锁,一次只有一个线程操作同享资源。

num = 100

def fun_sub():
	global num
	lock.acquire()
	print('---------加锁------', t.name)
	num2 = num
	time.sleep(0.001)
	num = num2 - 1
	lock.release()
	print('--------释放锁----------', num)


if __name__ == '__main__':
	print('开始测试同步锁 at %s' % time.ctime())
	lock = threading.Lock() #创建锁
	thread_list = []
	for thread in range(100):
		t = threading.Thread(target = fun_sub)
		t.start()
		thread_list.append(t)
	print('join')
	for t in thread_list:
		t.join()
	print('num is %d'%num)
	print('结束测试同步锁 at %s'%time.ctime())
num is 0
结束测试同步锁 at Wed Mar 24 18:35:09 2021

2.2 python死锁

介绍下死锁怎么产生的:

  1. A拿了一个苹果
  2. B拿了一个香蕉
  3. A现在想再拿个香蕉,就在等待B释放这个香蕉
  4. B同时想要再拿个苹果,这时候就等待A释放苹果
  5. 这样就是陷入了僵局,这就是生活中的死锁

python中在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,因为系统判断这部分资源都正在使用,所有这两个线程在无外力作用下将一直等待下去。下面是一个死锁的例子:

lock_apple = threading.Lock()
lock_banana = threading.Lock()

class MyThread(threading.Thread):
	def __init__(self):
		threading.Thread.__init__(self)

	def run(self):
		self.fun1()
		self.fun2()

	def fun1(self):
		lock_apple.acquire()
		print("线程 %s, 想拿: %s--%s"%(self.name,"苹果",time.ctime()))

		lock_banana.acquire()
		print("线程%s, 想拿: %s--%s"%(self.name,"香蕉",time.ctime()))
		lock_banana.release()
		lock_apple.release()

	def fun2(self):
		lock_banana.acquire()
		print("线程%s, 想拿: %s--%s"%(self.name,"香蕉",time.ctime()))
		time.sleep(0.1)

		lock_apple.acquire()
		print("线程 %s, 想拿: %s--%s"%(self.name,"苹果",time.ctime()))
		
		lock_apple.release()
		lock_banana.release()

if __name__ == "__main__":
	for i in range(0, 10):
		my_thread = MyThread()
		my_thread.start()
线程 Thread-1, 想拿: 苹果--Wed Mar 24 22:42:14 2021
线程Thread-1, 想拿: 香蕉--Wed Mar 24 22:42:14 2021
线程Thread-1, 想拿: 香蕉--Wed Mar 24 22:42:14 2021
线程 Thread-2, 想拿: 苹果--Wed Mar 24 22:42:14 2021

代码处理流程:

  1. fun1中,线程1先拿了苹果,然后拿了香蕉,然后释放香蕉和苹果,然后再在fun2中又拿了香蕉,sleep 0.1秒。
  2. 在线程1的执行过程中,线程2进入了,因为苹果被线程1释放了,线程2这时候获得了苹果,然后想拿香蕉
  3. 这时候就出现问题了,线程一拿完香蕉之后想拿苹果,返现苹果被线程2拿到了,线程2拿到苹果执行,想拿香蕉,发现香蕉被线程1持有了
  4. 双向等待,出现死锁,代码执行不下去了

2.3 Python递归锁RLock

import threading
import time

lock = threading.RLock()  #递归锁


class MyThread(threading.Thread):

    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        self.fun1()
        self.fun2()

    def fun1(self):

        lock.acquire()  # 如果锁被占用,则阻塞在这里,等待锁的释放

        print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))

        lock.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        lock.release()
        lock.release()


    def fun2(self):

        lock.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "香蕉",time.ctime()))
        time.sleep(0.1)

        lock.acquire()
        print ("线程 %s , 想拿: %s--%s" %(self.name, "苹果",time.ctime()))
        lock.release()

        lock.release()

if __name__ == "__main__":
    for i in range(0, 10):  #建立10个线程
        my_thread = MyThread()  #类继承法是python多线程的另外一种实现方式
        my_thread.start()

上面我们用一把递归锁,就解决了多个同步锁导致的死锁问题。大家可以把RLock理解为大锁中还有小锁,只有等到内部所有的小锁,都没有了,其他的线程才能进入这个公共资源。

3.多进程

3.1 多进程模块multiprocessing

from multiprocessing import Process

# sample
def fun1(name):
    print("测试%s多进程"%name)

if __name__ == '__main__':
    process_list = []
    for i in range(5):
        p = Process(target = fun1, args=('python',))
        p.start()
        process_list.append(p)

    for i in process_list:
        p.join()

    print('结束测试')
使用类继承方法实现多进程
## inherit
class MyProcess(Process):
    def __init__(self, name):
        super(MyProcess, self).__init__()
        self.name = name

    def run(self):
        print('测试%s多进程'%self.name)

if __name__ =='__main__':
    process_list = []
    for i in range(5):
        p = MyProcess('Python')
        p.start()
        process_list.append(p)
    for i in process_list:
        p.join()
    print('测试结束')

3.2 python多线程通信

进程是系统独立调度核分配系统资源(CPU、内存)的基本单位,进程之间是相互独立的,每启动一个新的进程相当于把数据进行了一次克隆,子进程里的数据修改无法影响到主进程中的数据,不同子进程之间的数据也不能共享,这是多进程在使用中与多线程最明显的区别。但是难道Python多进程中间难道就是孤立的吗?当然不是,python也提供了多种方法实现了多进程中间的通信和数据共享(可以修改一份数据)。

3.2.1进程队列 Queue

通过Queue获取子进程中put的数据,实现进程间的通信。

from multiprocessing import Process, Queue

def fun1(q, i):
    print('子进程%s 开始put数据'%i)
    q.put('我是%s通过Queue通信'%i)

if __name__ == '__main__':
    q = Queue()

    process_list = []
    for i in range(3):
        p = Process(target=fun1, args = (q, i))
        p.start()
        process_list.append(p)
    for i in process_list:
        p.join()

    print('主进程获取Queue数据')
    print(q.get())
    print(q.get())
    print(q.get())
    print('结束测试')
3.2.2 管道Pipe

管道Pipe和Queue的作用大致差不多,也是实现进程间的通信

from multiprocessing import Process, Pipe
def fun1(conn):
    print('子进程发送消息:')
    conn.send('你好主进程')
    print('子进程接收消息:')
    print(conn.recv())
    conn.close()

if __name__ == '__main__':
    conn1, conn2 = Pipe()
    p = Process(target = fun1, args = (conn2, ))
    p.start()
    print('主进程接受消息:')
    print(conn1.recv())
    print('主进程发送消息: ')
    conn1.send("你好子进程")
    p.join()
    print('测试结束')
主进程接受消息:
子进程发送消息:
子进程接收消息:
你好主进程
主进程发送消息: 
你好子进程
测试结束
3.2.3 Manager

Queue和Pipe只是实现了数据交互,并没实现数据共享,即一个进程去更改另一个进程的数据。那么就要用到Managers。

from multiprocessing import Process, Manager
def fun1(dic, lis, index):
    dic[index] = 'a'
    dic['2'] = 'b'
    lis.append(index)

if __name__ == '__main__':
    with Manager() as manager:
        dic = manager.dict()
        l = manager.list(range(5))

        process_list = []
        for i in range(10):
            p = Process(target = fun1, args = (dic, l, i))
            p.start()
            process_list.append(p)

        for res in process_list:
            res.join()
        print(dic)
        print(l)
{0: 'a', '2': 'b', 1: 'a', 2: 'a', 3: 'a', 4: 'a', 5: 'a', 6: 'a', 9: 'a', 8: 'a', 7: 'a'}
[0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 6, 9, 8, 7]

可以看到主进程定义了一个字典和一个列表,在子进程中,可以添加和修改字典的内容,在列表中插入新的数据,实现进程间的数据共享,即可以共同修改同一份数据.

3.2.4 进程池

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。就是固定有几个进程可以使用。

3.2.4 进程池map方法
import os 
import PIL 

from multiprocessing import Pool 
from PIL import Image

SIZE = (75,75)
SAVE_DIRECTORY = \'thumbs\'

def get_image_paths(folder):
    return (os.path.join(folder, f) 
            for f in os.listdir(folder) 
            if \'jpeg\' in f)

def create_thumbnail(filename): 
    im = Image.open(filename)
    im.thumbnail(SIZE, Image.ANTIALIAS)
    base, fname = os.path.split(filename) 
    save_path = os.path.join(base, SAVE_DIRECTORY, fname)
    im.save(save_path)

if __name__ == \'__main__\':
    folder = os.path.abspath(
        \'11_18_2013_R000_IQM_Big_Sur_Mon__e10d1958e7b766c3e840\')
    os.mkdir(os.path.join(folder, SAVE_DIRECTORY))

    images = get_image_paths(folder)

    pool = Pool()
    pool.map(creat_thumbnail, images) #关键点,images是一个可迭代对象
    pool.close()
    pool.join()

上边这段代码的主要工作就是将遍历传入的文件夹中的图片文件,一一生成缩略图,并将这些缩略图保存到特定文件夹中。这我的机器上,用这一程序处理 6000 张图片需要花费 27.9 秒。 map 函数并不支持手动线程管理,反而使得相关的 debug 工作也变得异常简单。

map在爬虫的领域里也可以使用,比如多个URL的内容爬取,可以把URL放入元祖里,然后传给执行函数。