Python——线程

线程的定义

线程是操作系统能够进行运算调度的最小单位。它被包含在进程中。是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流。一个进程中可以并发多个线程,每条线程并行执行不同的任务
多个线程的执行会通过线程的调度去抢占CPU的资源

进程的定义

程序执行的实例称为进程
每个进程提供执行程序所需的资源。进程具有虚拟地址空间,可执行代码,系统对象的打开句柄,安全上下文,唯一进程标识符,环境变量,优先级类别,最小和最大工作集。每个进程都使用单线程启动,通常称为主线程,但可以从其任何线程创建其它线程

进程和线程的比较
进程和线程之间的比较是没有意义的,因为进程是一个程序的执行实例,而进程是由线程进行执行的,但线程和进程毕竟还是两种机制

  • 进程可以创建子进程,而每个子进程又可以开多个线程
  • 线程之间可以共享数据,而线程之间不可以共享数据,线程之间可以进行通信,而进程之间进行通信就会比较麻烦
  • 开辟进程要比开辟线程的开销大很多

Python中创建线程

Python中创建线程有多种模式

threading 模块

直接调用threading模块 创建线程

Python中创建线程可以使用threading模块

  • threading.Thread(target=func,args = params,) 创建线程 target指定执行的函数 target指定参数元组形式
'''
python thread
'''
import threading

import time

beggin = time.time()


def foo(n):
    print('foo%s' % n)
    time.sleep(1)


def bar(n):
    print('bar %s' % n)


end = time.time()
cast_time = end - beggin
print(float(cast_time))
# 创建线程
t1 = threading.Thread(target=foo, args=('thread1',))
t2 = threading.Thread(target=bar, args=('thread2',))
t1.start()
t2.start()

通过继承threading模块调用线程

import threading
import time
 
 
class MyThread(threading.Thread):
    def __init__(self,num):
        threading.Thread.__init__(self)
        self.num = num
 
    def run(self):#定义每个线程要运行的函数
 
        print("running on number:%s" %self.num)
 
        time.sleep(3)
 
if __name__ == '__main__':
 
    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()
  • 创建类继承threading.Thread
  • 重写类的run方法

Python 多线程中的GIL

Python的GIL并不是Python的特性,它是在实现Python解析器也就是基于C语言的解析器 CPython时所引入的一个概念。Python可以用不同的编译器来编译成可执行代码。例如C语言中的GCC等。也就是说只有在CPython中才会出现GIL的情况
GIL又称为全局解释器锁(Global Interpreter Lock)
现代的CPU已经是多核CPU,为了更有效的利用多核处理器的性能,就出现了多线程的编程方式。而在解决多线程之间数据完整性和状态同步的最简单的方法就是加锁。GIL就是给Python解释器加了一把大锁。我们知道Python是由解释器执行的,由于GIL的存在 只能有一个线程被解释器执行,这样就使得Python在多线程执行上的效率变低。由于历史遗留问题,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。也就是说在多核CPU上,并行执行的Python多线程,甚至不如串行执行的Python程序,这就是GIL存在的问题

Python GIL的出现场景

在Python中如果任务是IO密集型的,可以使用多线程。而且Python的多线程非常善于处理这种问题
而如果Python中如果任务是计算密集型的,就需要处理一下GIL

join 和daemon

join

  • 在子线程完成运行之前,这个子线程的父线程将一直被阻塞。在一个程序中我们执行一个主线程,这个主线程又创建一个子线程,主线程和子线程就互相执行,当子线程在主线程中调用join方法时,主线程会等待子线程执行完后再结束
'''in main thread'''
t.join() 主线程会等待线程t执行完成后再继续执行

daemon

  • setDaemon(true)
    将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦
  • currentThread() 获取当前执行的线程

线程中的锁

先看一个线程共享数据的问题

'''
线程安全问题
'''
# 定义一个共享变量
import threading

import time

num = 100


def sub():
    # 操作类变量
    global num
    tmp = num
    time.sleep(0.1)
    num = tmp - 1


if __name__ == '__main__':
    thread_list = []
    for i in range(100):
        t1 = threading.Thread(target=sub)
        t1.start()
        thread_list.append(t1)
    for i in range(100):
        t2 = thread_list[i]
        t2.join()

print('final num' + str(num))
>>> 
final num99

分析

