线程

导读目录

1 线程介绍

1.1 有了进程为什么要有线程

  进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。

  仔细观察就会发现进程还是有很多缺陷的,主要体现在两点上:

  • 进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。

  • 进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。

#    举个现实的例子:
    如果把我们上课的过程看成一个进程的话,那么我们要做的是耳朵听老师讲课,手上还要记笔记,脑子还要思考问题,这样才能高效的完成听课的任务。而如果只提供进程这个机制的话,上面这三件事将不能同时执行,同一时间只能做一件事,听的时候就不能记笔记,也不能用脑子思考,这是其一;
    如果老师在黑板上写演算过程,我们开始记笔记,而老师突然有一步推不下去了,阻塞住了,他在那边思考着,而我们呢,也不能干其他事,即使你想趁此时思考一下刚才没听懂的一个问题都不行,这是其二。
举例子

1.2 线程的出现

  随着计算机技术的发展,进程出现了很多弊端:
  一:是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入 轻型进程 
  二:是由于对称多处理机(SMP)出现, 可以满足多个运行单位 ,而多个进程并行开销过大。
  因此在80年代,出现了 能独立运行的基本单位 ——线程(Threads) 
# 进程是资源分配的最小单位,线程是CPU调度的最小单位.
# 每一个进程中至少有一个线程。

1.3 进程和线程的关系

  通过漫画了解进程和线程

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

1.4 线程的特点

  在多线程的操作系统中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
  1)轻型实体
  线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
  线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
TCB包括
  2)独立调度和分派的基本单位
  在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
  3)共享进程资源
  线程在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的进程id,这意味着,线程可以访问该进程的每一个内存资源;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
  4)可并发执行
  在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

1.5 内存中的线程

# 多个线程共享同一个进程的地址空间中的资源,是对一台计算机上多个进程的模拟,有时也称线程为轻量级的进程。多线程的运行,是cpu在多个线程之间的快速切换。

# 不同的进程之间是充满敌意的,彼此是抢占、竞争cpu的关系,如果迅雷会和QQ抢资源。#    同一个进程是由一个程序员的程序创建,所以同一进程内的线程是合作关系,一个线程可以访问另外一个线程的内存地址,大家都是共享的。

# 类似于进程,每个线程也有自己的堆栈,不同于进程,线程库无法利用时钟中断强制线程让出CPU,可以调用thread_yield运行线程自动放弃cpu,让另外一个线程运行。

# 线程通常是有益的,但是带来了不小程序设计难度,线程的问题是:
    1. 父进程有多个线程,那么开启的子线程是否需要同样多的线程
    2. 在同一个进程中,如果一个线程关闭了文件,而另外一个线程正准备往该文件内写内容呢?

    因此,在多线程的代码中,需要更多的心思来设计程序的逻辑、保护程序的数据。

2 在python中使用线程

2.1 threading模块

  multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性。

2.2 线程的创建

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    print('主线程')
线程的创建方式1
from threading import Thread
import time
class Sayhi(Thread):
    def __init__(self,name):
        super().__init__()
        self.name=name
    def run(self):
        time.sleep(2)
        print('%s say hello' % self.name)


if __name__ == '__main__':
    t = Sayhi('egon')
    t.start()
    print('主线程')
线程的创建方式2 

2.3 多线程与多进程

from threading import Thread
from multiprocessing import Process
import os

def work():
    print('hello',os.getpid())

if __name__ == '__main__':
    #part1:在主进程下开启多个线程,每个线程都跟主进程的pid一样
    t1=Thread(target=work)
    t2=Thread(target=work)
    t1.start()
    t2.start()
    print('主线程/主进程pid',os.getpid())

    #part2:开多个进程,每个进程都有不同的pid
    p1=Process(target=work)
    p2=Process(target=work)
    p1.start()
    p2.start()
    print('主线程/主进程pid',os.getpid())

#运行结果:
hello 4808
hello 4808
主线程/主进程pid 4808

主线程/主进程pid 4808
hello 14980
hello 10696
pid比较
from threading import Thread
from multiprocessing import Process
import time

