python多任务,线程详解

python 多任务

多线程

python的thread模块是⽐较底层的模块,python的threading
模块是对thread做了⼀些包装的,可以更加⽅便的被使⽤

调用

1 直接调用

# –*– coding: utf-8 –*–
# @Time      : 2019/1/7 22:21
# @Author    : Damon_duanlei
# @FileName  : thread_test01.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
import time


def hello_bye(name):
    print("hello! {}".format(name))
    time.sleep(1)
    print("bye! {}".format(name))


if __name__ == '__main__':
    t1 = threading.Thread(target=hello_bye, args=("臭臭",))
    t2 = threading.Thread(target=hello_bye, args=("小迪",))
    t1.start()
    t2.start()

运行结果:

>>>
hello! 臭臭
hello! 小迪
bye! 臭臭
bye! 小迪

2 继承 Threading.Thread

# –*– coding: utf-8 –*–
# @Time      : 2019/1/7 22:29
# @Author    : Damon_duanlei
# @FileName  : thread_test02.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei

import threading
import time


def hello_bye(name):
    print("hello! {}".format(name))
    time.sleep(1)
    print("bye! {}".format(name))


class MyThread(threading.Thread):
    def __init__(self, func, name):
        threading.Thread.__init__(self)
        self.func = func
        self.name = name

    def run(self):
        self.func(self.name)


if __name__ == '__main__':
    t1 = MyThread(hello_bye, "臭臭")
    t2 = MyThread(hello_bye, "小迪")
    t1.start()
    t2.start()

运行结果:

>>>
hello! 臭臭
hello! 小迪
bye! 臭臭
bye! 小迪

区别

使用继承的方式重写run()方法,start()时不走父类的run()方法,而是走子类重写的run()方法.使用threading.Thread直接创建,start()的时候走Thread类下的run()方法,该run()方法会主动调用 target 函数. 所以使用继承方式调用多线程需要将业务逻辑写入重写后的run()方法.

线程的执行顺序

# –*– coding: utf-8 –*–
# @Time      : 2019/1/13 11:07
# @Author    : Damon_duanlei
# @FileName  : thread_runing_order.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei

import threading
import time


class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm " + self.name + ' @ ' + str(i)
            print(msg)


def test():
    for i in range(5):
        t = MyThread()
        t.start()


if __name__ == '__main__':
    test()

运行结果:( 运行的结果可能不一样, 但是大体一致)

>>>
I'm Thread-1 @ 0
I'm Thread-5 @ 0
I'm Thread-3 @ 0
I'm Thread-2 @ 0
I'm Thread-4 @ 0
I'm Thread-1 @ 1
I'm Thread-5 @ 1
I'm Thread-4 @ 1
I'm Thread-3 @ 1
I'm Thread-2 @ 1
I'm Thread-1 @ 2
I'm Thread-4 @ 2
I'm Thread-5 @ 2
I'm Thread-3 @ 2
I'm Thread-2 @ 2

结论:

多线程程序的执行顺序是不确定的. 当执行到 sleep 语句时, 线程将被阻塞( Blocked ), 到 sleep 结束后, 线程进入就绪状态( Runable ), 等待调度. 而线程调度间自行选择一个线程执行. 上面代码中能保证每个线程都运行完整个 run 函数, 但是线程的启动顺序及 run 函数中每次循环的执行顺序都不能确定.

阻塞

python 中主线程会等待所有的子线程都执行结束后才结束, 有时实际开发需要子线程开启后, 主线程等待子线程执行结束后再继续向下执行,可以使用 线程.join()方法进行阻塞.(以下示例代码均使用直接调用线程的方法)

# –*– coding: utf-8 –*–
# @Time      : 2019/1/7 22:21
# @Author    : Damon_duanlei
# @FileName  : thread_test01.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
import time


def hello_bye(name, thread_name):
    print("线程{}启动...".format(thread_name))
    print("hello! {}".format(name))
    time.sleep(3)
    print("bye! {}".format(name))
    time.sleep(3)
    print("线程{}结束...".format(thread_name))


if __name__ == '__main__':
    t1 = threading.Thread(target=hello_bye, args=("臭臭", "t1"))
    t2 = threading.Thread(target=hello_bye, args=("小迪", "t2"))
    print("程序开始执行")
    t1.start()
    # t1.join()
    print("主线程继续向下执行")
    # t1.join()
    t2.start()
    t1.join()
    t2.join()
    print("主线程执行结束")

