多任务_线程

线程

多任务介绍、以及Thread的基本使用

多任务介绍

现实生活中

有很多的场景中的事情是在同时的情况下进行的,比如驾驶需要手和脚共同来驾驶汽车,再比如唱歌和跳舞也是可以同时进行的;
试想一下,如果把唱歌和跳舞分开成两件事分别来做的话,先唱歌,唱完歌再跳舞,估计效果就没有那么好了。

程序中【没有多任务的程序】

如下方程序,来模拟“唱歌跳舞这件事情”


import time


def sing():
	"""唱歌5秒钟"""

	for i in range():
	print("-----正在跳舞-----")
	time.sleep(1)	


def dance():
	"""跳舞5秒钟"""

	for i in range():
	print("-----正在跳舞-----")
	time.sleep(1)


def main():
	sing()
	dance()


if __name__ == "__main__":
	main()

模拟多任务的程序

import time
import threading


def sing():
	"""唱歌5秒钟"""
	for i in range(5):
		print("-----正在唱:菊花台-----")
	time.sleep(1)


def dance():
	"""跳舞5秒钟"""
	for i in range(5):
		print("-----正在跳舞-----")
	time.sleep(1)


def main():
	t1 = threading.Thread(target=sing)
	t2 = threading.Thread(target=dance)
	t1.start()
	t2.start()


if __name__ == "__main__":
	main()

多任务的概念

什么叫多任务?

简单的说,就是操作系统可以同时运行多个任务。
打个比方,你一边用浏览器看小说,一边用音乐播放器听歌,一边用Word写文档,这就叫多任务,至少同时有3个任务正在运行。还有很多任务悄悄的在后台同时运行着,只是桌面上没有显示而已。

现在,多核CPU已经非常的普及了,但是即使过去的单核CPU,也是可以执行多任务的。由于CPU执行代码都是顺序执行的,那么单核CPU是如何执行多任务的呢?

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

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

Thread创建线程完成多任务

线程

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

使用thread模块

单线程执行

# coding=utf-8
import time


def saySorry():
    print("亲爱的,我错了,我能吃饭了吗?")
    time.sleep(1)


if __name__ == "__main__":
    for i in range(5):
        saySorry()

多线程执行

# coding=utf-8
import threading
import time


def saySorry():
    print("亲爱的,我错了,我能吃饭了吗?")
    time.sleep(1)


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

查看正在运行的线程、主线程等待子线程先结束

查看线程_让某些线程先执行

# coding=utf-8
import threading
import time


def test1():
    for i in range(5):
        print("-----test1-----%d" % i)


def test2():
    for i in range(5):
        print("-----test2-----%d" % i)


def main():
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)

    t1.start()

    time.sleep(1)
    print("---1---")

    t2.start()

    time.sleep(1)
    print("---2---")

    print(threading.enumerate())


if __name__ == "__main__":
    main()

循环查看当前运行的线程

# coding=utf-8
import threading
import time


def test1():
    for i in range(5):  # 循环5秒
        print("-----test1-----%d" % i)
        time.sleep(1)

    # 如果创建Thread时执行的函数,运行结束。那么意味着 这个子线程结束了!


def test2():
    for i in range(10):  # 循环10秒
        print("-----test2-----%d" % i)
        time.sleep(1)


def main():
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)

    t1.start()
    t2.start()

    while True:
        print(threading.enumerate())  # 主线程一直在显示有多少个线程
        if len(threading.enumerate()) <= 1:  # 判断是否还有子线程,如果没有则退出循环
            break
        time.sleep(1)


if __name__ == "__main__":
    main()

  • 如果创建Thread时执行的函数,运行结束。那么意味着 这个子线程结束了!

验证创建线程以及运行时间

# coding=utf-8
import threading
import time


def test1():
    for i in range(5):  # 循环5秒
        print("-----test1-----%d" % i)
        time.sleep(1)


def main():

    # 在调用Thread之前先打印当前线程信息
    print(threading.enumerate())
    t1 = threading.Thread(target=test1)  # 当调用Thread的时候,不会创建线程

    # 在调用Thread之后打印
    print(threading.enumerate())

    t1.start()  # 当调用Thread创建出来的实例对象的start方法的时候,才会创建线程以及让这个线程开始运行

    # 在调用start之后打印
    print(threading.enumerate())


