Python笔记_第四篇_高阶编程_进程、线程、协程_2.线程

1. 线程概述

  在一个进程的内部,要同时干多件事情,就需要同时运行“多个子任务”,我们把进程内的这些“子任务”叫做线程。也就说线程是进程成的子任务。
  线程通常叫做情景的进程。线程是通过向内侧控件的并发执行的多任务。每一个线程都共享一个进程的资源。

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

  线程有两个模块:

    _thread模块:低级模块(现在很少用了)

    threading模块:高级模块,是对_thread的进一步封装。

2. 启动一个线程

  先上代码:

import threading,time

a = 10
def run(num):
    print("子线程(%s)开始" % threading.current_thread().name)

    # 实现线程的功能
    print("打印",num)
    time.sleep(2)
    print(a)

    print("子线程(%s)结束" % threading.current_thread().name)

if __name__ == '__main__':
    # 任何进程默认就会启动一个线程,成为主线程,主线程可以启动新的子程序
    # current_thread().name 返回当前线程的实例的名称
    print("主程序(%s)启动" % (threading.current_thread().name))

    # 创建子进程
    t = threading.Thread(target=run,name="runThread",args=(1,))
    t.start()
    # 等待线程结束
    t.join()

    print("主程序(%s)结束" % (threading.current_thread().name))


主程序(MainThread)启动
子线程(runThread)开始
打印 1
10
子线程(runThread)结束
主程序(MainThread)结束

   说明1:任何进程默认就会启动一个线程,成为主线程,主线程可以启动新的子程序。

  说明2:current_thread().name方法是返回一个档期线程的实例名称。

3. 线程间的数据共享存在的问题

   多线程和多进程最大的不同在于:多进程中,同一个变量,格子有一份拷贝存在在每个进程中,互不影响,而多线性中,所有变量都由所有线程共享,所以任何一个变量都可以被任意一个线程修改,因此线程间的通向数据存在的最大危险在于多个线程同时修改一个变量,容易把内存内容改乱了。  

  实例1:我们以加一个减一个的方式不停循环一个很大的数,我们发现,数越大到最后就出现了混乱,而不是还是等于0的状态。

import threading

num = 0

def run(n):
    global num
    for i in range(10000000):
        num = num + n
        num = num - n


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

    print("num = ", str(num))
# num =  -72

   说明1:这种问题的存在,与进程不一样,而且为了达到相互的交替执行,而不出现随机性的混乱情况。我们加入了“锁”的概念。

4. 线程锁解决数据混乱

  在两个线程同时工作的时候,我们为了防止数据可能出现的混乱,加入了线程锁。

  先上代码:

import threading

# 创建锁对象
lock = threading.Lock()

num = 0

def run(n):
    global num
    for i in range(10000000):
        # 锁
        # 确保了这段代码由一个线程从头到尾的完成执行
        # 阻止了多线程的并发执行,包含锁的某段代码,实际上只能以单线程模式执行,所以效率大大的降低了。
        # 由于可以存在多个锁,不同线程可以持有不同的锁,并试图获取其他的锁,这样锁来锁去的,可能造成死锁,导致多个线程挂起。只能靠操作系统强制终止。
        lock.acquire()
        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()
    t1.join()

    print("num = ", str(num))

# num =  0
# 上锁后成功。

  说明:上锁我们用的方法是:lock.acquire()需要一把锁,解锁我们用的方法是:lock.release()释放这把锁。

  说明2:这样确保了这段代码由一个线程从头到尾的完成执行。阻止了多线程并发(前面提到过)的执行,包含锁的代码,实际上只能单线程执行,所以效率大大降低了。

  说明3:由于可以存在多个锁,不能线程可以持有不同的锁,并试图获取其他的锁,这样锁来锁去,可能造成死锁,导致多个线程挂起。只能靠系统强制终止。

  说明4:因此我们采用try...finally异常的代码处理。这样写稍微麻烦一些,之前我们在学文件的io操作的时候,用过一个with的方式,用这种方式文件也不需要再关闭。因此我们用另外一种写法,让锁也不用关闭了,他会自动去关闭。

  代码:

# 锁的第二种写法:
import threading

# 创建锁对象
lock = threading.Lock()

num = 0

def run(n):
    global num
    for i in range(10000000):
        # 与上面代码功能相同,with lock可以自动上锁与解锁。
        with lock:
            num = num + n
            num = num - n

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

    print("num = ", str(num))

# num =  0
# 上锁后成功。