小伙伴可以尝试 t1.join()三处位置不同执行的结果有什么差异.

结论:

线程名.join()添加在何处, 主线程就阻塞在何处等待该子线程执行结束后方解阻塞. 根据业务需求可以将线程的开启和添加阻塞写为类似以下代码结构:

thread_list = []
    t1 = threading.Thread(target=hello_bye, args=("臭臭", "t1"))
    thread_list.append(t1)
    t2 = threading.Thread(target=hello_bye, args=("小迪", "t2"))
    thread_list.append(t2)
    t3 = threading.Thread(target=hello_bye, args=("笨笨", "t3"))
    thread_list.append(t3)
    t4 = threading.Thread(target=hello_bye, args=("白克", "t4"))
    thread_list.append(t4)
    for t in thread_list:
        t.start()
    
    for t in thread_list:
        t.join()    

多线程共享全局变量

# –*– coding: utf-8 –*–
# @Time      : 2019/1/13 11:28
# @Author    : Damon_duanlei
# @FileName  : thread_globle_var.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
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)


if __name__ == '__main__':
    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---

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

多线程共享全局变量问题

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

假设两个线程 t1 和 t2 都要对全局变量进行加1运算(g_num = 0), t1 和 t2 都各自对同一个全局变量加10次, g_num 的结果应该为20. 但是由于多线程同时操作, 很有可能出现下面的情况:

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

测试

# –*– coding: utf-8 –*–
# @Time      : 2019/1/13 14:13
# @Author    : Damon_duanlei
# @FileName  : test_01.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
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 {}---".format(g_num))


print("---线程创建之前g_num is {}---".format(g_num))
t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()
t1.join()
t2.join()
print("2个线程对同⼀个全局变量操作之后的最终结果是:{}".format(g_num))

运行结果:

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

结论:

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

同步的概念

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

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

思路如下:

  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 进行修改时, 都要先上锁, 处理完后再解锁, 在上锁的整个过程中不允许其他线程访问, 这就保证了数据的正确性.

互斥锁

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

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

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

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

# 创建锁
lock = threading.Lock()
# 锁定
lock.acquire()
# 释放锁
lock.release()

注意:

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

对上文测试代码引入互斥锁后代码及运行结果:

# –*– coding: utf-8 –*–
# @Time      : 2019/1/13 14:47
# @Author    : Damon_duanlei
# @FileName  : thread_lock.py
# @BlogsAddr : https://blog.csdn.net/Damon_duanlei
import threading
g_num = 0


def test1(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁
    print("---test1---g_num={}".format(g_num))


def test2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上锁
        g_num += 1
        mutex.release()  # 解锁
    print("---test2---g_num={}".format(g_num))


# 创建⼀个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()
# 创建2个线程,让他们各⾃对g_num加1000000次
t1 = threading.Thread(target=test1, args=(1000000,))
t1.start()
t2 = threading.Thread(target=test2, args=(1000000,))
t2.start()
# 等待计算完成
t1.join()
t2.join()
print("2个线程对同⼀个全局变量操作之后的最终结果是:{}".format(g_num))

运行结果:

>>>
---test2---g_num=1997389
---test1---g_num=2000000
2个线程对同⼀个全局变量操作之后的最终结果是:2000000

可以看到最后的结果, 加入互斥锁后, 其结果与预期相符.

锁的好处:

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

锁的坏处:

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

避免死锁:

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

GIL全局解释器锁

GIL是什么?为什么会有GIL? 网上有茫茫多的答案, 感兴趣的小伙伴请自行了解, 总之,因为 GIL 的存在Cpython的解释器同时只有一个线程运行. 因此python的多线程在面对计算密集型( CPU密集型 ) 相比单线程没有效率优势,甚至在python2.7之前面对计算密集型多线程效率远远低于单线程效率. 但是Cpython的多线程并非鸡肋, 在IO密集型程序中,因多次进行IO操作反复对线程进行阻塞,在等待阻塞的时间系统可以将线程调用在非阻塞的地方.所以在IO密集型程序中python多线程效率远远大于单线程效率.

线程池

待续 …

你可能感兴趣的:(自学总结,多任务,多线程,GIL,互斥锁)