if __name__ == "__main__":
    main()

  • 当调用Thread的时候,不会创建线程
  • 当调用Thread创建出来的实例对象的start方法的时候,才会创建线程以及让这个线程开始运行

通过继承Thread类完成创建线程

线程执行代码的封装

通过上文的小节,能够看出通过使用threading模块能够完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块是,往往会定义一个新的子类class,只要继承 *threading.Thread * ** 就可以了,然后重写 run **方法

代码示例如下

# coding=utf-8
import threading
import time


class MyThread(threading.Thread):

    def run(self):
        for i in range(3):
            time.sleep(1)
            message = "I’m " + self.name + "@" + str(i)  # name属性中保存的是当前线程的名字
            print(message)

        self.login()

        self.logout()

    @staticmethod
    def login():
        print("这是登录...")

    @staticmethod
    def logout():
        print("这是退出...")


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

    t.login()  # 这是错误的调用 MyThread 类 的 login() 的方法

多线程-共享全局变量

  • 多个线程之间,是共享全局变量的

  • 全局变量与局部变量的区别:一个写在函数里面,一个写在函数外面

  • 如果做了一个多任务多线程的程序,那么子线程的函数,子线程与子线程之间,共享全局变量。

  • 附:在一个函数中,对全局变量进行修改的时候,到底是否需要使用 global 进行说明。要看是否对全局变量的指向进行了修改,如果修改了指向,即让全局变量指向了一个新的内存地址,那么必须使用 global。如果仅仅是修改了指向的空间中的数据,此时不用使用 global

import threading
import time


# 定义一个全局变量
g_num = 100


def test1():
    global g_num
    g_num += 1
    print("-----in test1 g_num = %d" % g_num)


def test2():
    print("-----in test2 g_num = %d" % g_num)


def main():
    t1 = threading.Thread(target=test1)
    t2 = threading.Thread(target=test2)

    t1.start()
    time.sleep(1)

    t2.start()
    time.sleep(1)

    print("-----in main Thread g_num = %d-----" % g_num)


if __name__ == "__main__":
    main()


多线程-共享全局变量-args

代码示例如下

import threading
import time


# 定义一个全局变量
g_nums = [11, 22]


def test1(temp):
    temp.append(33)
    print("-----in test1 temp = %s -----" % str(temp))


def test2(temp):
    print("-----in test2 temp = %s -----" % str(temp))


def main():
    # target 指定将来 这个线程去哪个函数执行代码
    # args   指定将来调用 函数的时候 传递什么数据过去
    t1 = threading.Thread(target=test1, args=(g_nums,))
    t2 = threading.Thread(target=test2, args=(g_nums,))

    t1.start()
    time.sleep(1)

    t2.start()
    time.sleep(1)

    print("-----in main Thread g_num = %s-----" % g_nums)


if __name__ == "__main__":
    main()

总结:

  • 在一个进程内的所有线程共享全局变量,很方便在多个线程间共享数据
  • 缺点就是,线程是对全局变量随意修改可能造成多线程之间对全局变量的混乱(即线程非安全)

多线程-共享全局变量问题

  • 共享全局变量有的时候会产生资源竞争,资源竞争就是多个线程都用同一个变量

多线程开发可能遇到的问题

假设两个线程t1t2都要对全局变量g_num(默认是0)进行加1运算,t1t2都各对g_num加100次,g_num的最终结果应该为200.

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

  • 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又把之前得到的01赋值给g_num
  • 4、这样就导致虽然t1t2都对g_num加1,但结果仍然是g_num = 1

代码示例如下

import threading
import time


# 定义一个全局变量
g_num = 0


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


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


def main():

	# t1 = threading.Thread(target=test1, args=(100,))
    # t2 = threading.Thread(target=test2, args=(100,))

    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))

    t1.start()
    t2.start()

    # 等待上面的2个线程执行完毕
    time.sleep(5)

    print("-----in main Thread g_num = %d-----" % g_num)


if __name__ == "__main__":
    main()


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

同步概念、互斥锁及资源竞争的解决

