Python——多线程以及多线程会产生的一些问题

Python 中的多线程

什么是线程

​ 一个进程中包括多个线程,线程是 CPU 调度和分派的基本单位,是进程中执行运算的最小单位,真正在 CPU 上运行的是线程,可以与同一个进程中的其他线程共享进程的全部资源

Python 中实现多线程

​ Python 中有两种方式床架多线程,一种是调用底层的 thread模块(Python3 中已弃用),另一种是使用threading模块,下面我说的也是使用这个模块实现多线程的方法

​ 从形式上将,多线程的实现和多进程的实现十分类似,threading模块提供了Thread类来创建线程,同Process类一样,我们可以通过直接调用或创建子类来继承这两种方式来创建线程

使用 Thread 实现多线程

​ 直接调用threading,模块的Thread类来创建线程十分简单,使用threading.Thread([target], [(item1, item2, ...)])方法,target为目标函数名称(如果有目标函数的话),后边为参数元组,用来传递函数参数

​ 线程创建好后,调用Thread.start()方法,就可以运行线程,如果没有目标函数,start()会自动执行Thread类中的run()方法,示例如下:

import threading
import time


def saySorry():
    time.sleep(5)
    print('I am sorry')


if __name__ == '__main__':
    for i in range(5):
        print('create %i Thread' % i)
        t = threading.Thread(target=saySorry)
        t.start()

​ 运行结果:

create 0 Thread
create 1 Thread
create 2 Thread
create 3 Thread
create 4 Thread
I am sorry
I am sorry
I am sorry
I am sorry
I am sorry

​ 上边的程序也证明了,程序在创建了子线程后,不会等待线程执行完毕,而是会继续向下执行,而当主程序全部执行完毕后,却会等待所有子线程执行完毕再结束,这点和进程有些区别

继承 Thread 实现多线程

​ 另外一种方式是通过创建继承Thread类的子类来实现多线程,这样做的好处就是可以将线程要运行的代码全部放入run函数中,用起来更方便,示例如下:

import threading
import time


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


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

​ 运行结果:

I am Thread-1 @ 0
I am Thread-1 @ 1
I am Thread-1 @ 2

Python 多线程中全局变量和非全局变量的使用问题

​ 开始就说过,线程可以和同一个进程中的其他线程共享进程的全部资源,那么在多线程程序中,各线程对全局变量和非全局变量的使用到底是怎样的呢

非全局变量

​ 非全局变量在多线程中是不会被共享的,这就像是假设有一个存在局部变量a的函数,当程序调用两次这个函数时,每次调用所产生的局部变量a都是一个新的变量,不会受另一个函数执行的干扰,示例如下:

from threading import Thread
import threading


def test1():
    str = threading.current_thread().name
    g_num = 100
    if str == 'Thread-1':
        print(str)
        g_num += 1
    else:
        print(str)
        g_num -= 1
    print('-----test1----- g_num = %d' % g_num)


p1 = Thread(target=test1)
p1.start()

p2 = Thread(target=test1)
p2.start()

​ 运行结果:

Thread-1
-----test1----- g_num = 101
Thread-2
-----test1----- g_num = 99

​ 可以看到,局部变量g_num并不会因为另一个线程中的同名函数而收到影响

全局变量

​ 在多线程中,全局变量是可以在各线程间共享的,这也就是说,线程间通信不需要通过管道、内存映射等方法,只需要使用一个全局变量(同一个进程中的共享资源)便可以,示例如下:

from threading import Thread

g_flag = 0
g_num = 0


def test1():
    global g_num
    for i in range(1000000):
        g_num += 1

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


def test2():
    global g_num
    for i in range(1000000):
        g_num += 1

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


p1 = Thread(target=test1)
p1.start()


p2 = Thread(target=test2)
p2.start()

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

​ 运行结果:

---g_num= 372639 ---
---test1--- g_num = 1456098
---test2--- g_num = 1596586 

​ 到这里,我们可以发现一个问题,正常来讲,g_num最后的值不应给是2000000吗,为什么不是呢?这就是多线程使用全局变量时有可能出现的 bug,请继续阅读!

