网络编程三:多线程、锁、threadlocal、事件、队列

一、多线程

在一个进程的内部,要同时做多件事,就需要同时运行多个“子任务”,把进程内的这些“子任务”叫做线程。

线程,是共享内存空间的并发执行的多任务。

线程,是在进程里开启的,每一个线程都共享同一个进程的资源;线程是没有堆栈的,进程才有。

线程是最小的执行单元,进程由至少一个线程组成。如何调度进程和线程,完全由操作系统决定的,程序自己不能决定什么时侯执行,执行多长时间。

线程的模块:

  • _thread模块,低级模块
  • threading模块,高级模块,对_thread进行了封装
  • threadingPool
import threading, time
def run():
    thread_name = threading.current_thread().name
    print("子线程%s开始" % thread_name)
    time.sleep(2)
    print("子线程%s结束" % thread_name)
if __name__ == '__main__':
    # 任何进程默认就会启动一个线程,称为主线程
    # 主线程,可以启动新的子线程
    # current_thread():返回当前线程的实例
    print("主线程%s启动" % (threading.current_thread().name))

    # 创建子线程
    t = threading.Thread(target=run, name="rooThread")
    t.start()
    # 主线程,不会等待子线程的结束后,才结束
    print("主线程%s结束" % (threading.current_thread().name))
主线程MainThread启动
子线程rooThread开始
主线程MainThread结束
子线程rooThread结束

 要保证所有子线程结束后,主线程才结束,需要子线程调用join()方法

if __name__ == '__main__':
    print("主线程%s启动" % (threading.current_thread().name))
    t = threading.Thread(target=run, name="rooThread")
    t.start()
    t.join()
    # 要保证所有子线程结束后,主线程才结束,需要子线程调用join()方法
    print("主线程%s结束" % (threading.current_thread().name))

 主线程MainThread启动

子线程rooThread开始
子线程rooThread结束
主线程MainThread结束

 在进程间,全局变量是不能共享的;但在线程间,共享数据。

在多进程中,同一个全局变量,各自有一份拷贝在每个进程中,互不影响。

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

示例:假如只有一个线程时,run方法的结果,应该是0;如果同时启用了多个线程,则可能将变量num的内容修改乱了

import threading, time
num = 0
def run(n):
    global num
    for i in range(10000000):
        # 假设此时num为0;
        # 线程1: 6 = 0 + 6
        # 线程2: 9 = 0 + 9
        # 线程1:应该是0=6 - 6,假设在线程1减法之前,线程2可能修改了num为9,此时3 = 9 - 6
        # 线程2:应该是0=9 - 9,假设在线程2减法之前,线程1可能修改了num为3,此时-3 = 3 - 6
        # 最终num的值,不一定为0,不可预知
        num  = num + n
        num = num - n
