Python 多线程

简介

  • 多线程,就类似与操作系统中的多进程。简单的讲,就是可以同时并发执行多个任务,处理多件事情
  • 线程是最小的执行单元,而进程由至少一个线程组成
  • 主线程会等待所有的子线程结束后才结束

创建线程

  • 使用threading模块的Thread类,通过指定target与args(可选)参数。

  • 通过继承Thread类,重写run方法。

方式一:使用threading.Thread
from threading import Thread
from time import sleep, ctime


def sing():
    for i in range(3):
        print('正在唱歌%d:%s' % (i, ctime()))
        sleep(1)


def dance():
    for i in range(3):
        print('正在跳舞%d:%s' % (i, ctime()))
        sleep(1)


if __name__ == '__main__':
    t1 = Thread(target=sing)
    t2 = Thread(target=dance)
    t1.start()
    t2.start()

输出信息:

正在唱歌0:Thu Jul 26 11:21:33 2018
正在跳舞0:Thu Jul 26 11:21:33 2018
正在唱歌1:Thu Jul 26 11:21:34 2018
正在跳舞1:Thu Jul 26 11:21:34 2018
正在唱歌2:Thu Jul 26 11:21:35 2018
正在跳舞2:Thu Jul 26 11:21:35 2018
方式二:继承Thread
from threading import Thread
from time import sleep, ctime


class MyThread1(Thread):
    def run(self):
        for i in range(3):
            print('%s:%d:%s' % (self.name, i, ctime()))
            sleep(1)


class MyThread2(Thread):
    def run(self):
        for i in range(3):
            print('%s:%d:%s' % (self.name, i, ctime()))
            sleep(1)


if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

输出信息:

Thread-1:0:Thu Jul 26 11:24:22 2018
Thread-2:0:Thu Jul 26 11:24:22 2018
Thread-1:1:Thu Jul 26 11:24:23 2018
Thread-2:1:Thu Jul 26 11:24:23 2018
Thread-1:2:Thu Jul 26 11:24:24 2018
Thread-2:2:Thu Jul 26 11:24:24 2018

线程的生命周期

  1. 新建:创建对象
  2. 就绪:调用start后
  3. 运行:获得CPU资源
  4. 阻塞(挂起):失去CPU资源
  5. 死亡:线程执行结束或抛出未捕获的异常

线程相关操作

threading模块相关功能:

  • threading.active_count() : 当前活动线程数量
  • theading.enumerate() : 获取所有活跃线程
  • theading.current_thread() : 获取当前运行线程
  • threading.get_ident() : 获取线程标识
  • threading.main_thread() : 获取主线程

线程对象相关功能:

  • start():启动线程
  • run():运行线程
  • join(timeout=None) : 阻塞调用线程,直到队列中的所有任务被处理掉
  • is_alive():是否活动

线程锁

互斥锁:当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制

  • threading.Lock():获得线程锁
  • acquire():尝试去获得锁。
  • release():释放所获得的锁。

注意:要确保锁得到有效的释放

锁的好处:
  • 确保了某段关键代码只能由一个线程从头到尾完整地执行
锁的坏处:
  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
  • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁
from threading import Thread, Lock


def thread1():
    global g_num
    for i in range(100000):
        # 上锁
        # True:表示阻塞,如果这个锁在上锁之前已经上锁,那么这个线程就会在这里一直等待解锁位置
        # False:表示非阻塞,即不管本次调用能否成功上锁,都不会卡在这里,而是继续执行下面的代码
        mutex_flag = mutex.acquire(True)
        if mutex_flag:
            g_num += 1
            # 释放锁
            mutex.release()
    print('-- test1 --', g_num)


def thread2():
    global g_num
    for i in range(100000):
        # 上锁
        #
        mutex_flag = mutex.acquire(True)
        if mutex_flag:
            g_num += 1
            mutex.release()
    print('-- test2 --', g_num)


g_num = 0
# 创建一个互斥锁
# 这个锁默认是未上锁的状态
mutex = Lock()

t1 = Thread(target=thread1)
t2 = Thread(target=thread2)
t1.start()
t2.start()
t1.join()
t2.join()

print('-- main --', g_num)

输出信息:

-- test2 -- 189183
-- test1 -- 200000
-- main -- 200000

线程同步

多个线程有序执行

from threading import Thread, Lock
from time import sleep


class Task1(Thread):
    def run(self):
        while True:
            if lock1.acquire():
                print('-- Task 1 --')
                sleep(0.5)
                lock2.release()


class Task2(Thread):
    def run(self):
        while True:
            if lock2.acquire():
                print('-- Task 2 --')
                sleep(0.5)
                lock3.release()


class Task3(Thread):
    def run(self):
        while True:
            if lock3.acquire():
                print('-- Task 3 --')
                sleep(0.5)
                lock1.release()


lock1 = Lock()
lock2 = Lock()
lock3 = Lock()

lock2.acquire()
lock3.acquire()

t1 = Task1()
t2 = Task2()
t3 = Task3()

t1.start()
t2.start()
t3.start()

输出信息:

-- Task 1 --
-- Task 2 --
-- Task 3 --
-- Task 1 --
-- Task 2 --
-- Task 3 --
-- Task 1 --
-- Task 2 --
...

ThreadLocal

  • 一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题
  • 一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁
