最新消息:欢迎光临 魔力 • Python!大家可以点开导航菜单中的【学习目录】,这个目录类似图书目录,更加方便学习!

Python3萌新入门笔记(48)

Python教程 小楼一夜听春语 4115浏览 0评论

这一篇教程,我们一起来了解多线程。

我们通过进程的学习,能够知道一个进程是一个任务。

那么,线程又是什么?

线程的作用执行进程的任务。

实际上,每一个进程的启动,都至少包含了一个线程。

这就好像一家食品公司(进程),至少得有一条生产线(线程),食品公司(进程)要生产食品(任务),但是实际上进行生产(执行任务)的是这家公司的生产线(线程)。

我们可以通过代码,验证一下这个概念。

示例代码:

import multiprocessing, threading

print(multiprocessing.current_process().name)  # 显示输出结果为:MainProcess
print(threading.current_thread().name)  # 显示输出结果为:MainThread

通过运行上方代码,我们能够看到主进程和主线程的名称。

也就意味着,程序执行的时候,启动了主进程,主进程启动了主线程。

接下来是多线程。

还是以食品公司举例。

速冻食品公司不但能生产饺子,还能够生产馄饨。

假如这时来了多个订单(任务),有些订单是饺子,有些订单是馄饨。

虽然一条生产线能够生产这两种食品,但是必须先生产完其中一种,再生产另外一种。

那么,当有多个订单要同时进行的时候,怎么解决呢?

一种方法,我们可以把公司(主进程)变成集团,在集团下开设多个分公司(子进程),每个公司建立一条生产线(主线程)。

另一种方法,我们可以把食品公司(主进程)的生产线升级生产组(主线程),在生产组内建立多条生产线(子线程)。

还有一种方法,我们可以把公司(主进程)变成集团,在集团下开设多个分公司(子进程),每个公司设立生产组(主线程),每个生产组建立多条生产线(子线程)。

简单来说,在程序中多任务的实现有3种方式:

  • 多进程单线程
  • 单进程多线程
  • 多进程多线程

接下来,我们通过代码看一下如何通过多线程执行多任务。

示例代码:

import time, threading  # 导入需要使用的模块

def task01(name):
    print(threading.current_thread().name)  # 显示输出结果为:Thread-1
    print(name, 1)
    time.sleep(0.001)
    print(name, 2)
    time.sleep(0.001)
    print(name, 3)

def task02(name):
    print(threading.current_thread().name)  # 显示输出结果为:Thread-1
    print(name, 1)
    time.sleep(0.001)
    print(name, 2)
    time.sleep(0.001)
    print(name, 3)

if __name__ == '__main__':
    print(threading.current_thread().name)  # 显示输出结果为:MainProcess
    thread1 = threading.Thread(target=task01, args=('函数01:',))  # 创建子线程运行函数task01
    thread2 = threading.Thread(target=task02, args=('函数02:',))  # 创建子线程运行函数task02
    thread1.start()  # 启动子进程
    thread2.start()  # 启动子进程

运行上方代码,显示输出结果类似:

MainThread
Thread-1
函数01: 1
Thread-2
函数01: 2
函数02: 1
函数01: 3
函数02: 2
函数02: 3

这里,大家能够看出,这段代码和之前我们通过多进程实现多任务的代码非常相像,只是把进程模块换为了线程模块,创建进程变成了创建线程。

不过,多进程和多线程是有区别的。

多进程中对于同一个变量,各有一份副本在进程中,所以对同一个变量的操作互不影响。

多线程中对于同一个变量,是各个线程共享的,所以对同一个变量,每个线程都能够进行操作。

我们通过代码来尝试一下。

示例代码:(多进程)

import multiprocessing

count = 0
def task01():
    print(multiprocessing.current_process().name)  # 显示输出结果为:Process-1
    global count
    count += 1
    print(count)  # 显示输出结果为:1

def task02():
    print(multiprocessing.current_process().name)  # 显示输出结果为:Process-2
    global count
    count += 1
    print(count)  # 显示输出结果为:1

if __name__ == '__main__':
    process1 = multiprocessing.Process(target=task01)
    process2 = multiprocessing.Process(target=task02)
    process1.start()
    process2.start()

示例代码:(多线程)

import threading

count = 0
def task01():
    print(threading.current_thread().name)  # 显示输出结果为:Thread-1
    global  count
    count+=1
    print(count)  # 显示输出结果为:1

def task02():
    print(threading.current_thread().name)  # 显示输出结果为:Thread-2
    global  count
    count+=1
    print(count)  # 显示输出结果为:2

