Python多线程解析

概述

记得前些日子伞哥发过一个微博调侃过Python由于GIL锁的存在,所以现在死活想把自己和机器学习扯上关系。确实,由于这个全局解释锁的存在,任何时刻只有一个核在执行Python代码,这样就导致不能充分利用多核处理器的特性。但是,我们的程序也不总是在计算的,程序有IO密集型和CPU计算密集型。如果我们的程序需要等待用户输入,等待文件读写以及网络收发数据,那计算机就会把这些等待操作放到后台去处理,把CPU留出来用于计算。所以,虽然CPU密集型的程序用Python多线程确实无法提高效率,但是如果是IO密集型的程序,是可以使用多线程提高效率的。

接下来,让我们通过例子一步一步了解多线程:

利用threading模块使用多线程

Python标准库自带了两个多线程模块,分别是threadingthread,其中,thread是低级模块,threading是对thread的封装,一般,我们直接使用threading即可。下面来看一个简单的多线程例子:

import threading

def say_hello():
    print("Hello world!")

def main():
    for i in range(10):
        thread = threading.Thread(target=say_hello)
        thread.start()

main()

在这个例子中,我们首先定义了要多线程执行的函数say_hello,然后我们在主函数里创建了10个线程,target取值是say_hi,告诉线程要执行的函数,然后我们调用start()方法吩咐线程去执行这些线程。
这个程序最终会输出10个Hello world!,与["Hello world!" for i in range(10)]效果一致,那么为什么还要使用多线程呢,我们通过下面这个例子理解下多线程的意义:

import threading
import time

def say_hello():
    time.sleep(1)
    print("Hello world!")

def main():
    for i in range(10):
        thread = threading.Thread(target=say_hello)
        thread.start()

main()

在这个例子里,我们加了time.sleep(1)来模拟等待事件。现在如果用普通的循环来迭代,代码执行完需要至少20秒,而多线程运行只需要1秒多,减少了程序整体运行的时间。

给线程传参和线程常用方法

在上面的代码中,我们并没有给say_hello传参数,在多线程里传参很简单,只需要这样做就好了:

import threading

def say_hello(count, name):
    print("Hello world!", name)
    count -= 1

def main():
    name_list = ['Bob', 'Jack', 'Jone', 'Mike', 'David']
    for i in range(5):
        thread = threading.Thread(target=say_hello, args=(10, name_list[i]))
        thread.start()

main()

threading.Thread类中,常用的方法有:

  • isAlive: 检查线程是否在运行中
  • getName: 获取线程名称
  • setName: 设置线程名称
  • join:阻塞线程调用,直到线程中止
  • setDaemon:设置线程为守护线程
  • isDaemon: 判断线程是否是守护线程

通过继承创建线程

除了直接实例化threading.Thread对象,我们还可以通过继承threading.Thread来编写多线程的类。然后把多线程调用的函数携程一个run方法。方法如下:

import threading

class MyThread(threading.Thread):
      def __init__(self, count, name):
          super(MyThread, self).__init__()
          self.count = count
          self.name = name
     
      def run(self):
          while self.count > 10:
                print("hello", self.name) 
                self.count -= 1

线程与互斥锁

多个线程之间 内存是共享的,所以线程比进程轻量。多个线程是可以同时访问内存中的数据的,如果多个线程同时修改一个对象,那这份数据可能会被破坏,Python的threading类中提供了Lock方法,它会返回一个锁对象,一般通过lock.acquire()来获取锁,通过lock.release()来释放锁,对于那种只允许一个线程操作 的数据,一般把对其的操作放在lock.acquire()lock.release()中间。
无论在什么情况下,我们都要保证代码要释放锁,所以其他语言中一般把加锁和释放锁放在try/finally语句中。在Python中,其实我们可以用上下文管理器来简化代码,关于上下文管理器的介绍可以参考我前面的文章:上下文管理器,这里我们可以这样使用锁:

with lock:
    #lock processing

下面来看一个使用互斥锁的例子,在这个例子中,我们使用了全局变量,然后创建10个线程,每个线程做同样的事情,由于num是全局变量,而且每个线程都需要使用这个变量,因此存在着数据争用的问题,所以,我们就需要使用互斥锁保护这个全局变量:所有修改这个变量的线程在修改前都需要加锁,在increment函数中,我们通过with语句进行加锁。如下所示:

import threading

lock = threading.Lock()
num = 0


def increment(count):
    global num
    while count > 0:
        with lock:
            num += 1
        count -= 1

def main():
    threads = []
    for i in range(10):
        thread = threading.Thread(target=increment,args=(100,))
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()

    print("except value is 1000, real value is{}".format(num))
    
main()


有兴趣的读者可以试试把锁去掉是什么结果,实际上,我们永远得不到正确的结果。

对于这段代码,我们可以这样理解:

    threads = []
    for i in range(10):
        thread = thread.Threading(target=increment, args=(100,))
        thread.start()
        threads.append(thread)
 
    for thread in threads:
        thread.join()

第一个for循环,意思是吩咐十个线程去做target里面的事,执行完第一个for循环后就吩咐完了,但仅仅是只是吩咐完了,target里面的任务没有执行完,因为不知道是否执行完,所以还需要创建一个列表把他们都保存起来。对于第二个for循环,如果在第一个循环里 他们的target已经执行完了,那后面直接就join,不用等待,遇到有的线程没有执行完的,join 就阻塞调用,直到这些线程执行完。

线程安全队列queue

队列是线程间最常用的交换数据的形式,queue模块实现了线程安全的队列,有三种类型的队列:

  • queue Queue:FIFO(先进先出) 的队列。最常用的队列!
  • queue LifoQueue: LIFO(后进先出)的队列,最后加入队列的元素最先取出
  • queue PriorityQueue: 优先级队列,队列中的元素根据优先级排序。

下面是Queue类常用的方法:

  • empty: 判断队列是否为空
  • full: 判断队列是否已满
  • put: 向队列中添加元素
  • get: 从队列中取出元素
  • put_nowait: 非阻塞 向队列中添加元素
  • get_nowait: 非阻塞 从队列中取出元素
  • join:阻塞等待,直到所有任务完成

来看一个官方给的多线程模型:

def worker():
    while True:
    item = q.get()
    do_work()
    q.task_done()

q = Queue()

for i in range(thread_number):
    t = Thread(target=worker)
    t.daemon = True
    t.start()

for item in source():
    q.put(item)  

q.join()

之后会有一个线程池的例子运用Queue队列。写完后放链接!待续!

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

推荐阅读更多精彩内容

  • 一文读懂Python多线程 1、线程和进程 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运...
    星丶雲阅读 1,380评论 0 4
  • Python 面向对象Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对...
    顺毛阅读 4,165评论 4 16
  • 线程 引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0....
    不浪漫的浪漫_ea03阅读 340评论 0 0
  • 引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0.1秒,...
    chen_000阅读 457评论 0 0
  • Jenkins: 是一个可扩展的持续集成引擎。 主要用于: 持续自动构建测试的项目(开发和测试人员均可)、监控一些...
    王洋Future阅读 775评论 0 1