import threading

# 创建全局ThreadLocal对象
local_school = threading.local()


def process_student():
    # 获取当前线程管理的student
    std = local_school.student
    print('%s(in %s)' % (std, threading.current_thread().name))


def process_thread(name):
    # 绑定ThreadLocal的student
    local_school.student = name
    process_student()


if __name__ == '__main__':
    t1 = threading.Thread(target=process_thread, args=('Tom',), name='Thread_A')
    t2 = threading.Thread(target=process_thread, args=('Jim',), name='Thread_B')
    t1.start()
    t2.start()

输出信息:

Tom(in Thread_A)
Jim(in Thread_B)

Condition (条件变量)

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

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

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

# 商品
product = None
# 条件变量
con = threading.Condition()


# 生产者
def produce():
    global product

    # 开启锁
    if con.acquire():
        while True:
            if product is None:
                print('produce...')
                product = '商品'

                # 通知消费者,商品已经生产
                # 收到通知的线程将自动调用acquire()尝试获得锁定
                con.notify()

            # 等待通知
            # 线程进入Condition的等待池等待通知,并释放锁
            con.wait()
            time.sleep(1)


# 消费者
def consume():
    global product

    if con.acquire():
        while True:
            if product is not None:
                print('consume...')
                product = None

                # 通知生产者,商品没了
                con.notify()

            # 等待通知
            con.wait()
            time.sleep(1)


t1 = threading.Thread(target=produce)
t2 = threading.Thread(target=consume)
t1.start()
t2.start()

输出信息:

produce...
consume...
produce...
consume...
...

线程队列

Python的Queue模块中提供了同步的、线程安全的队列类

queue模块提供了队列的功能,该模块具有三个类:
  • queue.Queue(maxsize=0):FIFO(先入先出)队列Queue
  • queue.LifoQueue(maxsize=0):LIFO(后入先出)队列LifoQueue
  • queue.PriorityQueue(maxsize=0):优先级队列PriorityQueue

这些队列都实现了锁原语(可以理解为原子操作,即要么不做,要么就做完),能够在多线程中直接使用。可以使用队列来实现线程间的同步。

from queue import Queue
import threading
import time


class Producer(threading.Thread):
    def run(self):
        global queue
        count = 0
        while True:
            # 当商品数量低于1000时,生产者生产100个
            if queue.qsize() < 1000:
                for i in range(100):
                    count = count + 1
                    msg = '生产商品' + str(count)
                    print(msg)
                    queue.put(msg)
            time.sleep(0.5)


class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            # 当商品数量多于100个时,消费者消费3个
            if queue.qsize() > 100:
                for i in range(3):
                    msg = self.name + '消费了' + queue.get()
                    print(msg)
            time.sleep(1)


if __name__ == '__main__':
    queue = Queue()

    for i in range(500):
        queue.put('初始产品' + str(i))

    # 开启2位生产者
    for i in range(2):
        p = Producer()
        p.start()

    # 开启5位消费者
    for i in range(5):
        c = Consumer()
        c.start()

GIL (global interpreter lock)

全局解释器锁,是在解释器层面上进行的线程同步

cpython多线程执行的效率往往不够理想,受到GIL锁的影响 同一时间内,只有一个线程对python对象进行操作,基本有点接近单线程 有很多人都想进城门,每一次只能有一个人进,甲进门,其他都不能进门 这样设计的目的主要是为了安全性,cpython内存管理是非线程安全的 python代码的执行是由python虚拟机(解释器主循环)来控制的,在主循环中同一时间内只能有一个线程来执行

python虚拟机如何来执行代码
  1. 要获取 GIL
  2. 切换到当前线程去执行python代码
  3. 1 先执行当前线程中的代码
  4. 2 当前线程代码执行完毕,主动让出控制权
  5. 当前让出了控制权,状态处于休眠或者等待
  6. GIL解锁
  7. 重复上述的过程
并发执行,并行执行
  1. 并发执行:多个线程之间来回切换—>并不是同一时间同时在执行多个任务
  2. 并行执行:多线程同一时间内同时运行 百米赛跑:所有运动员都开始奔跑

死锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁

import threading
import time


class MyThread1(threading.Thread):
    def run(self):
        if mutexA.acquire():
            print(self.name + '--do1 up--')
            time.sleep(1)

            if mutexB.acquire():
                print(self.name + '--do1 down--')
                mutexB.release()
            mutexA.release()


class MyThread2(threading.Thread):
    def run(self):
        if mutexB.acquire():
            print(self.name + '--do2 up--')
            time.sleep(1)

            if mutexA.acquire():
                print(self.name + '--do2 down--')
                mutexA.release()
            mutexB.release()


mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

并行与并发

  • 并发:看上去同时执行,任务数多于核心数
  • 并行:真正同时执行,任务数小于等于核心数

线程和进程的对比

定义不同
  • 进程是系统进行资源分配和调度的一个独立单位
  • 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程 自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但 是它可与同属一个个进程的其他的线程共享进程所拥有的全部资源.
区别
  • 一个程序至少有一个进程,一个进程至少有一个线程
  • 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高
  • 进程在执⾏过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了 程序的运行效率
  • 线线程不能够独立执行,必须依存在进程中
优缺点
  • 线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和 保护;而进程正相反
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值