Python多线程

关于Python多线程的概述

由于GIL的存在,Python的多线程在CPU密集型任务并没有多大的优势,任何Python线程执行之前必须先获取GIL锁,然后每执行100条字节码解释器会释放锁,让其他线程有机会执行,阻塞时期的其他线程没有太大机会去占用其他CPU的资源,而是会阻塞等待当前CPU的线程执行完,对于多核并没有什么卵用。但是,对于I/O密集型任务,多线程还是有用的,比如爬虫,很多时候都是等待网站返回、等待网页解析,而这个等待的时间CPU是空闲的,此时可以将CPU的资源供其他I/O任务运行,充分利用单核资源。值得注意的是,Python的GIL并不是Python语言固有的特点,而是因为Python解释器--CPython引入的概念,Python解释器有CPython、PyPy、Psyco,Python本身完全可以不依赖于GIL,因为大部分默认的Python解释器环境是CPython,被误解为GIL是Python本身的缺陷。

另外多核CPU多线程可能会比单线处理总任务的时间更长,比如,有CPU1, CPU2, 有线程A,B,C,初始时期线程A在CPU1上执行,当A释放锁后,处于CPU2的C线程被唤醒,准备执行,可是由于上一个线程的释放锁到下一个线程的加锁,时间极其短,C准备取锁执行时候,处于CPU1的线程B提前拿到了锁,开始执行,C线程只好继续等待调度,如此反复,浪费时间。

1.threading介绍

threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。python当前版本的多线程库没有实现优先级、线程组,线程也不能被停止、暂停、恢复、中断。

threading模块提供的类:   Thread, Lock, Rlock, Condition, [Bounded]Semaphore, Event, Timer, local。

threading 模块提供的常用方法:
  threading.currentThread(): 返回当前的线程变量。
  threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

threading 模块提供的常量:

threading.TIMEOUT_MAX 设置threading全局超时时间。

2.Thread类

构造方法: Thread(group=None, target=None, name=None, args=(), kwargs={})

group: 线程组,目前还没有实现,库引用中提示必须是None;
  target: 要执行的方法;
  name: 线程名;
  args/kwargs: 要传入方法的参数。

实例方法:   isAlive(): 返回线程是否在运行。正在运行指启动后、终止前。
  get/setName(name): 获取/设置线程名。

start(): 线程准备就绪,等待CPU调度
  is/setDaemon(bool): 获取/设置是后台线程(默认前台线程(False))。(在start之前设置)

如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,主线程和后台线程均停止
  如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止
  start(): 启动线程。
  join([timeout]): 阻塞当前上下文环境的线程,直到调用此方法的线程终止或到达指定的timeout(可选参数)。

3.Python多线程的两种使用方法

3.1 重写类方法

3.1.1 类方法重写代码块:

import threading
import random
import time
import datetime


class MyThread(threading.Thread):

    def __init__(self, lang):
        super(MyThread, self).__init__()
        self.lang = lang

    def run(self):
        # s = random.randint(1, 3)
        time.sleep(2)
        print("I Love {:<10} Time: {}  ThreadName: {:^}".format(self.lang, datetime.datetime.now(), threading.Thread().name))

3.1.2 不设置deamon和join:

if __name__ == "__main__":

    lang = ["Python", "Java", "PHP", "Golang", "C++", "C", "Julia"]
    for l in lang:
        T = MyThread(lang=l)
        # T.setDaemon(True)
        T.start()
    # T.join()
    print("主线程退出,time: %s" % datetime.datetime.now())

console: 主进程提前于子进程输出

主线程退出,time: 2019-04-07 20:37:47.847619
I Love Python     Time: 2019-04-07 20:37:49.846555  ThreadName: Thread-8
I Love C++        Time: 2019-04-07 20:37:49.852451  ThreadName: Thread-9
I Love Java       Time: 2019-04-07 20:37:49.852451  ThreadName: Thread-10
I Love Golang     Time: 2019-04-07 20:37:49.852932  ThreadName: Thread-11
I Love Julia      Time: 2019-04-07 20:37:49.853937  ThreadName: Thread-12
I Love C          Time: 2019-04-07 20:37:49.854098  ThreadName: Thread-13
I Love PHP        Time: 2019-04-07 20:37:49.854098  ThreadName: Thread-14

