zoukankan      html  css  js  c++  java
  • 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分钟完全解读

     

  • 相关阅读:
    FJoi2017 1月21日模拟赛 comparison(平衡树+thita重构)
    juruo的刷题&博文祭
    [bzoj4247][挂饰] (动规+排序)
    FJoi2017 1月20日模拟赛 直线斯坦纳树(暴力+最小生成树+骗分+人工构造+随机乱搞)
    FJoi2017 1月20日模拟赛 交错和(等差数列+rmq)
    FJoi2017 1月20日模拟赛 恐狼后卫(口糊动规)
    【spoj 5971】lcmsum
    【bzoj 4025 改编版】graph
    【CF 718C】fibonacci
    【CF 482E】ELCA
  • 原文地址:https://www.cnblogs.com/qianyeliange/p/11075051.html
Copyright © 2011-2022 走看看