Python多线程编程-Threading库

1.概述

线程(英文:thread),台湾地区译为执行绪(英文:thread of execution)、绪程,操作系统技术中的术语,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是行程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并行多个线程,每条线程并行执行不同的任务。

线程,有时被称为轻量级进程(Lightweight Process,LWP):

线程,是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性:

1)轻型实体
  线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源,比如,在每个线程中都应具有一个用于控制线程运行的线程控制块TCB,用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
2)独立调度和分派的基本单位。
  在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小。
3)可并发执行。
  在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行。
4)共享进程资源。
  在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。

线程与进程的区别可以归纳为以下几点:

1)地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。
2)通信:进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
3)调度和切换:线程上下文切换比进程上下文切换要快得多。
4)在多线程OS中,进程不是一个可执行的实体。

多进程与多线程

2.编程实战-数据共享机制

共享机制的弊端:

# Authors:xiaobei
# ###数据共享机制###

import threading,time
# _thread 低级(接近底层,c语言)
# threading 高级(对_thread进行封装)
num = 100
def func(var):
    global num
    print("[子线程]:%s开始" % threading.current_thread().name)
    for i in range(1000000):
        num = num + var
        num = num - var
    print("[子线程]:%s结束" % threading.current_thread().name)
if __name__ =="__main__":
    #任何进程默认启动一个线程,称为主线程,主线程可以启动新的子线程
    #current_thread():返回当前线程实例
    print("[主线程]:(%s)启动" % threading.current_thread().name)
    th1 = threading.Thread(target=func,name="th1",args=(5,))
    th2 = threading.Thread(target=func,name="th2",args=(9,))
    th1.start()
    th2.start()
    th1.join()
    th2.join()
    print("[主线程]:(%s)结束" % threading.current_thread().name)
    print("最后结果为:",num)
# 由于不同线程共享数据,在不同线程处理数据时,容易出现数据错误

运行结果理论上为100,但实际上·是不确定的。因为不同的进程之间存在数据共享,产生干扰,导致计算失误·。这种情况在计算量大的情况下尤为明显·。

线程池示意图:

线程池

3.线程锁解决数据共享混乱(Lock)

线程锁解决不同线程之间数据共享产生的数据混乱:

  1. 锁确保了一段代码只能由一个线程从头至尾完整执行
  2. 阻止了多线程之间的并发执行,包含锁的代码段只能以单线程模式执行,所以效率大大降低
  3. 由于可以存在多个锁,不同线程的有不同的锁,并试图获取其它的锁可能造成死锁,导致多个线程挂起,只能靠操作系统强行终止
# Authors:xiaobei
# ###线程锁实战###

import threading,time
# _thread 低级(接近底层,c语言)
# threading 高级(对_thread进行封装)
#创建锁对象
lock = threading.Lock()
num = 100
def func(var):
    global num
    print("[子线程]:%s开始" % threading.current_thread().name)
    for i in range(1000000):
        '''方法一:'''
        # # 锁住
        # lock.acquire()
        # try:
        #     num = num + var
        #     num = num - var
        # finally:
        #     #释放锁,不然该线程会锁死
        #     lock.release()
        '''方法二:'''
        # 锁住
        # 自动释放锁,更安全
        with lock:
            num = num + var
            num = num - var
    print("[子线程]:%s结束" % threading.current_thread().name)
if __name__ =="__main__":
    #任何进程默认启动一个线程,称为主线程,主线程可以启动新的子线程
    #current_thread():返回当前线程实例
    print("[主线程]:(%s)启动" % threading.current_thread().name)
    th1 = threading.Thread(target=func,name="th1",args=(5,)) #name参数可以修改子线程名称,不然默认是Thread-[序号]
    th2 = threading.Thread(target=func,name="th2",args=(9,))
    th1.start()
    th2.start()
    th1.join()
    th2.join()
    print("[主线程]:(%s)结束" % threading.current_thread().name)
    print("最后结果为:",num)
    #锁确保了一段代码只能由一个线程从头至尾完整执行
    #阻止了多线程之间的并发执行,包含锁的代码段只能以单线程模式执行,所以效率大大降低
    #由于可以存在多个锁,不同线程的有不同的锁,并试图获取其它的锁可能造成死锁,导致多个线程挂起,只能靠操作系统强行终止
    

4.线程之间数据不共享(Location)

创建一个全局的ThreadLocal对象,此方法使每个线程都开辟有独立空间,每个线程对ThreadLocal对象都可读写,但是互不影响。

# Authors:xiaobei
# ###线程之间数据不共享(Location)###

import threading,time
# _thread 低级(接近底层,c语言)
# threading 高级(对_thread进行封装)

# 此方法使每个线程都开辟有独立空间
# 每个线程对ThreadLocal对象都可读写,但是互不影响
# 创建一个全局的ThreadLocal对象
local = threading.local()
num = 100
def run(num,var):
    num = num + var
    num = num - var
def func(var):
    # 每个线程都有local.x,就是线程的局部变量
    local.x = num
    print("[子线程]:%s开始" % threading.current_thread().name)
    for i in range(1000000):
        run(local.x,var)
    print("[子线程]:%s结束" % threading.current_thread().name)
if __name__ =="__main__":
    #任何进程默认启动一个线程,称为主线程,主线程可以启动新的子线程
    #current_thread():返回当前线程实例
    print("[主线程]:(%s)启动" % threading.current_thread().name)
    th1 = threading.Thread(target=func,name="th1",args=(5,))
    th2 = threading.Thread(target=func,name="th2",args=(9,))
    th1.start()
    th2.start()
    th1.join()
    th2.join()
    print("[主线程]:(%s)结束" % threading.current_thread().name)
    print("最后结果为:",num)
# 为每一个线程绑定一个数据库链接、HTTP请求,用户身份信息等
# 这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源

5.Semaphore控制线程数量

# Authors:xiaobei
# ###Semaphore控制线程数量###

import threading,time
sem = threading.Semaphore(3)
def func():
    with sem:
        for i in range(5):
            print("%s"%threading.current_thread().name,i)
            time.sleep(2)
if __name__ == "__main__":
    for i in range(5):
        threading.Thread(target=func).start()

6.Barrier凑够一定数量才运行的线程

# Authors:xiaobei
# ###Barrier凑够一定数量才运行的线程###

import threading,time
# 创建全局Barrier对象
bar = threading.Barrier(3)
def func():
    print("%s 开始"%threading.current_thread().name)
    time.sleep(1)
    # 在这里等凑够一定的线程数才继续往下执行
    bar.wait()
    print("%s 结束"%threading.current_thread().name)
if __name__ == "__main__":
    for i in range(10):
        threading.Thread(target=func).start()

7.Timer定时线程

# Authors:xiaobei
# ###Timer定时线程###

import threading,time
def func():
    print("子进程(%s)启动"%threading.current_thread().name)
if __name__ == "__main__":
    print("---父进程%s启动---"%threading.current_thread().name)
    start = time.time()
    # 延时执行线程
    t = threading.Timer(5,func)
    t.start()
    t.join()
    end = time.time()
    print("子进程延时:%.2f s"%(end-start))
    print("---父进程%s结束---"%threading.current_thread().name)

8.Event线程通信

# Authors:xiaobei
# ###Event线程通信###

import threading,time
def func():
    # 事件对象
    event = threading.Event()
    def run():
        for i in range(5):
            # 阻塞,等待事件触发
            event.wait()
            # 重置
            event.clear()
            print("事件%d触发"%i)
    t = threading.Thread(target=run)
    t.start()
    return event
e = func()
# 触发事件
for i in range(5):
    time.sleep(2)
    e.set()