Process finished with exit code 0

3.1.3 设置deamon:

if __name__ == "__main__":

    lang = ["Python", "Java", "PHP", "Golang", "C++", "C", "Julia"]
    for l in lang:
        T = MyThread(lang=l)
        T.setDaemon(True)
        T.start()
    # T.join()
    print("主线程退出,time: %s" % datetime.datetime.now())

console:只要主进程结束,不管子进程如何直接退出

主线程退出,time: 2019-04-07 20:39:03.144132

Process finished with exit code 0

3.1.4 设置join:

if __name__ == "__main__":

    threadPool = []
    lang = ["Python", "Java", "PHP", "Golang", "C++", "C", "Julia"]
    for l in lang:
        T = MyThread(lang=l)
        threadPool.append(T)

    for t in threadPool:
        t.start()

    for t in threadPool:
        t.join()

    print("主线程退出,time: %s" % datetime.datetime.now())

console:子进程执行完,主进程退出

I Love Golang     Time: 2019-04-07 20:46:09.863986  ThreadName: Thread-8
I Love Julia      Time: 2019-04-07 20:46:09.864122  ThreadName: Thread-9
I Love C++        Time: 2019-04-07 20:46:09.864122  ThreadName: Thread-10
I Love Java       Time: 2019-04-07 20:46:09.865120  ThreadName: Thread-11
I Love C          Time: 2019-04-07 20:46:09.865120  ThreadName: Thread-12
I Love Python     Time: 2019-04-07 20:46:09.865120  ThreadName: Thread-13
I Love PHP        Time: 2019-04-07 20:46:09.866113  ThreadName: Thread-14
主线程退出,time: 2019-04-07 20:46:09.866113

Process finished with exit code 0

如上所有子进程输出并没有间隔两秒,可见当某一个子进程处于等待时期,其他子进程迅速开启,以此类推。

3.2 使用Thread类

3.2.1



def func(language):
    s = random.randint(1, 5)
    time.sleep(s)
    print("I Love {:<10} Time: {}  ThreadName: {:^}".format(language, datetime.datetime.now(), threading.current_thread().name))


if __name__ == "__main__":

    threadPool = []
    lang = ["Python", "Java", "PHP", "Golang", "C++", "C", "Julia"]
    for l in lang:
        T = threading.Thread(target=func, args=(l, ))
        threadPool.append(T)

    for t in threadPool:
        t.start()

    for t in threadPool:
        t.join()

    print("主线程退出,time: %s" % datetime.datetime.now())

console:

I Love Python     Time: 2019-04-07 20:59:02.136552  ThreadName: Thread-1
I Love C++        Time: 2019-04-07 20:59:02.142802  ThreadName: Thread-5
I Love Julia      Time: 2019-04-07 20:59:02.142802  ThreadName: Thread-7
I Love PHP        Time: 2019-04-07 20:59:03.139293  ThreadName: Thread-3
I Love C          Time: 2019-04-07 20:59:04.137688  ThreadName: Thread-6
I Love Golang     Time: 2019-04-07 20:59:04.137688  ThreadName: Thread-4
I Love Java       Time: 2019-04-07 20:59:05.139252  ThreadName: Thread-2
主线程退出,time: 2019-04-07 20:59:05.139708

Process finished with exit code 0

注意:threading.current_thread().name 和 threading.Thread().name方法的区别

4.线程锁

当多个线程访问同一个资源有可能造成混乱,此时就需要锁来规避可能造成的混乱。

Lock(指令锁)是可用的最低级的同步指令。Lock处于锁定状态时,不被特定的线程拥有。Lock包含两种状态——锁定和非锁定,以及两个基本的方法。