Python 多线程中如何防止使用全局变量出现 bug(轮询和互斥锁)

​ 通过刚才在多线程中使用全局变量我们发现,当代码逻辑稍微复杂一些时,在两个线程中同时使用一个全局变量会出现问题,是什么导致了这个问题呢?

​ 从代码中我们可以发现g_num += 1这句代码,实际上是g_num + 1和将其结果赋给g_num两步,正是因为这连续的两次对全局变量的操作造成了这个问题

​ 当一个线程执行到g_num + 1这步后,cpu 有可能会转头去处理另一个线程,另一个线程也运行了g_num + 1,当 cpu 再回头执行第一个线程时,g_num 已经不止被运算过一次了

​ 那么怎么避免这样的情况发生呢,只能是如果存在对全局变量变量值的修改时,我们要优先运行一个线程,当它结束修改后,再允许另一个线程去访问这个局部变量,下面提供两种方式,轮询和互斥锁

轮询

​ 顾名思义,轮询的意思就是反复询问,抽象起来理解就是,我们可以设置另一个用来作为目标值的全局变量,两个线程执行的条件根据目标值的不同而不同,当目标值满足一个线程执行时,其他线程就会一直处在一个堵塞的过程,它会一直询问目标值是否符合自己,当上一个线程结束时,这个线程会将目标值修改,这样下一个符合目标值的线程就会运行,示例如下:

from threading import Thread

g_flag = 0
g_num = 0


def test1():
    global g_num
    global g_flag
    if g_flag == 0:
        for i in range(1000000):
            g_num += 1
        g_flag = 1

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


def test2():
    global g_num
    global g_flag
    while True:
        if g_flag != 0:
            for i in range(1000000):
                g_num += 1
            break

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


p1 = Thread(target=test1)
p1.start()


p2 = Thread(target=test2)
p2.start()

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

​ 运行结果:

---g_num= 181893 ---
---test1--- g_num = 1000000
---test2--- g_num = 2000000 

​ 如结果所示,线程之间使用全局变量的 bug 已经解决,但是轮询的方法十分消耗资源,堵塞的线程其实一直都处在一个死循环的状态占用系统资源

互斥锁

​ 相比而言,互斥锁就是一种比较优化的方法,互斥锁会使用threading模块的Lock

​ 互斥锁的思想是,当一个线程运行时,它会给它需要的这部分资源上锁,这样同样使用这把锁的其他线程全部都会被堵塞,但被互斥锁所堵塞的线程不会占用系统资源,它们会处在睡眠状态,当运行的线程用完被锁的这部分资源后,它会解锁,这时其他线程就会被唤醒来抢占 cpu 资源,得到资源的线程会再次上锁,达到多线程下全局变量的访问安全,示例如下:

from threading import Thread
from threading import Lock

g_num = 0
mutex = Lock()


def test1():
    global g_num
    mutex.acquire()
    for i in range(1000000):
        g_num += 1
    mutex.release()

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


def test2():
    global g_num
    mutex.acquire()
    for i in range(1000000):
        g_num += 1
    mutex.release()

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


p1 = Thread(target=test1)
p1.start()

p2 = Thread(target=test2)
p2.start()

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

​ 运行结果:

---g_num= 200009 ---
---test1--- g_num = 1000000
---test2--- g_num = 2000000 

​ 互斥锁的方法说明:

# 创建锁
mutex = threading.Lock()
# 上锁,blocking 为 True 表示堵塞
mutex.acquire([blocking])
# 解锁,只要开了锁,那么接下来会让所有因为这个锁而被阻塞的线程抢着上锁
mutex.release()

​ 使用互斥锁时要注意,为了提高运算效率,上锁的资源越少,运算的效率越高

​ 另外,线程等待解锁的方式不是通过轮询,二十通过通知,线程会睡眠,等待唤醒的通知,所以互斥锁较轮询来讲更为优化

发布了38 篇原创文章 · 获赞 44 · 访问量 3407

猜你喜欢

转载自blog.csdn.net/scfor333/article/details/104176403
今日推荐