def work():
    print('hello')

if __name__ == '__main__':
    #在主进程下开启线程
    start_time = time.time()
    t=Thread(target=work)
    t.start()
    print('在主进程下开启线程用时:',time.time()-start_time)

    #在主进程下开启子进程
    start_time = time.time()
    t=Process(target=work)
    t.start()
    print('在主进程下开子进程用时:', time.time() - start_time)

# 运行结果:
hello
在主进程下开启线程用时: 0.0020072460174560547
在主进程下开子进程用时: 0.020978689193725586
hello
开启效率的比较
from  threading import Thread
from multiprocessing import Process
import os
def work():
    global n
    n=0

if __name__ == '__main__':
    n=100
    p=Process(target=work)
    p.start()
    p.join()
    print('主进程中的n:',n)   #毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100

    n=1
    t=Thread(target=work)
    t.start()
    t.join()
    print('主线程中的n:',n)  #查看结果为0,因为子进程p已经将自己的全局的n改成了0,同一进程内的线程之间共享进程内的数据

#运行结果:
主进程中的n: 100
主线程中的n: 0
数据共享比较

2.4 Thread类的其他方法

Thread实例对象的方法
  # isAlive(): 返回线程是否活动的。
  # getName(): 返回线程名。
  # setName(): 设置线程名。

threading模块提供的一些方法:
  # threading.currentThread(): 返回当前的线程变量。
  # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
import threading
from threading import Thread
import os

def work():
    import time
    time.sleep(3)
    print(threading.current_thread().getName())

if __name__ == '__main__':
    #在主进程下开启线程
    t=Thread(target=work)
    t.start()

    print(threading.current_thread().getName())
    print(threading.current_thread()) #主线程
    print(threading.enumerate())    #连同主线程在内有两个运行的线程
    print(threading.active_count())
    print('主线程/主进程')
    print()

# 运行结果:
MainThread
<_MainThread(MainThread, started 15140)>
[<_MainThread(MainThread, started 15140)>, <Thread(Thread-1, started 15152)>]
2
主线程/主进程

Thread-1
方法示例
from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    t.join()       # 主进程感知子进程的结束
    print('主线程')
    print(t.is_alive())

# 运行结果:
egon say hello
主线程
False
join方法

2.5 守护线程

  无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。 需要强调的是:运行完毕并非终止运行

#1.对主进程来说,运行完毕:指的是主进程代码运行完毕
    #主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,

#2.对主线程来说,运行完毕:指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
    # 因为主线程的结束意味着进程的结束,进程整体的资源都会被回收,而进程必须保证非守护进程都运行完毕后才能结束
from threading import Thread
import time
def func1():
    while True:
        print('*'*10)
        time.sleep(1)

def func2():
    print('in func2')
    time.sleep(10)

if __name__ == '__main__':
    t1 = Thread(target = func1)
    t1.daemon = True   ##必须在t1.start()之前设置
    t1.start()

    t2 = Thread(target=func2)
    t2.start()
    t2.join()

    print('主线程结束')   #主线程得等子线程t2:func2 sleep完10s以后才能结束,这10s中,守护线程t1一直在运行,当主线程结束后,t1页随之结束

# 运行结果:
**********
in func2
**********
**********
**********
**********
**********
**********
**********
**********
**********
主线程结束
守护线程示例

2.6 线程锁

  2.6.1 同步锁

from threading import Thread
import os,time

def work():
    global n
    temp=n
    time.sleep(2)
    n=temp-1

if __name__ == '__main__':
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)    # 将有100个线程抢占资源,只有一个能抢到
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n)    # 99
多个线程抢占资源的情况
from threading import Thread,Lock
import os,time

def work():
    global n
    lock.acquire()
    temp=n
    time.sleep(0.1)
    n=temp-1
    lock.release()
    
if __name__ == '__main__':
    lock=Lock()
    n=100
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全
互斥锁解决资源抢占问题

  2.6.2 死锁与递归锁

  死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁:

