Python高级教程笔记:多线程初步

0x01 线程、进程初步

进程是系统分配资源的最小单位。一个程序至少有一个进程。

线程是OS中可以进行调度的最小单位,被包含在进程之中,是进程中的实际运作单位。

一个进程中可以存在多个线程,在单核CPU中每个进程中同时刻只能运行一个线程,只有在多核CPU中才能存在线程并发的情况。

进程拥有自己的独立地址空间(相对地址空间),内存、数据栈等,因此进程占用资源较多。由于进程的资源独立,所以通讯不方便,只能使用进程间通讯(IPC)。但是,同一个进程中的线程都有共性多个线程共享同一个进程的虚拟空间。线程共享的环境包括进程代码段、进程的公有数据及文件等,利用这些共享的数据,线程之间很容易实现通信。

此外,操作系统在创建进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源,但创建线程则简单得多。

因此,使用多线程来实现并发比使用多进程的性能要高得多。

0x02 线程实现

普通的创建方式:需要继承threading.Thread,继承出的子类需要实现run方法。

import threading

import time

class Mythread(threading.Thread):
    def __init__(self, n):
        super(Mythread, self).__init__()
        self.n = n
        
    def run(self):
        print("task", self.n)
        time.sleep(1)
        print('2s')
        time.sleep(1)
        print('1s')
        time.sleep(1)
        print('0s')
        time.sleep(1)
        print("finish", self.n)
        
if __name__ == "__main__":
    t1 = Mythread("t1")
    t1.start()
    t2 = Mythread("t2")
    t3 = Mythread("t3")
    t2.start()
    t3.start()
    t4 = Mythread("t4")
    t5 = Mythread("t5")
    
    t4.start()
    t5.start()

运行结果如下:

task t1
task t2
tasktask t4
 t3
task t5
2s
2s
2s2s

2s
1s
1s
1s1s

1s
0s
0s
0s0s

0s
finish t1
finish t2
finishfinish  t3
t4
finish t5

之所以出现排版这么奇葩的情况,就是因为各个线程只会运行一定量的时间片。一个线程如果运行时间到了之后,CPU就会转而执行其他的线程。

比如线程 i 执行一个循环,执行了一半之后cpu暂停执行线程 i 了,转而执行线程 j …

0x03 守护线程

若在主线程中创建了子线程,当主线程结束时根据子线程daemon(设置Thread.setDaemon(True))属性值的不同可能会发生下面的两种情况之一:

  • 如果某个子线程的daemon属性为True,主线程运行结束时不对这个子线程进行检查而直接退出,同时所有daemon值为True的子线程将随主线程一起结束,而不论是否运行完成。
  • 如果某个子线程的daemon属性为False,主线程结束时会检测该子线程是否结束,如果该子线程还在运行,则主线程会等待它完成后再退出。

实验代码如下:

from threading import Thread, Lock
import os, time

def work():
	global n
	#lock.acquire()
	temp = n
	time.sleep(.1)
	n = temp - 1
	print(n)
	#lock.release()

if __name__ == "__main__":
	lock = Lock()
	n = 100
	for i in range(10):
		p = Thread(target=work)
		p.setDaemon(True)
		p.start()

结果如下:

9999
99
9999
99
99
99

99

99

可以看见,主线程在执行完之后直退出了,并没有等待子线程结束完才退出。

0x04 多线程共享全局变量

在同一个进程中的多线程是共享资源的,比如变量、文件等等。

import threading
import time

g_num = 100.0

def work1():
	global g_num
	for i in range(5):
		g_num += 1
		time.sleep(2)
		print(f"work 1 :g_num is {
      
      g_num}")
		
def work2():
	global g_num
	for i in range(10):
		g_num -= .5
		time.sleep(1)
		print(f"work 2 :g_num is {
      
      g_num}")

if __name__ == "__main__":
	t1 = threading.Thread(target=work1)
	t2 = threading.Thread(target=work2)
	t1.start()
	t2.start()

运行结果如下:

work 2 :g_num is 100.5
work 1 :g_num is 100.0
work 2 :g_num is 101.0
work 2 :g_num is 100.5
work 1 :g_num is 100.0
work 2 :g_num is 101.0
work 2 :g_num is 100.5
work 1 :g_num is 100.0
work 2 :g_num is 101.0
work 2 :g_num is 100.5
work 1 :g_num is 100.0
work 2 :g_num is 101.0
work 2 :g_num is 100.5
work 1 :g_num is 100.0
work 2 :g_num is 100.0

0x05 互斥锁

为了解决线程不同步的问题,对于有些资源(比如变量、代码)我们需要确保一件事:在任意时刻只有0个或者1个线程可以访问资源。解决办法就是使用互斥锁Lock。

from threading import Thread, Lock
import os, time

def work():
	global n
	lock.acquire()
	temp = n
	time.sleep(.1)
	n = temp - 1
	print(n)
	lock.release()

