• Python多线程


    1.  什么是多线程

    线程,有时被称为轻量进程,是程序执行流的最小单元。一个标准的线程由线程ID当前指令指针(PC寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程不拥有私有的系统资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行

    线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。Python多线程用于I/O操作密集型的任务,如Socket Server网络并发,网络爬虫。

    现代处理器都是多核的,几核的处理器表示能同时处理几个线程,多线程执行程序看起来是同时进行,实际上是CPU在多个线程之间快速切换执行,这中间就涉及到上下文的切换,所谓的上下文切换就是指一个线程Thread被分配的时间片用完了之后,线程的信息被保存起来,CPU执行另外的线程,再到CPU重新读取线程Thread的信息并继续执行Thread的过程。

    2.  Python线程模块

    Python的标准库提供了两个模块:_threadthreading_thread 提供了低级别的、原始的线程以及一个简单的互斥锁,它相比于 threading 模块的功能还是比较有限的。Threading模块是_thread模块的替代,在实际的开发中,绝大多数情况下还是使用高级模块threading,因此本书着重介绍threading高级模块的使用。

    Python创建Thread对象语法如下:

    importthreading
    threading.Thread(
    target=None, name=None,  args=())

    主要参数说明:

    l target 是函数名字,需要调用的函数。

    l  name 设置线程名字。

    l  args 函数需要的参数,以元组( tuple)的形式传入

    Thread对象主要方法说明:

    l  run(): 用以表示线程活动的方法。

    l  start():启动线程活动。

    l  join(): 等待至线程中止。

    l  isAlive(): 返回线程是否活动的。

    l  getName(): 返回线程名。

    l  setName(): 设置线程名。

    3.   创建线程

    Python中实现多线程有两种方式:函数式创建线程和创建线程类。

    3.1. 函数式创建线程

    函数式创建线程的时候,只需要传入一个执行函数和函数的参数即可完成threading.Thread实例的创建。下面的例子使用Thread类来产生2个子线程,然后启动2个子线程并等待其结束:

    import threading
    import time
    import random
    import math

    def print_num(idx):
       
    for num in range(idx):
           
    # 打印当前运行的线程名字
           
    print("{0} num={1}".format(threading.current_thread().getName(), num))
            delay = math.ceil(random.random() *
    2)
            time.sleep(delay)
       
    print()

    if __name__ == '__main__':
        thread_list = []
       
    for i in range(1, 3):
            thread = threading.Thread(
                
    target=print_num,
               
    args=(i + 1,),
               
    name="thread%s" % i
            )
            thread.start()
            thread_list.append(thread)
        [t.join()
    for t in thread_list]
       
    print("{0} 线程结束".format(threading.current_thread().getName()))

    运行代码得到以下结果:

     运行程序时,默认会启动一个线程,把该线程称主线程,主线程可以启动新的线程,threading模块有个current_thread()函数,它可以返回当前线程的相关信息。从当前线程的示例可以获得前运行线程名字,代码如下。

    threading.current_thread().getName()

    启动一个线程就是把一个函数和参数传入并创建Thread实例,然后调用start()开始执行:

    thread = threading.Thread(
       
    target=print_num,
        
    args=(i + 1,),
       
    name="thread"
    )
    thread.start()

    从返回结果可以看出主线程示例的名字叫MainThread,子线程的名字在创建时指定,例创建2个子线程,名字叫thread1thread2。如果没有给线程起名字,Python就自动给线程命名为Thread-1,Thread-2…等等。在本例中定义了线程函数print_num(),打印idx记录后退出,每次打印使用time.sleep()让程序休眠一段时间。

    3.2.创建线程类

    直接创建threading.Thread的子类来创建一个线程对象,实现多线程。通过继承Thread类,并重写Thread类的run()方法,在run()方法中定义具体要执行的任务。在Thread类中,提供了一个start()方法用于启动新进程,线程启动后会自动调用run()方法。

    import threading
    import time
    import random
    import math

    class MultiThread(threading.Thread):

       
    def __init__(self, thread_name, num):
            threading.Thread.
    __init__(self)
           
    self.name = thread_name
           
    self.num = num

       
    def run(self):
           
    for i in range(self.num):
               
    print("{0} i={1}".format(threading.current_thread().getName(), i))
                delay = math.ceil(random.random() *
    2)
                time.sleep(delay)

    if __name__ == '__main__':
        thread_list = []
       
    for n in range(1, 3):
            thread = MultiThread(
               
    thread_name="thread%s" % n, num=n + 1
            
    )
            thread.start()
            thread_list.append(thread)
        [t.join()
    for t in thread_list]
       
    print("{0} 线程结束".format(threading.current_thread().getName()))

    运行脚本得到以下结果:

    从返回结果可以看出,通过创建Thread类来产生2个线程对象,重写Thread类的run()函数,把业务逻辑放入其中,通过调用线程对象的start()方法启动线程。通过调用线程对象的join()函数,等待该线程完成,再继续下面的操作。

    本例中,主线程MainThread等待子线程thread1thread2运行结束后才输出“MainThread线程结束”。若子线程thread1thread2不调用join()函数,那么主线程MainThread2个子线程是并行执行任务的,2个子线程加上join()函数后,程序就变成顺序执行了。所以子线程用到join()的时候,通常都是主线程等到其他多个子线程执行完毕后再继续执行,其他的多个子线程并不需要互相等待。

    4.    守护线程

    在线程模块中,使用子线程对象用到join()函数,主线程需要依赖子线程执行完毕后才继续执行代码。如果子线程不使用join()函数,主线程和子线程是并行运行的,没有依赖关系,主线程执行了,子线程也在执行。

    在多线程开发中,如果子线程设定为了守护线程,守护线程会等待主线程运行完毕后被销毁。一个主线程可以设置多个守护线程,守护线程运行的前提是,主线程必须存在,如果主线程不存在了,守护线程会被销毁。

    在本例中创建1个主线程3个子线程,让主线程和子线程并行执行。内容如下:

     1 import time
     2 import threading
     3 
     4 
     5 def run(task_name):
     6     print("任务:", task_name)
     7     time.sleep(2)
     8 
     9     # 查看每个子线程
    10     print("{0} 任务执行完毕, 线程名称:{1}".format(
    11         task_name,
    12         threading.current_thread().getName()
    13     ))
    14 
    15 
    16 if __name__ == '__main__':
    17     start_time = time.time()
    18     for i in range(3):
    19         thr = threading.Thread(target=run, args=("task-{0}".format(i),))
    20         # 把子线程设置为守护线程
    21         thread.setDaemon(False)
    22         thread.start()
    23 
    24     # 查看主线程和当前活动的所有线程数
    25     print("{0}线程结束,当线程数量={1}".format(
    26         threading.current_thread().getName(),
    27         threading.active_count()
    28     ))
    29     print("消耗时间:", time.time() - start_time)

    运行脚本得到以下结果:

     

    从返回结果可以看出,当前的线程个数是4,线程个数=线程数 + 子线程数,在本例中有1个主线程和3个子线程。主线程执行完毕后,等待子线程执行完毕,程序才会退出。

    在本例的基础上,把所有的子线程都设置为守护线程。子线程变成守护线程后,只要主线程执行完毕,管子线程有没有执行完毕,程序都会退出。使用线程对象的setDaemon(True)函数来设置守护线程。

    import time
    import threading

    def run(task_name):
       
    print("任务:", task_name)
        time.sleep(
    2)
       
    print("{0} 任务执行完毕, 线程名称:{1}".format(
            task_name
    ,
           
    threading.current_thread().getName()
        ))

    if __name__ == '__main__':
        start_time = time.time()
       
    for i in range(3):
            thread = threading.Thread(
    target=run, args=("task-{0}".format(i),))
           
    # 把子线程设置为守护线程,在启动线程前设置
           
    thread.setDaemon(True)
            thread.start()
       
    # 查看主线程和当前活动的所有线程数
       
    print("{0}线程结束,当线程数量={1}".format(
            threading.current_thread().getName()
    ,
           
    threading.active_count()
        ))
       
    print("消耗时间:", time.time() - start_time)

    运行脚本得到以下结果。

     

    从本例的返回结果可以看出,主线程执行完毕后,程序不会等待守护线程执行完毕后就退出了。设置线程对象为守护线程,一定要在线程对象调用start()函数前设置。 

    5.  多线程的锁机制

    多线程编程访问共享变量时会出现问题,但是多进程编程访问共享变量不会出现问题。因为多进程中,同一个变量各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享。

    多个进程之间对内存中的变量不会产生冲突,一个进程由多个线程组成,多线程对内存中的变量进行共享时会产生影响,所以就产生了死锁问题,怎么解决死锁问题是本节主要介绍的内容。

    5.1. 变量的作用域

    一般在函数体外定义的变量称为全局变量,在函数内部定义的变量称为局部变量。全局变量所有作用域都可读,局部变量只能在本函数可读。函数在读取变量时,优先读取函数本身自有的局部变量,再去读全局变量。 
    内容如下。

    # 全局变量
    balance = 1


    def change():
       
    # 定义全局变量
       
    global balance
        balance =
    100
       
    # 定义局部变量
        num = 20
       
    print("change  balance to {0}".format(balance))


    if __name__ == "__main__":
       
    print("修改前的 balance={0}".format(balance))
        change()
       
    print("修改后的 balance={0}".format(balance))

    运行脚本得到以下结果:

     

    如果注释掉change()函数里的 global,则返回结果如下:

     

    在本例中在change()函数外定义的变量balance是全局变量,在change()函数内定义的变量num是局部变量,全局变量默认是可读的,可以在任何函数中使用,如果需要改变全局变量的值,需要在函数内部使用global定义全局变量,本例中在change()函数内部使用global定义全局变量balance,在函数里就可以改变全局变量了。

    在函数里可以使用全局变量,但是在函数里不能改变全局变量。想实现多个线程共享变量,需要使用全局变量。在方法里加上全局关键字 global定义全局变量,多线程才可以修改全局变量来共享变量。

    5.2. 多线程中的锁

    多线程同时修改全局变量时会出现数据安全问题,线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。在本例中我们生成2个线程同时修改change()函数里的全局变量balance时,会出现数据不一致问题。示例内容如下:

    import threading

    balance =
    100


    def change(num, counter):
       
    global balance
       
    for i in range(counter):
            balance += num
            balance -= num
           
    if balance != 100:
               
    # 如果输出这句话,说明线程不安全
               
    print("balance=%d" % balance)
               
    break


    if
    __name__ == "__main__":
        thread_list = []
       
    for i in range(5):
            thread = threading.Thread(
    target=change, args=(100, 500000), name='t1')
            thread.start()
            thread_list.append(thread)
        [t.join()
    for t in thread_list]
       
    print("{0} 线程结束".format(threading.current_thread().getName()))

    运行以上脚本,当5个线程运行次数达到500000次时,会出现以下结果:

     

    在本例中定义了一个全局变量balance,初始值为100,当启动2个线程后,先加后减,理论上balance应该为100。线程的调度是由操作系统决定的,当线程交替执行时,只要循环次数足够多,balance结果就不一定是100了。从结果可以看出,在本例中线程t1t2同时修改全局变量balance时,会出现数据不一致问题。

    注意

    在多线程情况下,所有的全局变量有所有线程共享。所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

    在多线程情况下,使用全局变量并不会共享数据,会出现线程安全问题。可以采用加锁机制,当一个线程访问该类的某个数据时,对数据进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致,在单线程运行时没有代码安全问题。但在多线程情况下,才会出现安全问题。

    针对线程安全问题,需要使用“互斥锁”,就像数据库里操纵数据一样,也需要使用锁机制。某个线程要更改共享数据时,先将其锁定,此时资源的状态为锁定,其他线程不能更改;直到该线程释放资源,将资源的状态变成非锁定,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

    互斥锁的核心代码如下: 

    1 # 创建锁
    2 mutex = threading.Lock()
    3 # 锁定
    4 mutex.acquire()
    5 # 释放
    6 mutex.release()

    如果要确保balance计算正确,使用threading.Lock()来创建锁对象lock,把 lock.acquire()lock.release()加在同步代码块里,本例的同步代码块就是对全局变量balance进行先加后减操作。

    当某个线程执行change()函数时,通过lock.acquire()获取锁,那么其他线程就不能执行同步代码块了,只能等待知道锁被释放了,获得锁才能执行同步代码块。由于锁只有一个,无论多少线程,同一个时刻最多只有一个线程持有该锁,所以修改全局变量balance不会产生冲突。改良后的代码内容如下:

    import threading

    balance =
    100
    lock = threading.Lock()


    def change(num, counter):
       
    global balance
       
    for i in range(counter):
           
    # 先要获取锁
           
    lock.acquire()
            balance += num
            balance -= num
           
    # 释放锁
           
    lock.release()

           
    if balance != 100:
               
    # 如果输出这句话,说明线程不安全
               
    print("balance=%d" % balance)
               
    break


    if
    __name__ == "__main__":
        thread_list = []
       
    for i in range(5):
            thread = threading.Thread(
    target=change, args=(100, 500000), name='t1')
            thread.start()
            thread_list.append(thread)
        [t.join()
    for t in thread_list]
       
    print("{0} 线程结束".format(threading.current_thread().getName()))

    在本例中多个线程同时运行lock.acquire()时,只有一个线程能成功的获取锁,然后执行代码,其他线程就继续等待直到获得锁位置,这时候就不会出现线程不一致的问题:

     

    获得锁的线程用完后一定要释放锁,否则其他线程就会一直等待下去,成为死线程。

    在运行上面脚本就不会产生输出信息,证明代码是安全的。把 lock.acquire()lock.release()加在同步代码块里,还要注意锁的力度不要加的太大了。第一个线程只有运行完了,第二个线程才能运行,所以锁要在需要同步代码里加上。

    6.参考文档

    Python中文社区Python线程5分钟完全解读

     

  • 相关阅读:
    How to create jar for Android Library Project
    Very large tabs in eclipse panes on Ubuntu
    64bit Ubuntu, Android AAPT, R.java
    Linux(Ubuntu)下如何安装JDK
    Configure xterm Fonts and Colors for Your Eyeball
    建立、配置和使用Activity——启动其他Activity并返回结果
    建立、配置和使用Activity——使用Bundle在Activity之间交换数据
    建立、配置和使用Activity——启动、关闭Activity
    建立、配置和使用Activity——Activity
    异步任务(AsyncTask)
  • 原文地址:https://www.cnblogs.com/qianyeliange/p/11075051.html
Copyright © 2020-2023  润新知