if __name__ == '__main__':
    thread1 = threading.Thread(target=task01)
    thread2 = threading.Thread(target=task02)
    thread1.start()
    thread2.start()

通过段代码显示输出结果的对比,就能够看出不同。

而且,因为这个原因,使用多线程对同一个变量进行操作时,还容易出现意料之外的错误。

例如下面这段代码。

示例代码:

import threading

count = 0
def task01(proc):
    global count
    for i in range(500000):
        count += 1
        count -= 1

def task02(proc):
    global count
    for i in range(500000):
        count += 1
        count -= 1

if __name__=='__main__':
    thread1=threading.Thread(target=task01,args=('线程1',))
    thread2=threading.Thread(target=task02,args=('线程2',))
    thread1.start()
    thread2.start()
    thread1.join()
    thread2.join()

print(count)

按代码里面的内容推断,代码运行结束时,全局变量count的值应该为0。

但是,实际上你会发现,运行之后结果不是固定的。

这是就是因为两个线程同时操作一个变量所导致的问题。

其中一种情况就是,thread1在执行了加法语句之后,尚未执行减法语句时,thread2的加法语句被执行了。

那后面不还是会执行两个减法语句吗?

看似是这个道理,但是实际上“count += 1”这个语句,在给变量count赋值的时候,并不是直接把计算结果写入变量count,而是先把“count+1”的计算结果写入到一个临时的局部变量,再把这个临时局部变量的值写入全局变量count。

过程是这样的(count初始值为0):

  1. thread1:temp1 = count + 1  # 此时temp1为1,count为0。
  2. thread2:temp2 = count + 1  # 此时temp2为1,count为0
  3. thread1:count = temp1  # 此时count为1。
  4. thread2:count = temp2  # 此时count为1。
  5. thread1:temp1 = count – 1  # 此时temp1为0,count为1。
  6. thread1:count = temp1  # 此时count为0。
  7. thread2:temp2 = count – 1  # 此时temp2为-1,count=0。
  8. thread2:count = temp2  # 此时count为-1。

这样的错误,在代码运行足够的次数后就会发生。

那么,如何避免这种错误呢?

我们通过Lock类来解决。

Lock类是实现原始锁对象的类。一旦一个线程获得了一个锁,就会执行锁之后的语句块,直到锁被释放;锁的释放可以在任何线程中执行。

示例代码:(以task01函数为例)

lock = threading.Lock() # 创建锁
def task01(proc):
    global count
    for i in range(500000):
        lock.acquire()  # 加锁
        count += 1
        count -= 1
        lock.release()  # 解锁

在上方代码中,创建一个锁的对象,并且为函数task01和task02都添加acquire()获得锁和release()释放锁的语句,就可以避免之前的错误出现。

加锁固然能够让某一段代码在执行完毕前不被其它线程所干扰,但是这样也导致其它线程不能并发执行,从而降低了代码的效率。

就拿上方代码来说,大家应该可以感受到加锁后代码的运行速度比加锁前慢了许多。

如果感受不明显,可以把循环次数再增加10倍试一试。

最后,大家还要知道,在Python解释器执行代码时,也有一个锁叫Global Interpreter Lock(简称GIL:全局解释器锁),这个锁导致任何Python线程执行之前,必须先开启GIL锁,每执行一定数量的代码后,再自动关闭GIL锁,让其它线程能够执行。这个全局解释器锁的机制导致Python中的线程不是真正的并发,而是轮流执行,所以,再多的线程也是一个CPU核心在执行,无法使用多个CPU核心。这也就意味着多核CPU执行多线程的Python代码时,无法发挥多核的性能。如果想有效利用CPU的多个核心,可以在Python的代码中使用多进程。多进程虽然也有多个线程,但是各个进程的GIL锁是独立的,不会互相影响。

本节知识点:

1、线程的概念;

2、多线程的使用;

3、线程的锁。

本节英文单词与中文释义:

1、thread:线程

2、lock:锁

3、acquire:获得

4、release:释放

5、temp(temporary):临时

6、interpreter:解释器

转载请注明:魔力Python » Python3萌新入门笔记(48)

头像
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网站 (可选)

网友最新评论 (2)

  1. 头像
    虽然看过很多线程什么的,但始终没在实际应用中运用。跟不学没差太多。
    走路爱走神7年前 (2018-05-30)回复
  2. 头像
    我想知道time.sleep()函数执行后,当前线程有没有放弃对CPU的占用?如果sleep()的参数写为0秒又会发生什么?搜了下各种博客都是在说Java里面Thread.sleep()的情况,找不到Python的解释。
    delphing5年前 (2019-10-12)回复