from threading import Lock as Lock
import time
mutexA
=Lock() mutexA.acquire() mutexA.acquire() print(123) mutexA.release() mutexA.release()

  解决方法:递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

  这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:

from threading import RLock as Lock
import time

mutexA=Lock()
mutexA.acquire()
mutexA.acquire()
print(123)
mutexA.release()
mutexA.release()

  经典问题:"科学家吃面" ———— 死锁问题

from threading import Lock,RLock,Thread
import time

fork_lock = noodle_lock = Lock()

def eat1(name):
    noodle_lock.acquire()
    print('%s拿到面条啦'%name)
    fork_lock.acquire()
    print('%s拿到叉子了'%name)
    print('%s吃面'%name)
    fork_lock.release()
    noodle_lock.release()

def eat2(name):
    fork_lock.acquire()
    print('%s拿到叉子了' % name)
    time.sleep(1)
    noodle_lock.acquire()
    print('%s拿到面条啦'%name)
    print('%s吃面'%name)
    noodle_lock.release()
    fork_lock.release()

Thread(target = eat1,args=('alex',)).start()
Thread(target = eat2,args=('Egon',)).start()
Thread(target = eat1,args=('Boss jin',)).start()
Thread(target = eat2,args=('Mike',)).start()
"科学家吃面" —— 死锁问题
import time
from threading import Thread,RLock

fork_lock = noodle_lock = RLock()   # 一个钥匙串上的两把钥匙,一旦拿到一把钥匙,那么就拿到了这串钥匙上的所有钥匙

def eat1(name):
    noodle_lock.acquire()
    print('%s拿到面条啦'%name)
    fork_lock.acquire()
    print('%s拿到叉子了'%name)
    print('%s吃面'%name)
    fork_lock.release()
    noodle_lock.release()

def eat2(name):
    fork_lock.acquire()
    print('%s拿到叉子了' % name)
    time.sleep(1)
    noodle_lock.acquire()
    print('%s拿到面条啦'%name)
    print('%s吃面'%name)
    noodle_lock.release()
    fork_lock.release()

Thread(target = eat1,args=('alex',)).start()
Thread(target = eat2,args=('Egon',)).start()
Thread(target = eat1,args=('Boss jin',)).start()
Thread(target = eat2,args=('Mike',)).start()

#运行结果:
alex拿到面条啦
alex拿到叉子了
alex吃面
Egon拿到叉子了
Egon拿到面条啦
Egon吃面
Boss jin拿到面条啦
Boss jin拿到叉子了
Boss jin吃面
Mike拿到叉子了
Mike拿到面条啦
Mike吃面
递归锁解决死锁问题

2.7 线程事件和和信号量

import time
from threading import Semaphore,Thread

def func(sem,a,b):
    sem.acquire()
    time.sleep(1)
    print(a + b)
    sem.release()

sem = Semaphore(4)   # 先直接出4个,之后释放一个进一个
for i in range(10):
    t = Thread(target=func,args = (sem,i,i+5))
    t.start()

# 运行结果:
7
5
11
9
13
15
19
17
23
21
信号量
##  事件【被创建的时候默认是阻塞False状态】

## 【实例】连接数据库,检测数据库的可连接情况
##  起两个线程
        # 第一个线程:连接数据库【等待一个信号,告诉我们之间的网络是通的】,然后连接数据库
        # 第二个线程:检测与数据库之间的网络是否连通,将事件的状态设置为True
import time
import random
from threading import Thread,Event

def connect_db(e):
    count = 0
    while count < 3:
        e.wait(1)       # 状态为False的时候,我只等待一秒就结束
        if e.is_set == True:
            print('连接数据库')
            break
        else:
            count += 1
            print('第%s次连接失败'%count)
    else:
        raise TimeoutError('数据库连接超时!')

def check_web(e):
    time.sleep(random.randint(0,3))
    e.set()

if __name__ == '__main__':
    e = Event()
    t1 = Thread(target=connect_db,args = (e,))
    t2 = Thread(target=check_web,args = (e,))
    t1.start()
    t2.start()

