多任务----线程、同步、互斥锁、死锁

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/Scrat_Kong/article/details/99642542

多任务

        简单来说,就是同时进行多个任务,比如一边上班一边刷手机,咳咳。。。一边唱歌,一边跳舞,一边打篮球,一边还偷瞄旁边的妹子,嗯,这都是多任务。

罗宾的技能,以及千手观音,哪吒三头六臂。

通常的程序是这样式儿的:

# 顺序执行(非多任务)
from time import sleep

def work():
    for i in range(3):
        print('我在工作...%d'%i)
        sleep(1)

def play():
    for i in range(3):
        print('我在玩手机...%d'%i)
        sleep(1)

if __name__ == '__main__':
    work()
    play()

        多核CPU已经非常普及了,即使过去的单核CPU,也可以执行多任务。CPU执行代码都是顺序执行的,单核CPU是怎么执行多任务的呢?

        时间片轮转----操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

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

  • 并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)。
  • 并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的。

 

 

线程

Python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用。

Python中threading的使用

1.单线程执行

import time

def sayLove():
    print('亲爱的,我爱你!')
    time.sleep(1)

if __name__ == '__main__':
    for i in range(5):
        sayLove()

运行结果:

# 1s一条
亲爱的,我爱你!
亲爱的,我爱你!
亲爱的,我爱你!
亲爱的,我爱你!
亲爱的,我爱你!

2.多线程执行

import time
import threading

def sayLove():
    print("亲爱的,我爱你!")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=sayLove)
        t.start() # 启动线程,让线程开始执行。
    

 运行结果:

# 1s 全出
亲爱的,我爱你!
亲爱的,我爱你!
亲爱的,我爱你!
亲爱的,我爱你!
亲爱的,我爱你!

注:

  1. 主线程会等待所有子线程结束后才结束
  2. 通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法。
  3. 多线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行。上面的代码中只能保证每个线程都运行完整个run函数,但是线程的启动顺序、run函数中每次循环的执行顺序都不能确定。
"""
重写run方法
Python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。
创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,
当该线程获得执行的机会时,就会调用run方法执行线程。
"""
import threading
import time

class MyThread(threading.Thread): # 继承threading.Thread
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
            print(msg)


if __name__ == '__main__':
    t = MyThread()
    t.start()

 

多线程共享全局变量

虽然大家都能骑,不过最好一次一个哦~不然翻车警告!
from threading import Thread
import time

g_num = 100

def work1():
    global g_num
    for i in range(3):
        g_num += 1

    print("----in work1, g_num is %d---"%g_num)


def work2():
    global g_num
    print("----in work2, g_num is %d---"%g_num)


print("---线程创建之前g_num is %d---"%g_num)

t1 = Thread(target=work1)
t1.start()

#延时一会,保证t1线程中的事情做完
time.sleep(1)

t2 = Thread(target=work2)
t2.start()

运行结果:

---线程创建之前g_num is 100---
----in work1, g_num is 103---
----in work2, g_num is 103---

列表做实参传递到线程中

from threading import Thread
import time

def work1(nums):
    nums.append(44)
    print("----in work1---",nums)


def work2(nums):
    #延时一会,保证t1线程中的事情做完
    time.sleep(1)
    print("----in work2---",nums)

g_nums = [11,22,33]

t1 = Thread(target=work1, args=(g_nums,))
t1.start()

t2 = Thread(target=work2, args=(g_nums,))
t2.start()

运行结果:

----in work1--- [11, 22, 33, 44]
----in work2--- [11, 22, 33, 44]
  • 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
  • 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全

线程安全----多线程共享全局变量可能遇到的问题

假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算,t1和t2各对g_num加10次,g_num的最终的结果应该为20。

但是由于是多线程同时操作,有可能出现下面情况:

  1. 在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0
  2. 然后t2对得到的值进行加1并赋给g_num,使得g_num=1
  3. 然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
  4. 这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1

即多线程状态下,加了两次结果只加了一次。

import threading
import time

g_num = 0

def work1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work1, g_num is %d---"%g_num)


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)


print("---线程创建之前g_num is %d---"%g_num)

t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()  # 启动线程

t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

运行结果:

---线程创建之前g_num is 0---
----in work1, g_num is 1088005---
----in work2, g_num is 1286202---
2个线程对同一个全局变量操作之后的最终结果是:1286202

---线程创建之前g_num is 0---
----in work1, g_num is 1240520---
----in work2, g_num is 1347813---
2个线程对同一个全局变量操作之后的最终结果是:1347813

---线程创建之前g_num is 0---
----in work2, g_num is 1328364---
----in work1, g_num is 1342582---
2个线程对同一个全局变量操作之后的最终结果是:1342582

# 执行三次,结果完全不同啊

 多线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确。

 

同步

协同同步,按照预定的先后顺序依次运行,你先bb, 然后我再说。如上的多线程竞争资源问题,就可以通过线程同步来解决。

