Python多线程编程初步探索(二)

Python多线程编程初步探索(二)

多个线程

参考博文:https://www.jianshu.com/p/e50b9e4ce5aa
7. 关于同步、锁的问题
真正的多线程在操作系统底层是如何实现的呢?——GIL(全局解释器锁)。无论你启多少个线程,你有多少个cpu, Python在执行一个进程的时候会淡定的在同一时刻只允许一个线程运行。所以,python是无法利用多核CPU实现多线程的。 这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。详见博文[https://www.jianshu.com/p/e50b9e4ce5aa],这部分不再讨论。
(1)当两个并行的线程同时要访问或修改一个数据时,怎么办呢?是谁来修改?这就关乎同步的问题了。
先看代码:

import time
import threading

def addNum():
    global num  # 在每个线程中都获取这个全局变量
    # ''' 实验1
    # num -= 1
    # time.sleep(1)
    # ''' 实验1

    # ''' 实验2
    # temp = num
    # time.sleep(1)
    # num = temp - 1  # 对此公共变量进行-1操作
    # ''' 实验2

    # ''' 实验3
    # temp = num
    # time.sleep(0.0001)
    # num = temp - 1  # 对此公共变量进行-1操作
    # ''' 实验3

    # ''' 实验4
    lock.acquire()  # 获取这把锁
    temp = num
    time.sleep(0.01)
    num = temp - 1
    lock.release()  # 释放这把锁
    time.sleep(5)
    # ''' 实验4

num = 100  # 设定一个共享变量
thread_list = []
lock=threading.Lock()       #创建一把锁
for i in range(100):
    t = threading.Thread(target=addNum)
    t.start()
    # 如果这里紧跟t.join(),则start一个线程后,该线程随即阻塞主线程。
    # 主线程上该线程运行完毕后,主线程方能进入下一次循环开启新线程。这里有join的话就不能实现多线程。
    print(threading.enumerate())
    thread_list.append(t)

for t in thread_list:  # 等待所有线程执行完毕
    t.join() # 这里的join与上面的情况就不同了。此时多个线程(如100个)已经启动了,对于线程列表中的每个线程依次阻塞主线程。
    # 某一时间只有1个线程阻塞了主线程,或者说,主线程等待所有线程结束后方才结束。
# print(threading.enumerate())
print('Result: ', num)

对于实验1,100个线程几乎同时运行函数,但肯定是有先后的,但不管谁先谁后,不管是谁只要运行一次num -= 1,num就会减小1.最终结果为0.
对于实验2,100个线程同时竞争运行函数,100个线程启动的时间很快,相差时间估计小于0.0001秒,大家都在很短的时间内获得num=100,都交给各自的临时变量temp,然后几乎同时进入睡眠(1s的时间)。睡眠时间较长,在这个时间里,所有线程均已获得num的值,而且num并没有被任何一个线程更改。
第一个线程竞争到的肯定率先醒来速度极快计算完,num = temp - 1=99,线程2醒来时仍然从上面携带的100同样计算num=99,…,大家算的结果都是99。
对于实验3,各线程的睡眠时间变得很短,当线程1醒来时,for循环还未执行完毕,即线程1开始修改num时还有其他线程刚刚启动,此时其他线程首次获得的num值已经被线程1修改为99.而num不断有线程修改,而且不断有线程要初次获得这个值。此时,0.0001秒内究竟有几个线程在睡眠,又有几个线程在修改,每次都不确定,结果呈现了很大的不确定性。这就是多线程同时修改数据导致的不一致性。
为此,需要对访问、修改该数据的过程进行上锁。即同一时间只能有一个线程进行这一段的操作。大家都来了的话,就要排队。这就是实验4.“上锁的作用是这个线程未结束其他线程无法竞争,只能等,使这一段成为一个串行,运行时间为0.0001s*100次。”那么多线程变为一个串行后,还有用吗?当然,因为串行仅在这一段,其他部分(time.sleep(5))仍然是并行的。如果不采用多线程,则执行这100次功能,就需要(0.0001s+5s)100,。而多线程的总时间约为0.0001s100+5s。
综上,采用多线程执行同一函数,通过lock的方式可以避免对共享数据修改造成的混乱。经过以上这些内容,我们可以轻松的通过创建多线程的方式,加快程序的运行时间:通过类继承创建新线程,新线程中的run来执行同一或不同函数,并向函数传递各自的实参,对于共享数据采用global的方式及lock,实现对共享数据的修改。下面是一个综合实例:

import time
import threading

global num  # 在每个线程中都获取这个全局变量,每个线程都要用这个数据并可能对其修改
num = 100  # 设定一个共享变量的初值

def addNum(name, change_num, delay):
    global num  # 在每个线程中都获取这个全局变量,每个线程都要用这个数据并可能对其修改
    lock.acquire()  # '''上锁
    temp = num
    time.sleep(0.0001)
    num = temp - change_num
    lock.release()  # '''释放这把锁,
    print("The current thread is " + name)  # 这一段不再需要上锁,因为这不是共同内容,这是各线程自己的事
    print("The current number is" + str(num))  # 这一段不再需要上锁,因为这不是共同内容,这是各线程自己的事
    time.sleep(delay)  # 这一段不再需要上锁,因为这不是共同内容,这是各线程自己的事


class MyThread(threading.Thread):  # 线程类继承式创建
    def __init__(self, name, change_num, delay):
        threading.Thread.__init__(self)
        self.name = name
        self.change_num = change_num
        self.delay = delay

    def run(self):  # 调用同一个函数,并赋予各自的实参
        addNum(self.name, self.change_num, self.delay)



thread_list = []
lock = threading.Lock()  # 创建一把锁
for i in range(100):  # 创建100个线程,同时运行
    if (i % 2 == 0):
        a = 1
        b = 4
    else:
        a = 0
        b = 5  # 所有线程并发,系统总的耗时大体取决于delay最大者。

    t = MyThread(str(i), a, b)  # 创建线程,每个线程可以同时调用一个函数,并且向函数传递各自的实参
    t.start()  # 线程启动,随即运行run
    print(threading.enumerate())  # 看一下当下的线程有哪些(观察后台的情况)
    thread_list.append(t)

for t in thread_list:  # 主线程要等待所有线程执行完毕,方可结束
    t.join()

print('Result: ', num)  # 由于仅偶数线程修改num值,故最终结果为50.

(2)上锁、开锁使得数据免于混乱,各线程对于数据的操作有了一个排队的秩序。其实锁这个概念很形象,大家都要访问一个房间,必须排队、上锁、开锁,而不能一拥而上。但是有时候上锁这个手段缺可能导致死锁。见下述代码:

import threading
import time

mutexA = threading.Lock()   #创建一把锁
mutexB = threading.Lock()


class MyThread(threading.Thread):

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

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

    def fun1(self):
        mutexA.acquire()  # 如果锁被占用,则阻塞在这里,等待锁的释放

        print("I am %s , get res: %s---%s" % (self.name, "ResA", time.time()))

        mutexB.acquire()
        print("I am %s , get res: %s---%s" % (self.name, "ResB", time.time()))
        mutexB.release()
        mutexA.release()   #释放这把锁

    def fun2(self):
        mutexB.acquire()
        print("I am %s , get res: %s---%s" % (self.name, "ResB", time.time()))
        time.sleep(0.2)

        mutexA.acquire()  #获取这把锁
        print("I am %s , get res: %s---%s" % (self.name, "ResA", time.time()))
        mutexA.release()

        mutexB.release()

if __name__ == "__main__":

    print("start-----------%s" % time.time())

    for i in range(0, 10):
        my_thread = MyThread()
        my_thread.start()

首先,线程1启动执行run,经过fun1后进入fun2,获得B锁开始睡眠,其他线程启动执行run,进入fun1获得A锁等待B锁,线程1醒来后等待A锁。这样便形成一个死锁环,线程1拥有B需要等A,其他线程拥有A需要等B。这个概念其实和动力学仿真中的代数环雷同。究其原因,是因为数据访问过程存在一个外环(房间的外间)和一个内环(内间)的结构,有人进入了外间在等内间的锁开,持有内间锁的人却在屋外等待外间开锁。
解决方法为采用可重入锁,RLock。“这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:”

import threading
import time

Rlock = threading.RLock()  # 创建一个递归锁


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

    def run(self):
        self.func1()
        self.func2()

    def func1(self):
        Rlock.acquire()  # 如果锁被占用,则阻塞在这里,等待锁的释放 counter = 1
        print('I am %s ,get res: %s --- %s ' % (self.name, 'ResA', time.time()))

        Rlock.acquire()  # counter = 2
        print('I am %s ,get res: %s --- %s ' % (self.name, 'ResB', time.time()))
        Rlock.release()  # counter = 1

        Rlock.release()  # counter = 0

    def func2(self):
        Rlock.acquire()  # counter = 1
        print('I am %s ,get res: %s --- %s ' % (self.name, 'ResB', time.time()))
        time.sleep(0.2)

        Rlock.acquire()  # counter = 2
        print('I am %s ,get res: %s --- %s ' % (self.name, 'ResA', time.time()))
        Rlock.release()  # counter = 1   #释放这把锁

        Rlock.release()  # counter = 0

if __name__ == '__main__':
    print('start ----------- %s' % time.time())

    for i in range(0, 10):
        mt = MyThread()
        mt.start()

其实RLock就是一种锁中锁,或者说是一把锁,这把锁可以提供子锁,counter记录了这种锁中锁的层数。各线程要么获得这把锁中锁,要么等待,即外间和内间的锁同一时间只能被一人所有。这样就避免了死锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值