可以认为Lock有一个锁定池,当线程请求锁定时,将线程至于池中,直到获得锁定后出池。池中的线程处于状态图中的同步阻塞状态。

RLock(可重入锁)是一个可以被同一个线程请求多次的同步指令。RLock使用了“拥有的线程”和“递归等级”的概念,处于锁定状态时,RLock被某个线程拥有。拥有RLock的线程可以再次调用acquire(),释放锁时需要调用release()相同次数。

可以认为RLock包含一个锁定池和一个初始值为0的计数器,每次成功调用 acquire()/release(),计数器将+1/-1,为0时锁处于未锁定状态。

简言之:Lock属于全局,Rlock属于线程。

构造方法: Lock(),Rlock(),推荐使用Rlock()

实例方法:   acquire([timeout]): 尝试获得锁定。使线程进入同步阻塞状态。
  release(): 释放锁。使用前线程必须已获得锁定,否则将抛出异常

4.1 实例

我的想法是新建一个共享文件tets.txt 其中已经写入一行 "a", 现在实现用多线程访问该文件,每访问一次,在原来的基础上加 “a"。

  • 不加锁代码块

    import threading
    import time
    
    def save(s):
        with open("test.txt", "w") as f:
            f.write(s)
    
    def read():
        time.sleep(1)
        with open("test.txt", "r") as f:
            line = f.readline()
            print("before : {:>10}  ThreadName: {:^}".format(line, threading.current_thread().name))
        save(line + "a")
    
    
    for _ in range(10):
        T = threading.Thread(target=read)
        T.start()
    
  • console: 可见并没有如我想象的输出 "a" "aa" "aaa"这样的,

    before :          a  ThreadName: Thread-2
    before :          a  ThreadName: Thread-1
    before :         aa  ThreadName: Thread-7
    before :         aa  ThreadName: Thread-8
    before :         aa  ThreadName: Thread-9
    before :             ThreadName: Thread-4
    before :             ThreadName: Thread-3
    before :             ThreadName: Thread-6
    before :             ThreadName: Thread-10
    before :             ThreadName: Thread-5
    
    Process finished with exit code 0
    
  • 加锁代码块:

    import threading
    import time
    
    
    def save(s):
        with open("test.txt", "w") as f:
            f.write(s)
    
    def read(lock):
        time.sleep(1)
        lock.acquire()
        with open("test.txt", "r") as f:
            line = f.readline()
            print("before : {:>10}  ThreadName: {:^}".format(line, threading.current_thread().name))
        save(line + "a")
        lock.release()
    
    loc = threading.RLock()
    for _ in range(10):
        T = threading.Thread(target=read, args=(loc, ))
        T.start()
    
  • console: 实现了预期的功能

    before :          a  ThreadName: Thread-1
    before :         aa  ThreadName: Thread-9
    before :        aaa  ThreadName: Thread-2
    before :       aaaa  ThreadName: Thread-5
    before :      aaaaa  ThreadName: Thread-3
    before :     aaaaaa  ThreadName: Thread-4
    before :    aaaaaaa  ThreadName: Thread-8
    before :   aaaaaaaa  ThreadName: Thread-10
    before :  aaaaaaaaa  ThreadName: Thread-6
    before : aaaaaaaaaa  ThreadName: Thread-7
    
    Process finished with exit code 0
    
  • Lock和RLock的对比

    import threading
    lock = threading.Lock() #Lock对象
    lock.acquire()
    lock.acquire()  #产生了死锁。
    lock.release()
    lock.release()
    print lock.acquire()
     
     
    import threading
    rLock = threading.RLock()  #RLock对象
    rLock.acquire()
    rLock.acquire() #在同一线程内,程序不会堵塞。
    rLock.release()
    rLock.release()
    
4.2 Condition

Condition(条件变量)通常与一个锁关联。需要在多个Contidion中共享一个锁时,可以传递一个Lock/RLock实例给构造方法,否则它将自己生成一个RLock实例。