if __name__ == '__main__':
    print("主线程%s启动" % (threading.current_thread().name))
    t1 = threading.Thread(target=run, args=(6,))
    t2 = threading.Thread(target=run, args=(9,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("num =", num)
    print("主线程%s结束" % (threading.current_thread().name))

怎么解决,数据混乱的问题?使用线程间的同步

 二、线程间的同步之一:锁

import threading, time
num = 0
# 锁对象
lock = threading.Lock()
def run(n):
    global num
    for i in range(10000000):
        # 对以下可能使变量改变的代码,加锁
        lock.acquire()
        # 加锁的地方,尽量加上tr...finally,以避免加锁的代码段出现异常
        try:
            num  = num + n
            num = num - n
        finally:
            # 修改完变量之后,一定要释放锁
            lock.release()
if __name__ == '__main__':
    t1 = threading.Thread(target=run, args=(6,))
    t2 = threading.Thread(target=run, args=(9,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("num =", num)

 

线程锁,确保了加锁和释放锁之间的代码,只能由一个线程从头到尾的完整执行。

加锁和释放锁之间的代码,阻止了多线程的并发执行,这段代码以单线程模式执行,所以整个程序的 vgy效率大大的降低了。

没加锁的代码部分,不受影响,还是并发执行。

死锁:

由于可以存在多个锁,不同线程可以持有不同的锁,并试图获取其它的锁,可能造成死锁,导致多个线程挂起。只能靠操作系统强制终止。

避免死锁的办法一:try...finally或with lock,保证每次加锁都一定会释放锁即可。如以上示例。

避免死锁的办法二:各个线程不共享变量,各自修改自己的独立的私有变量-----创建一个全局的threading.local()对象;每个线程对此对象都可以读写,但是互不影响;类似于多进程间的变量。

示例:使用threading.local()保证不共享变量

import threading, time
num = 0
# local对象
local = threading.local()
def run(n):
    # 每个线程都有local.x,就是线程的局部变量
    local.x = num
    for i in range(10000000):
        local.x = local.x + n
        local.x = local.x - n
    print("%s: num = %d" % (threading.current_thread().name, local.x))

if __name__ == '__main__':
    t1 = threading.Thread(target=run, args=(6,))
    t2 = threading.Thread(target=run, args=(9,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("num =", num)

 

Thread-1: num = 0
Thread-2: num = 0
num = 0

 

使用local对象后,各个线程修改自己的私有变量,这样既避免了线程间变量的同步,而且因为没有锁,使得全部代码是并发执行。

将local.x对象,和业务方法run拆离:

import threading, time
num = 0
# local对象
local = threading.local()
def run(x, n):
    x = x + n
    x = x - n
def func(n):
    # 每个线程都有local.x,就是线程的局部变量
    local.x = num
    for i in range(10000000):
        run(local.x, n)
    print("%s: num = %d" % (threading.current_thread().name, local.x))

if __name__ == '__main__':
    t1 = threading.Thread(target=func, args=(6,))
    t2 = threading.Thread(target=func, args=(9,))
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print("num =", num)

 

线程1和线程2的local.x,都是局部变量,互不影响。

local对象的通常用法:为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便的访问这些资源,而且各个线程之间的这些变量互不相同,都只是自己线程的局部变量,互不影响。

 

三、线程间的同步之二:事件

import threading, time
def run():
    event = threading.Event()
    def _run():
        for i in range(5):
            # 阻塞,等待事件触发
            event.wait()
            # 重置事件。如果不重置事件,那么事件只会阻塞一次。
            event.clear()
            print("run %d......" % i)
    threading.Thread(target=_run).start()
    return event
# 如果没事件触发,执行以下函数,run中的线程将一线阻塞
event = run()
# 触发事件
for i in range(5):
    time.sleep(2)
    # 触发事件
    event.set()

 四、线程间的同步之三:以队列实现生产者与消费者模型

import threading, time, queue, random
# 生产者
def producer(i, q):
    while True:
        num = random.randint(0, 1000)
        q.put(num)
        print ("生产者%d生产了%d数据,放入队列" % (i, num))
        time.sleep(3)
    # 队列完成
    q.task_done()
# 消费者
def comsumer(i, q):
    while True:
        item = q.get()
        if item is None:
            break
        print ("消费者%d消费了%d数据" % (i, item))
        time.sleep(2)
    # 任务完成
    q.task_done()

if __name__ == '__main__':
    # 消息队列
    q = queue.Queue()
    # 启动生产者
    for i in range(4):
        threading.Thread(target=producer, args=(i, q)).start()
    #启动消费者
    for i in range(3):
        threading.Thread(target=comsumer, args=(i, q)).start()
生产者0生产了433数据,放入队列
生产者1生产了242数据,放入队列
生产者2生产了36数据,放入队列
生产者3生产了689数据,放入队列
消费者0消费了433数据
消费者1消费了242数据
消费者2消费了36数据
消费者0消费了689数据
生产者0生产了432数据,放入队列
生产者1生产了425数据,放入队列
消费者2消费了432数据
消费者1消费了425数据

 

以上代码,以队列的形式实现了生产者与消费者。但是生产者与消费者的线程调度顺序,不可控。

再来看个没有线程调度的例子。启动两个线程,如果线程的执行是有顺序的,那么应该0-10依次打印。

import threading, time
def run1():
    for i in range(0, 10, 2):
        print(threading.current_thread().name, i)
        time.sleep(1)
def run2():
    for i in range(1, 10, 2):
        print(threading.current_thread().name, i)
        time.sleep(1)
threading.Thread(target=run1).start()
threading.Thread(target=run2).start()

 

实际上,线程的执行没有顺序,打印结果如下:

Thread-1 0
Thread-2 1
Thread-2 3
Thread-1 2
Thread-2 5
Thread-1 4
Thread-2 7
Thread-1 6
Thread-2 9
Thread-1 8

 

线程调度:使用线程条件变量实现--cond = threading.Condition()

import threading, time
cond = threading.Condition()
def run1():
    with cond:
        for i in range(0, 10, 2):
            print(threading.current_thread().name, i)
            time.sleep(1)
            cond.wait()  # 1.线程等待通知:生产者线程run1生产一条数据之后,等待消费者run2的线程发送通知
            cond.notify()  # 4.线程通知:通知消费者线程run2,可以继续消费数据了
def run2():
    with cond:
        for i in range(1, 10, 2):
            print(threading.current_thread().name, i)
            time.sleep(1)
            cond.notify()  # 2.线程通知:消费者线程run2消费一条数据之后,通知等待的线程生产者run1可以往后执行了
            cond.wait()   # 3.线程等待:生产者线程run1需要使用wait线程等待通知后,才可以生产下一条数据
                        # 同样,消费者线程run2也需要一条数据一条数据的消费,
            # 因此,消费者线程run2也需要等待生产者线程run1的通知,才可以消费下一条数据
threading.Thread(target=run1).start()
threading.Thread(target=run2).start()
Thread-1 0
Thread-2 1
Thread-1 2
Thread-2 3
Thread-1 4
Thread-2 5
Thread-1 6
Thread-2 7
Thread-1 8
Thread-2 9

 

多任务的实现原理:

通常我们会设计master-worker模型,一个master负责分配任务,多个worker负责执行任务。

多进程与多线程的比较:

多进程使用master-worker -- 主进程就是master,由主进程fork的进程就是子进程worker

多进程使用master-worker的优点:稳定性高,一个子进程崩溃了,不会影响主进程和其它子进程;当然主进程挂了,所有子进程也跟着挂了,但是主进程master只负责分配任务,挂掉的概率低。

多进程使用master-worker的缺点:创建进程的代价大(在linux下不是特别大,在windows下较大)、操作系统能同时运行的进程数受CPU和内存的限制

多线程使用master-worker --- 主线程就是master,由主线程创建的子线程就是worker

多线程使用master-worker的优点:同样稳定性高;在linux下,多线程模式通常比多进程快一点点,快不了多少;在windows下,多进程的创建与销毁开销大,因此在windows下多线程的效率将比多进程快得多。

多线程使用master-worker的缺点:任何一个线程挂掉都可能造成整个进程崩溃,因为所有线程是共享进程的内存,而不像多进程的进程之间不共享内存。

计算密集型与IO密集型:

对于要进行大量的计算、视频解码等,全靠CPU的运算能力。这种计算密集型的任务,应减少多任务的数量;因为任务越多,花在切换任务的时间就越多,CPU执行任务的效率就越低;所以计算密集型的多任务,应当乖于CPU的核心数。

对于涉及到网络、磁盘等IO密集型任务,CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有限度。

常见的大部分任务都是IO密集型任务,如WEB应用。

 

posted on 2018-10-28 17:23  myworldworld  阅读(263)  评论(0)    收藏  举报

导航