if __name__ == "__main__":
	lock = Lock()
	n = 100
	l = []
	for i in range(10):
		p = Thread(target=work)
		l.append(p)
		p.start()
	
	# 这里的join是指让主线程等待新开的线程运行结束之后再结束
	for p in l:
		p.join()

加入线程同步之后的运行结果:

99
98
97
96
95
94
93
92
91
90
from threading import Thread, Lock
import os, time

def work():
	global n
	#lock.acquire()
	temp = n
	time.sleep(.1)
	n = temp - 1
	print(n)
	#lock.release()

if __name__ == "__main__":
	lock = Lock()
	n = 100
	l = []
	for i in range(10):
		p = Thread(target=work)
		l.append(p)
		p.start()

	# 这里的join是指让主线程等待新开的线程运行结束之后再结束
	for p in l:
		p.join()

没有线程同步的运行结果:

99
99
99
99
99
99
99
99
99
99

0x06 RLock锁(可重入锁,递归锁)

如果用了Lock锁,那么下面的代码会发生阻塞:

import threading
lock = threading.Lock()

lock.acquire()
    for i in range(10):
        print('获取第二把锁')
        lock.acquire()
        print(f'test.......{
      
      i}')
        lock.release()
    lock.release()

但是这个锁换成RLock锁,就不会发生阻塞。

RLock锁会检查申请使用锁的是哪个线程。在锁被占用的情况下,

  • 如果申请使用锁的是同一个线程,那么RLock会为该线程维护一个计数器,并且给计数器+1;
  • 如果申请使用锁的线程不是同一个,则会被阻塞。

0x07 信号量(Semaphore)

互斥锁只允许一个线程更改数据,但是信号量允许一定数量的线程更改数据。

比如只有三个蹲坑的撤硕。

import threading
import time

def run(n, semaphore):
    semaphore.acquire()   #加锁
    time.sleep(1)
    print("run the thread:%s\n" % n)
    semaphore.release()     #释放

if __name__ == '__main__':
    num = 0
    semaphore = threading.BoundedSemaphore(5)  # 最多允许5个线程同时运行
    for i in range(22):
        t = threading.Thread(target=run, args=("t-%s" % i, semaphore))
        t.start()
    while threading.active_count() != 1:
        pass  # print threading.active_count()
    else:
        print('-----all threads done-----')

0x08 事件类(Event)

Event类用于主线程控制其它线程的运行,事件是一个简单的线程同步对象,其主要提供以下几个方法:

  • clear 将flag设置为“False”
  • set 将flag设置为“True”
  • is_set 判断是否设置了flag
  • wait 会一直监听flag,如果没有检测到flag就一直处于阻塞状态

事件处理的机制:全局定义了一个“flag”,当flag值为“False”,那么event.wait()就会阻塞,当flag值为“True”,那么event.wait()便不再阻塞。

#利用Event类模拟红绿灯
import threading
import time

event = threading.Event()

def lighter():
    count = 0
    event.set()     #初始值为绿灯
    while True:
        if 5 < count <=10 :
            event.clear()  # 红灯,清除标志位
            print("\33[41;1mred light is on...\033[0m")
        elif count > 10:
            event.set()  # 绿灯,设置标志位
            count = 0
        else:
            print("\33[42;1mgreen light is on...\033[0m")

        time.sleep(1)
        count += 1

def car(name):
    while True:
        if event.is_set():      #判断是否设置了标志位
            print("[%s] running..."%name)
            time.sleep(1)
        else:
            print("[%s] sees red light,waiting..."%name)
            event.wait()
            print("[%s] green light is on,start going..."%name)

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

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

0x09 全局解释器锁(GIL)

在非python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。

但在python中,无论有多少核,同时只能执行一个线程。究其原因,就是GIL的存在。

这也就是人们说Python是伪多线程的原因。

GIL全称全局解释器锁,来源是python设计之初的考虑,为了数据安全所做的决定。

某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在cpython中才有,因为cpython调用的是c语言的原生线程,所以他不能直接操作cpu,只能利用GIL保证同一时间只能有一个线程拿到数据。

而在pypy和jpython中是没有GIL的。

Python多线程的工作过程:

python在使用多线程的时候,调用的是c语言的原生线程。

  • 拿到公共数据
  • 申请gil
  • python解释器调用os原生线程
  • os操作cpu执行运算
  • 当该线程执行时间到后,无论运算是否已经执行完,gil都被要求释放
  • 进而由其他进程重复上面的过程
  • 等其他进程执行完后,又会切换到之前的线程(从他记录的上下文继续执行),整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)。

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

在python3.x中,GIL使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。

对于多核CPU运行单个python进程而言,真的就是“一核有难,七核围观”。

猜你喜欢

转载自blog.csdn.net/weixin_43466027/article/details/119818066