可以认为,除了Lock带有的锁定池外,Condition还包含一个等待池,池中的线程处于等待阻塞状态,直到另一个线程调用notify()/notifyAll()通知;得到通知后线程进入锁定池等待锁定。

构造方法: Condition([lock/rlock])

实例方法:   acquire([timeout])/release(): 调用关联的锁的相应方法。
  wait([timeout]): 调用这个方法将使线程进入Condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。
  notify(): 调用这个方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池);其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
  notifyAll(): 调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常

5.Event

Event(事件)是最简单的线程通信机制之一:一个线程通知事件,其他线程等待事件。Event内置了一个初始为False的标志,当调用set()时设为True,调用clear()时重置为 False。wait()将阻塞线程至等待阻塞状态。

Event其实就是一个简化版的 Condition。Event没有锁,无法使线程进入同步阻塞状态。

构造方法: Event()

实例方法:   isSet(): 当内置标志为True时返回True。
  set(): 将标志设为True,并通知所有处于等待阻塞状态的线程恢复运行状态。
  clear(): 将标志设为False。
  wait([timeout]): 如果标志为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()。

跟多进程类似

  • 代码块:

    import threading
    import time
    
    event = threading.Event()
    
    
    def func():
        print("输出第一部分,等待子线程设置event为True, event Status: {}".format(str(event.isSet())))
        event.wait(2) # 阻塞 如果wait()里面设置了时间,那么等时间到,event也会被设置为True ,就看与.set()相比谁先
        print("等待时间已经到,重置为False")
        event.clear()
        print("子线程将event设置为False,等待主线程置True,event Status: {}".format(str(event.is_set())))
        event.wait()
        print("主进程已经将event设置为True, event Status: {}".format(str(event.isSet())))
    
    t1 = threading.Thread(target=func)
    t1.start()
    
    print("准备将even设置为True")
    time.sleep(4)
    event.set()
    
  • console:

    输出第一部分,等待子线程设置event为True, event Status: False
    准备将even设置为True
    等待时间已经到,重置为False  #两秒后
    子线程将event设置为False,等待主线程置True,event Status: False
    主进程已经将event设置为True, event Status: True # 又过了两秒
    
    Process finished with exit code 0
    

6.timer

Timer(定时器)是Thread的派生类,用于在指定时间后调用一个方法。

构造方法: Timer(interval, function, args=[], kwargs={})
  interval: 指定的时间
  function: 要执行的方法
  args/kwargs: 方法的参数

实例方法: Timer从Thread派生,没有增加实例方法。

  • 代码块:注意 type hint

    # -*- coding: utf-8 -*-
    # @Time    : 2019/4/7 22:33
    # @Author  : YuChou
    # @Site    :
    # @File    : _thread.py
    # @Software: PyCharm
    
    import threading
    
    def f(*args: tuple, **kwargs: dict) -> None:
        print("args: {}, kwargs: {}".format(args, kwargs))
    
    t = threading.Timer(3, f, args=("a", 1), kwargs={'c': 2, 'd': 3})
    t.start()
    

    console:

    args: ('a', 1), kwargs: {'c': 2, 'd': 3}
    
    Process finished with exit code 0
    

7.local

local是一个小写字母开头的类,用于管理 thread-local(线程局部的)数据。对于同一个local,线程无法访问其他线程设置的属性;线程设置的属性不会被其他线程设置的同名属性替换。

可以把local看成是一个“线程-属性字典”的字典,local封装了从自身使用线程作为 key检索对应的属性字典、再使用属性名作为key检索属性值的细节

  • 代码块:

    import threading
    
    
    local = threading.local()
    local.tname = 'Hello'
    
    
    def func():
        local.tname = 'World'
        print(local.tname, end=" ")
    
    
    t1 = threading.Thread(target=func)
    t1.start()
    t1.join()
    
    print(local.tname)
    

console:

World Hello

Process finished with exit code 0

参考:https://www.cnblogs.com/tkqasn/p/5700281.html

https://www.cnblogs.com/bingabcd/p/6671368.html

https://www.python.org/dev/peps/pep-0484/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容