上面的程序中,我们想要的是开启100个线程,每个线程将共享数据减去1,但是我们发现 输出的结果是99,这种情况是因为多线程在cpu中执行时是抢占式的,程序在开始执行时,开启了100个线程去执行,当程序执行到time.sleep(0.1)时,由于发生了线程的阻塞,所以cpu进行了切换,此时,程序的共享变量num是100,中间变量tmp也是100 在线程阻塞过后,将共享变量num的值减1,值变为99 此时其它的线程获得cpu的执行机会,而当前线程中的共享变量num的值还是100所以执行减1操作后,又将中间值赋值给共享变量num所以num的值一直为99

  • 线程的执行情况


    多线程抢占.png

Python 同步锁

操作锁的方法在threading 模块中 Lock()

  • threading.Lock() 会获得一把锁
  • Python 中使用acquire() 获得锁
r = threading.Lock()
# 加锁
r.acquire()
  • Python中使用release()释放锁
r.release()

加锁后代码

'''
线程安全问题
'''
# 定义一个共享变量
import threading
import time
num = 100
r = threading.Lock()
def sub():
    # 操作类变量
    global num
    r.acquire()
    tmp = num
    time.sleep(0.1)
    num = tmp - 1
    r.release()
if __name__ == '__main__':
    thread_list = []
    for i in range(100):
        t1 = threading.Thread(target=sub)
        t1.start()
        thread_list.append(t1)
    for i in range(100):
        t2 = thread_list[i]
        t2.join()
print('final num' + str(num))

线程中的死锁和递归锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方释放对方的资源,就会造成死锁,因为系统判断这部分资源正在使用,所以这两个线程在无外力作用下将一直等待下去
看个栗子:

'''
线程死锁
'''

import threading, time


class myThread(threading.Thread):
    def doA(self):
        lockA.acquire()
        print(self.name, "gotlockA", time.ctime())
        time.sleep(3)
        lockB.acquire()
        print(self.name, "gotlockB", time.ctime())
        lockB.release()
        lockA.release()

    def doB(self):
        lockB.acquire()
        print(self.name, "gotlockB", time.ctime())
        time.sleep(2)
        lockA.acquire()
        print(self.name, "gotlockA", time.ctime())
        lockA.release()
        lockB.release()

    def run(self):
        self.doA()
        self.doB()


if __name__ == "__main__":

    lockA = threading.Lock()
    lockB = threading.Lock()

    threads = []
    for i in range(5):
        threads.append(myThread())
    for t in threads:
        t.start()
    for t in threads:
        t.join()  # 等待线程结束,后面再讲。

在以上程序中,多个线程互相持有对方的锁并且等待对方释放,这就形成了死锁

解决死锁的方式

  • threading.RLock() 可重入锁
    为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。可重入锁的内部维持了一个计数器和锁对象。

信号量

信号量用来控制线程并发数的,BoundedSemaphore或Semaphore管理一个内置的计数器,每当调用acquire()时-1,调用release()时+1
计数器不能小于0当计数器为0时,acquire()将阻塞线程至同步锁定状态,直到其他线程调用release()。
BoundedSemaphore与Semaphore的唯一区别在于前者将在调用release()时检查计数器的值是否超过了计数器的初始值。如果超过了将抛出一个异常

创建信号量

  • threading.BoundedSemaphore(num) 指定信号量为num
import threading

import time


class Mythread(threading.Thread):
    def run(self):
        # 判断是否加锁
        if semaphore.acquire():
            print(self.name)
            time.sleep(1)
            # 释放锁
            semaphore.release()


if __name__ == '__main__':
    # 创建带有信号量的锁
    semaphore = threading.BoundedSemaphore(5)
    # 存放线程的序列
    thrs = []
    for i in range(100):
        thrs.append(Mythread())
    for t in thrs:
        t.start()

条件变量同步

有一类线程需要满足条件之后才能够继续执行,Python提供了threading.Condition 对象用于条件变量线程的支持,它除了能提供RLock()或Lock()的方法外,还提供了 wait()、notify()、notifyAll()方法。
条件变量也是线程中的一把锁,但是条件变量可以实现线程间的通信,类似于Java中的唤醒和等待

创建条件变量锁

  • lock_con = threading.Condition(Lock/Rlock) 锁是可选选项,不传入锁对象自动创建一个RLock()
  • wait() 条件不满足时调用,线程会释放锁并进入等待阻塞
  • notify() 条件创造后调用,通知等待池激活一个线程
  • notifyAll() 条件创造后调用,通知等待池激活所有线程
    看个栗子
