Python 多线程基础

官方参考文档

https://docs.python.org/zh-cn/3.7/library/threading.html#module-threading

Thread 直接创建子线程

import threading
import time


def work(internal):
    name = threading.current_thread().name
    print(f"{name} start")
    time.sleep(internal)
    print(f"{name} end")


print("Main: ", threading.current_thread().name)
for i in range(5):
    thread_instance = threading.Thread(target=work, args=(i, ))
    thread_instance.start()

print("Main: end")

这里一共产生了三个线程,分别是主线程MainThread和两个子线程Thread-1、Thread-2。另外我们观察到,主线程首先运行结束,
Thread-1、Thread-2 才接连运行结束,分别间隔了 1 秒和 4 秒。这说明主线程并没有等待子线程运行完毕才结束运行,而是直接退出了,有点不符合常理。

规定主线程在子线程后退出

import threading
import time


def work(internal):
    name = threading.current_thread().name
    print(f"{name} start")
    time.sleep(internal)
    print(f"{name} end")


print("Main: ", threading.current_thread().name)
for i in range(5):
    thread_instance = threading.Thread(target=work, args=(i, ))
    thread_instance.start()
    # 规定主线程在子线程后退出 
    thread_instance.join()

print("Main: end")

有关于 join

如果我们测试上一步的运行时间,可以发现不管是单独运行,还是多线程运行,join 的运行时间均是 10s 左右。
(10 = 1+2+3+4) 似乎失去了多线程运行的意义,其实则是没有正确使用 join 的结果。

那么, join 真正的含义是什么呢?
join 会卡住主线程,并让当前已经 start 的子线程继续运行,直到调用.join的这个线程运行完毕。
所以,我们只需要 join 时间最长的一个线程即可。

import threading
import time


now = lambda :time.time()


def work(internal):
    name = threading.current_thread().name
    print(f"{name} start")
    time.sleep(internal)
    print(f"{name} end")


t1 = now()
print("Main: ", threading.current_thread().name)
for i in range(5):
    thread_instance = threading.Thread(target=work, args=(i, ))
    thread_instance.start()
    # 可规定主线程在子线程后退出
    if i == 4:
        thread_instance.join()

print(f"Main: end, Time: {now() - t1}")

当然,这是在我们知道哪个线程先运行完,哪个线程后面运行完的情况下。
在我们不知道哪个线程先运行完成的情况下,在以后之后,需要对每一个进行 join。

我们设想这样一个场景。你的爬虫使用10个线程爬取100个 URL,主线程需要等到所有URL 都已经爬取完成以后,再来分析数据。此时就可以通过 join 先把主线程卡住,
等到10个子线程全部运行结束了,再用主线程进行后面的操作。
如果我不知道哪个线程先运行完,那个线程后运行完怎么办?这个时候就要每个线程都执行 join 操作了。
这种情况下,每个线程使用 join是合理的:

thread_list = []
for _ in range(10):
    thread = threading.Thread(target=xxx, args=(xxx, xxx)) 换行thread.start()
    thread_list.append(thread)

for thread in thread_list:
    thread.join()

通过继承的方式创建多线程

import threading
import time


class MyThread(threading.Thread):
    def __init__(self, interval):
        super(MyThread, self).__init__()
        self.interval = interval

    def run(self):
        name = threading.current_thread().name
        print(f"{name} start")
        time.sleep(self.interval)
        print(f"{name} end")


print("Main: ", threading.current_thread().name)
for i in range(5):
    thread_instance = MyThread(i)
    thread_instance.start()
    # 可规定主线程在子线程后退出
    # 可规定主线程在子线程后退出
    if i == 4:
        thread_instance.join() 
print("Main: end")

两种实现方式的效果是相同的。

守护线程

在线程中有一个叫作守护线程的概念,如果一个线程被设置为守护线程,那么意味着这个线程是“不重要”的,这意味着,如果主线程结束了而该守护线程还没有运行完,
那么它将会被强制结束。在 Python 中我们可以通过 setDaemon 方法来将某个线程设置为守护线程。

import threading
import time


now = lambda:time.time()


def work(internal):
    name = threading.current_thread().name
    print(f"{name} start")
    time.sleep(internal)
    print(f"{name} end")


thread_1 = threading.Thread(target=work, args=(1, ))
thread_2 = threading.Thread(target=work, args=(5, ))
thread_2.setDaemon(True)
thread_1.start()
thread_2.start()

print("Main End.")

互斥锁

在一个进程中的多个线程是共享资源的,比如在一个进程中,有一个全局变量 count 用来计数,现在我们声明多个线程,每个线程运行时都给 count 加 1,
让我们来看看效果如何,代码实现如下:

import threading
import time

count = 0


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

    def run(self):
        global count
        temp = count + 1
        time.sleep(0.001)
        count = temp


def main():
    threads = []
    for _ in range(1000):
        thread_ = MyThread()
        thread_.start()
        threads.append(thread_)

    for t in threads:
        t.join()

    print("Final count: ", count)


main()

那这样,按照常理来说,最终的 count 值应该为 1000。但其实不然,我们来运行一下看看。
运行结果如下:
Final count: 69

这是为什么呢?因为count这个值是共享的,每个线程都可以在执行temp=count这行代码时拿到当前count的值,但是这些线程中的一些线程可能是并发或者并行执行的,
这就导致不同的线程拿到的可能是同一个 count 值,最后导致有些线程的 count 的加 1 操作并没有生效,导致最后的结果偏小。

所以,如果多个线程同时对某个数据进行读取或修改,就会出现不可预料的结果。为了避免这种情况,我们需要对多个线程进行同步,要实现同步,
我们可以对需要操作的数据进行加锁保护,这里就需要用到threading.Lock 了。

加锁保护是什么意思呢?就是说,某个线程在对数据进行操作前,需要先加锁,这样其他的线程发现被加锁了之后,就无法继续向下执行,会一直等待锁被释放,
只有加锁的线程把锁释放了,其他的线程才能继续加锁并对数据做修改,修改完了再释放锁。这样可以确保同一时间只有一个线程操作数据,多个线程不会再同时读取和修改同一个数据,
这样最后的运行结果就是对的了。

import threading
import time

count = 0
lock = threading.Lock()


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

    def run(self):
        global count
        # 获取锁
        lock.acquire()
        temp = count + 1
        time.sleep(0.001)
        count = temp
        # 释放锁
        lock.release()


def main():
    threads = []
    for _ in range(1000):
        thread_ = MyThread()
        thread_.start()
        threads.append(thread_)

    for t in threads:
        t.join()

    print("Final count: ", count)


main()

关于 Python 中的多线程

由于Python中GIL的限制,导致不论是在单核还是多核条件下,在同一时刻只能运行一个线程,导致Python多线程无法发挥多核并行的优势。
GIL全称为GlobalInterpreterLock,中文翻译为全局解释器锁,其最初设计是出于数据安全而考虑的。在Python多线程下,每个线程的执行方式如下:

  • 获取 GIL
  • 执行对应线程的代码
  • 释放 GIL
    可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是通行证,并且在一个Python进程中,GIL只有一个。拿不到通行证的线程,就不允许执行。
    这样就会导致,即使是多核条件下,一个 Python 进程下的多个线程,同一时刻也只能执行一个线程。

不过对于爬虫这种 IO 密集型任务来说,这个问题影响并不大。而对于计算密集型任务来说,由于 GIL 的存在,多线程总体的运行效率相比可能反而比单线程更低。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值