# 运行结果:
第1次连接失败
第2次连接失败
Exception in thread Thread-1:
第3次连接失败
TimeoutError: 数据库连接超时!
连接数据库事件实例

2.8 线程条件和定时器

##################### 条件(锁)
    # 提供acquire 和 release,还提供wait 和 notify【这俩都需要acquire和release之间】
    # 一个条件被创建之初,默认有一个False状态【会影响wait一直处于等待状态】
    # notify(int数据类型):制造钥匙,使用后不归还

import time
from threading import Condition,Thread,Timer

def func(con,i):
    con.acquire()
    con.wait()
    print('in 第%s个循环里'%i)
    con.release()

con = Condition()
for i in range(10):
    t = Thread(target = func,args=(con,i))       # 要拿钥匙进房间的人
    t.start()

while True:
    num = int(input('>>>'))
    con.acquire()
    con.notify(num)                # 钥匙
    con.release()

# 运行结果:
>>>3
>>>in 第0个循环里
in 第1个循环里
in 第2个循环里
条件(锁)
#################### 计时器(每隔几秒进行一次时间同步)
def func():
    print('时间同步')
while True:
    t = Timer(5,func)         # 定时开启一个线程,是异步的,非阻塞的
    t.start()
    time.sleep(5)              # 每次循环,它和 t同时开始执行,一共就延时(sleep)了5秒
    print(1234)

#运行结果:
时间同步
1234
1234
时间同步
时间同步
1234
...
计时器示例

2.9 队列

# 队列:先进先出

import queue

q = queue.Queue()      # 常规队列:先进先出
zhan = queue.LifoQueue()      # 栈:先进后出,后进先出
queue.PriorityQueue()  # 优先级队列(按照值的ASCII码排序,越靠前优先级越高)

# 基础的4个方法:
    # q.put()
    # q.get()
    # q.put_nowait()
    # q.get_nowait()
队列语法

2.10 线程池和concurrent.futures

# from concurrent.futures import ThreadPoolExecutor

#1 介绍
concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.

#2 基本方法
#submit(fn, *args, **kwargs)
异步提交任务

#map(func, *iterables, timeout=None, chunksize=1) 
取代for循环submit的操作

#shutdown(wait=True) 
相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前

#result(timeout=None)
取得结果

#add_done_callback(fn)
回调函数

# done()
判断某一个线程是否完成

# cancle()
取消某个任务
concurrent.futures语法
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
import os,time,random

def task(n):
    print('进程%s is runing' %os.getpid())
    time.sleep(random.randint(1,3))
    return n**2

if __name__ == '__main__':
    executor=ThreadPoolExecutor(max_workers=3)

    # for i in range(11):
    #     future=executor.submit(task,i)

    executor.map(task,range(1,12))      #map取代了for+submit
map用法
import time
from concurrent.futures import ThreadPoolExecutor

def func(n):
    time.sleep(2)
    print(n)         # 不按顺序打印
    return n*n

def call_back(m):
    print('结果是%s'%m.result())

tpool = ThreadPoolExecutor(max_workers = 5)
for i in range(10):
    t = tpool.submit(func,i)
    t.add_done_callback(call_back)

# 运行结果:
0
结果是0
1
2
结果是4
结果是1
4
结果是16
3
结果是9
5
结果是25
6
结果是36
9
7
结果是49
8
结果是64
结果是81
回调函数

3 全局解释器锁GIL

3.1 GIL介绍

  PS:GIL并不是Python的特性,Python完全可以不依赖于GIL。

  简单来说,在Cpython解释器中,因为有GIL锁的存在,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势。

  理解GIL

3.2 常见问题1

  GIL保护的是Python解释器级别的数据资源,自己代码中的数据资源就需要自己加锁防止竞争。

3.3 常见问题2

   有了GIL的存在,同一时刻同一进程中只有一个线程被执行,进程可以利用多核,但是开销大,而Python的多线程开销小,但却无法利用多核优势,也就是说Python这语言难堪大用。

原文地址:https://www.cnblogs.com/timetellu/p/10702276.html