'''
线程条件变量
'''
import threading
from random import randint

import time


class Producer(threading.Thread):
    def run(self):
        global L
        while True:
            val = randint(0, 100)
            print('生产者', self.name, ':Append' + str(val), L)
            if lock_con.acquire():
                L.append(val)
                lock_con.notify()
                lock_con.release()
            time.sleep(3)


class Consumer(threading.Thread):
    def run(self):
        global L
        while True:
            lock_con.acquire()
            if len(L) == 0:
                lock_con.wait()
            print('消费者',self.name,"Delete"+str(L[0]),L)
            del  L[0]
            lock_con.release()
            time.sleep(0.25)


if __name__ == '__main__':
    L = []
    # 创建条件变量锁
    lock_con = threading.Condition()
    # 线程存放列表
    threads = []
    for i in range(5):
        threads.append(Producer())
    threads.append(Consumer())
    for t in threads:
        t.start()
    for t in threads:
        t.join()

同步条件event

条件同步和条件变量同步差不多意思,只是少了锁功能,因为条件同步设计于不访问共享资源的条件环境。event=threading.Event():条件环境对象,初始值 为False;

  • event.isSet():返回event的状态值;

  • event.wait():如果 event.isSet()==False将阻塞线程;

  • event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

  • event.clear():恢复event的状态值为False。
    举个栗子:

'''
同步条件event
'''
import threading

import time


class Boss(threading.Thread):
    def run(self):
        print('BOSS: 今晚加班')
        # 改变事件
        event.isSet() or event.set()
        time.sleep(5)
        print('BOSS:加班结束')
        event.isSet() or event.set()


class Worker(threading.Thread):
    def run(self):
        event.wait()
        print('WORKER:OH NO')
        time.sleep(0.25)
        # 改变同步事件标志
        event.clear()
        event.wait()
        print('WORKER:OH YEAD!')

if __name__ == '__main__':
    # 获取同步事件
    event = threading.Event()
    threads = []
    for i in range(5):
        threads.append(Worker())
    threads.append(Boss())
    for t in threads:
        t.start()
    for t in threads:
        t.join()

线程利器队列 queue

队列是一种数据结构,队列分为先进先出(FIFO) 和 先进后出(FILO)
Python Queue模块有三种队列及构造函数:
1、Python Queue模块的FIFO队列先进先出。 class queue.Queue(maxsize)
2、LIFO类似于堆,即先进后出。 class queue.LifoQueue(maxsize)
3、还有一种是优先级队列级别越低越先出来。 class queue.PriorityQueue(maxsize)
队列能够保证数据安全,是因为队列的内部维护着一把锁。每个去队列中取数据的都会保证数据的安全。而列表虽然具有同样的功能,但是列表不是数据安全的

创建一个队列

Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

向队列中插入数据

  • q.put(item,block)
    调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。

从队列中取出数据

  • q.get()
    调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。

API

  • q.qsize() 返回队列的大小
  • q.empty() 如果队列为空,返回True,反之False
  • q.full() 如果队列满了,返回True,反之False
  • q.full 与 maxsize 大小对应
  • q.get([block[, timeout]]) 获取队列,timeout等待时间
  • q.get_nowait() 相当q.get(False)
    非阻塞 q.put(item) 写入队列,timeout等待时间
  • q.put_nowait(item) 相当q.put(item, False)
  • q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
  • q.join() 实际上意味着等到队列为空,再执行别的操作
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 线程 引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0....
    不浪漫的浪漫_ea03阅读 357评论 0 0
  • 引言&动机 考虑一下这个场景,我们有10000条数据需要处理,处理每条数据需要花费1秒,但读取数据只需要0.1秒,...
    chen_000阅读 501评论 0 0
  • 目录 一、开启线程的两种方式 在python中开启线程要导入threading,它与开启进程所需要导入的模块mul...
    CaiGuangyin阅读 2,396评论 1 16
  • 众所周知, 计算机是由软件和硬件组成. 硬件中的CPU主要用于解释指令和处理数据, 软件中的操作系统负责资源的管理...
    凉茶半盏阅读 651评论 2 16
  • 我在公平这件事上,做的真棒!那就是错过了两个最重要的人的婚礼,一点没偏心谁,哈哈哈(悲伤脸…) 慧儿姐你今天一定是...
    沈芮汐阅读 222评论 0 0