5. threadLocal

  我们发现通过上锁开锁的这样方式容易造成程序错误导致死机的情况。我们可以采取用本地分配线程变量的方式,让这些变量的计算有自己的独立运行线程也可以实现这样的功能。因此我们把下面代码中的num变量交送给“local”的线程变量,让local在每个线程中独立去交互数据。

  代码:

import threading

num = 0
# 创建一个全局的ThreadLocal对象
# 每个线程有的独立存储空间
# 每个线程对ThreadLocal对象,而且互不影响。
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(1000000):
        run(local.x,n)
    print("%s--%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()
    t1.join()

# Thread - 1 - -0
# Thread - 2 - -0
# 所用:为每个线程绑定一个数据库连接,或者是HTTP请求,或者是用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便的访问这些资源。

 

6. 信号量控制线程数量(Semaphore)

  前面我们发现了,我们手工的去制作一个线程,但是我们其实不知道当前有多少个线程,这些线程的多少我们可以叫做信号量,我们可以通过信号量的多少来控制线程。

  代码:

import threading,time

# 控制线程的数量,让多少个线程一起执行
sem = threading.Semaphore(3)

def run():
    with sem:
        for i in range(5):
            print("%s - %d" %(threading.current_thread().name,i))
            time.sleep(1)
if __name__ == '__main__':
    for i in range(5):
        threading.Thread(target=run).start()

Thread-1 - 0
Thread-2 - 0
Thread-3 - 0
Thread-3 - 1
Thread-2 - 1
Thread-1 - 1
Thread-1 - 2Thread-2 - 2

Thread-3 - 2
Thread-3 - 3
Thread-1 - 3
Thread-2 - 3
Thread-2 - 4
Thread-1 - 4
Thread-3 - 4
Thread-5 - 0Thread-4 - 0

Thread-4 - 1
Thread-5 - 1
Thread-4 - 2Thread-5 - 2

Thread-5 - 3
Thread-4 - 3
Thread-4 - 4Thread-5 - 4

  说明1:控制线程的数量,让多少个线程一起执行。

7. 凑够一定数量才能一起执行(Barrier):

  代码:

import threading,time

# 凑够多少个线程一起执
bar = threading.Barrier(4)

def run():
    print("%s - start" %(threading.current_thread().name))
    time.sleep(1)
    bar.wait()
    print("%s - end" % (threading.current_thread().name))
    # 如果凑不够一定数量不会执行,一直在那里等着

if __name__ == '__main__':
    for i in range(6):
        threading.Thread(target=run).start()

  说明1:我们发现程序一直停在那儿等待凑够一定的线程数量才能一起执行。其实这种方式在多数去情况下是用不到的。

8. 定时线程(Timer)

  我们之前又给线程上锁,又分配线程量的情况,我们如果想给线程定时去开启,需要用到Timer的方法。

  代码:

import threading


def run():
    print("Thomas is a good man")

# 延时执行线程
t = threading.Timer(5,run)

t.start()
t.join()

print("父线程结束")
# Thomas is a good man
# 父线程结束

  说明1:这样我们等待了5秒钟,线程才开始执行程序。

9. 线程通信(event)

  前面我们使用手工的方式进行进行线程之间上锁解锁之间的方式进行性通讯的。我们可以调用线程的事件(因为线程行动本身就是一个事件),让上一个线程等待时间触发。

  代码:

import threading,time


def func():
    # 事件对象
    event = threading.Event()
    def run():
        for i in range(5):
            # 阻塞,等待时间的触发
            event.wait()
            # clear是重置的意思
            event.clear()
            print("Thomas is a good man!!%d",i)

    threading.Thread(target=run).start()
    return event


e = func()
# 触发时间
for i in range(5):
    e.set()
    time.sleep(2)

10. 线程间的通信(Queue队列方式)

  代码:

import threading,queue,time,random


# 生产者
def producer(id,q):
    while True:
        num = random.randint(0,10000)
        q.put(num)
        print("生产者%d生产了%d的数据放入了队列" %(id,num))
        time.sleep(3)
    # 任务完成
    q.task_done()


# 消费者
def customer(id,q):
    while True:
        item = q.get()
        if item is None:
            break
        print("消费者%d消费了%d数据" %(id,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=customer,args=(i,q)).start()

11. 线程的调度(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()
            cond.notify()


def run2():
    with cond:
        for i in range(1,10,2):
            print(threading.current_thread().name,i)
            time.sleep(1)
            cond.notify()
            cond.wait()


threading.Thread(target=run1).start()
threading.Thread(target=run2).start()

  说明:这里有一点注意的是cond.wait控制等待,然后要通知下一个cond.notify这样才可以。

原文地址:https://www.cnblogs.com/noah0532/p/10939220.html