同步的概念

  • 同步就是协同步调,按预定的先后次序进行运行。如:你说完了,我再说。
  • "同"字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合
  • 如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时需要依靠B的某个结果,于是停下来,示意B执行;B执行,再将结果给A;A再继续执行。

解决线程同时修改全局变量的方式

对于前文提出的那个“多线程开发可能遇到的问题”,可以通过线程同步来进行解决

思路如下:

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

互斥锁

  • 当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
  • 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁
  • 互斥锁为资源引入一个状态:锁定/非锁定
  • 某个线程要更改共享数据时,先将其锁定,此时资源的状态为**“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”**,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

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

使用方法:


# 创建锁
mutex = threading.Lock()

# 锁定
mutex = acquire()

# 释放
mutex = release()


注意:

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

还用互斥锁解决2个线程对同一个全局变量各加100万次的操作

import threading
import time


# 定义一个全局变量
g_num = 0

# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()


def test1(num):
    global g_num

    # 上锁,如果之前没有被上锁,那么此时 上锁成功
    # 如果上锁之前 已经被上锁了,那么此时会堵塞在这里,直到这个锁被解开为止
    mutex.acquire()

    for i in range(num):
        g_num += 1

    # 解锁
    mutex.release()

    print("-----in test1 g_num = %d" % g_num)


def test2(num):
    global g_num
    mutex.acquire()

    for i in range(num):
        g_num += 1

    # 解锁
    mutex.release()

    print("-----in test2 g_num = %d" % g_num)


def main():

    t1 = threading.Thread(target=test1, args=(1000000,))
    t2 = threading.Thread(target=test2, args=(1000000,))

    t1.start()
    t2.start()

    # 等待上面的2个线程执行完毕
    time.sleep(2)

    print("-----in main Thread g_num = %d-----" % g_num)


if __name__ == "__main__":
    main()

上锁解锁的过程

  • 当一个线程调用锁的**acquire()**方法获得锁时,锁就进入了 **“locked”**状态。
  • 每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为**“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()的方法释放锁之后,锁进入unclocked**状态。
  • 线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入running状态。

总结

锁的好处:

  • 确保了某段代码只能由一个线程从头到尾完整的执行

锁的坏处:

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

死锁、银行家算法

死锁

  • 在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。
  • 尽管死锁很少发生,但一旦发生就会造成应用的停止响应。

下面看一个死锁的例子


"""
以下代码仅作为了解了解‘死锁’即可
"""
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)

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

        # 对mutexA解锁
        mutexA.release()


class MyThread2(threading.Thread):

    def run(self):
        # 对mutexB上锁
        mutexB.acquire()

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

        # 此时后堵塞,因为这个mutextB已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+"-----do1-----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个资金单位。某一时刻的状态如下所示

test1

对于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均不能完成工作,而银行家手中又没有钱了,系统陷入僵持局面,银行家也不能收回投资。

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

案例:多线程版本的UDP聊天器

案例仅做了解,不要浪费太多时间精力

案例代码示例如下

import socket
import threading


def recv_msg(udp_socket):
    """接收数据并显示"""

    # 接收数据
    while True:
        recv_data = udp_socket.recvfrom(1024)
        print(recv_data)


def send_msg(udp_socket, dest_ip, dest_port):
    """发送数据"""

    # 发送数据
    while True:
        send_data = input("请输入要发送的数据:")
        udp_socket.sendto(send_data.encode("utf-8"), (dest_ip, dest_port))


def main():
    """完成UDP聊天器的整体控制"""

    # 1.创建套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    # 2.绑定本地信息(端口)
    udp_socket.bind("", 12707)

    # 3.获取对方的IP
    dest_ip = input("请输入对方的IP:")
    dest_port = int(input("请输入对方的Port:"))

    # 4.创建2个线程,去执行响应的功能
    t_recv = threading.Thread(target=recv_msg, args=(udp_socket, ))
    t_send = threading.Thread(target=send_msg, args=(udp_socket, dest_ip, dest_port))

    t_recv.start()
    t_send.start()


if __name__ == "__main__":
    main()

猜你喜欢

转载自blog.csdn.net/weixin_42250835/article/details/89151304