思路如下:

  1. 系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num
  2. t1对g_num的值进行+1
  3. t1解锁,此时g_num的值为1,其他的线程就可以使用g_num了,而且是g_num的值不是0而是1
  4. 同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性

总结:同一时刻只允许一个线程对共享资源拥有处置权。

互斥锁

        当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

互斥锁为资源引入一个状态:锁定/非锁定

        某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

        比如生活中的各种排队场景,售票处(共享资源)每次只能为有限位顾客服务,服务时,就是上锁,未在服务状态时,便可用。

Python 的 threading模块中定义了Lock类,可以方便的处理锁定:

# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()

注意:

  • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
  • 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止
import threading
import time

g_num = 0

def work1(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()
    print('---work1----g_num=%d'%g_num)

def work2(num):
    global g_num
    for i in range(num):
        mutex.acquire()
        g_num += 1
        mutex.release()
    print('----work2----g_num=%d'%g_num)

# 创建互斥锁,默认未上锁
mutex = threading.Lock()


# 创建两个线程,让他们各自对g_num 加10000次
p1 = threading.Thread(target=work1, args=(10000,))
p1.start()

p2 = threading.Thread(target=work2, args=(10000,))
p2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)
print('2个线程对同一个全局变量操作之后的结果是%s'%g_num)

运行结果:

---work1----g_num=10000
----work2----g_num=20000
2个线程对同一个全局变量操作之后的结果是20000

上锁解锁过程

        当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

        每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

        线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

上锁的优缺点:

优点:

  • 上锁可以保证一个线程从头到尾完整的执行。

缺点:

  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程执行,效率会大大下降。
  • 锁可以存在多个,不同线程持有不同的锁,并试图获取对方持有的锁的时候,可能会造成死锁。

死锁

        一转身就是一辈子,AB互生情愫,但都不确信对方喜欢自己,都在等对方的态度,就错过了,这种都等待对方的状态就是死锁。人‘死锁'会错过,程序死锁会卡在那儿。

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

尽管死锁很少发生,但一旦发生就会造成应用的停止响应。

下面看一个死锁的例子:

import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()

        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name+'----do1---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()

        # 对mutexA解锁
        mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()

        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name+'----do2---up----')
        time.sleep(1)

        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()

        # 对mutexB解锁
        mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

避免死锁:

  • 程序设计时要尽量避免(银行家算法)
  • 添加超时时间等

 

 

银行家算法

[背景知识]

        一个银行家如何将一定数目的资金安全地借给若干个客户,使这些客户既能借到钱完成要干的事,同时银行家又能收回全部资金而不至于破产,这就是银行家问题。这个问题同操作系统中资源分配问题十分相似:银行家就像一个操作系统,客户就像运行的进程,银行家的资金就是系统的资源。

[问题的描述]

        一个银行家拥有一定数量的资金,有若干个客户要贷款。每个客户须在一开始就声明他所需贷款的总额。若该客户贷款总额不超过银行家的资金总数,银行家可以接收客户的要求。客户贷款是以每次一个资金单位(如1万RMB等)的方式进行的,客户在借满所需的全部单位款额之前可能会等待,但银行家须保证这种等待是有限的,可完成的。

例如:有三个客户C1,C2,C3,向银行家借款,该银行家的资金总额为10个资金单位,其中C1客户要借9各资金单位,C2客户要借3个资金单位,C3客户要借8个资金单位,总计20个资金单位。某一时刻的状态如图所示。

        对于a图的状态,按照安全序列的要求,我们选的第一个客户应满足该客户所需的贷款小于等于银行家当前所剩余的钱款,可以看出只有C2客户能被满足:C2客户需1个资金单位,小银行家手中的2个资金单位,于是银行家把1个资金单位借给C2客户,使之完成工作并归还所借的3个资金单位的钱,进入b图。同理,银行家把4个资金单位借给C3客户,使其完成工作,在c图中,只剩一个客户C1,它需7个资金单位,这时银行家有8个资金单位,所以C1也能顺利借到钱并完成工作。最后(见图d)银行家收回全部10个资金单位,保证不赔本。那麽客户序列{C1,C2,C3}就是个安全序列,按照这个序列贷款,银行家才是安全的。否则的话,若在图b状态时,银行家把手中的4个资金单位借给了C1,则出现不安全状态:这时C1,C3均不能完成工作,而银行家手中又没有钱了,系统陷入僵持局面,银行家也不能收回投资。

        综上所述,银行家算法是从当前状态出发,逐个按安全序列检查各客户谁能完成其工作,然后假定其完成工作且归还全部贷款,再进而检查下一个能完成工作的客户,......。如果所有客户都能完成工作,则找到一个安全序列,银行家才是安全的。

更多:https://blog.csdn.net/Scrat_Kong/article/details/90257118

 Scrat 一个热爱坚果的松鼠 ~

猜你喜欢

转载自blog.csdn.net/Scrat_Kong/article/details/99642542