线程

一:线程理论

一台计算机相当于一个工厂,工厂里有很多车间(进程),车间里面有很多工人(线程)。

真正干活儿的是工人(线程)。

1.什么是线程

进程资源分配的最小单位,而线程进程中的一部分,每个进程至少有一个线程,是CPU调度的最小单位

进程是资源分配的最小单位,线程是CPU调度的最小单位线程开销更小,更轻量级

2.线程的数据是共享的

在操作系统中,每一个进程的内存空间是独立的,数据不共享

但是同一个进程中的各个线程数据是共享的。

二:开启线程的2种方式

1.第一种 - 普通方式

from threading import Thread
import time


def task(t_name, s_time):
    print(f'子线程[{t_name}]开始')
    time.sleep(s_time)
    print(f'子线程[{t_name}]结束')


if __name__ == '__main__':
    t1 = Thread(target=task, args=('t1', 1))  # 实例化得到一个线程对象
    t1.start()  # 对象.start()启动线程
    print('主线程')


# 子线程[t1]开始
# 主线程
# 子线程[t1]结束

2.第二种 - 继承类的方式

from threading import Thread
import time


class MyThread(Thread):
    def run(self):
        print('子线程开始')
        time.sleep(1)
        print('子线程结束')


if __name__ == '__main__':
    t = MyThread()
    t.start()
    print('主线程')


# 子线程开始
# 主线程
# 子线程结束

三:TCP服务端实现并发效果

To be continue...

四:线程对象join方法

from threading import Thread
import time


def task(t_name, s_time):
    print(f'{t_name} 开始')
    time.sleep(s_time)
    print(f'{t_name} 结束')


if __name__ == '__main__':
    t1 = Thread(target=task, args=('t1', 1))
    t2 = Thread(target=task, args=('t2', 2))

    t1.start()
    t2.start()

    t1.join()  # 等待子线程执行结束
    t2.join()

    print('主线程')


# t1 开始
# t2 开始
# t1 结束
# t2 结束
# 主线程

五:同一个进程下的多个线程数据共享

from threading import Thread
import time

money = 99


def task(t_name, n):
    global money
    print(f'{t_name} 开始')
    money = n
    print(f'{t_name} 结束')


if __name__ == '__main__':
    t1 = Thread(target=task, args=('t1', 10))
    t2 = Thread(target=task, args=('t2', 100))

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print(money)
    print('主线程')
    

# t1 开始
# t1 结束
# t2 开始
# t2 结束
# 100
# 主线程

六:线程对象 及 其他方法

from threading import Thread, current_thread, active_count
import time


def task():
    print('子线程开始')
    print(current_thread().name)  # 打印出当前线程名字
    time.sleep(1)
    print('子线程结束')


if __name__ == '__main__':
    t1 = Thread(target=task, name='I\'m Thread1')
    t2 = Thread(target=task, name='I\'m Thread2')
    t1.start()
    t2.start()
    print(f'存活线程数:{active_count()}')  # 打印出当前有多少存活的线程

# 子线程开始
# I'm Thread1
# 子线程开始
# I'm Thread2
# 存活线程数:3
# 子线程结束
# 子线程结束
from threading import Thread, current_thread, active_count
import time
import os


def task(t_name, n):
    print(f'{t_name}开始')
    print(f'当前线程:{current_thread().name}')  # 线程名字
    print(f'当前线程所在的进程ID:{os.getpid()}')
    time.sleep(n)
    print(f'{t_name}结束')


if __name__ == '__main__':
    t1 = Thread(target=task, name='Thread1', args=('t1', 2))
    t2 = Thread(target=task, name='Thread2', args=('t2', 5))
    t1.start()
    t2.start()
    t1.join()
    print(f'线程t1是否存活:{t1.is_alive()}')
    print(f'线程t2是否存活:{t2.is_alive()}')

    # 当作线程id号
    print(f'线程t1的ID:{t1.ident}')
    print(f'线程t2的ID:{t2.ident}')

    print(f'当前线程所在的进程ID:{os.getpid()}')
    print(f'存活线程数:{active_count()}')  # 打印出2 ,存活的是t2和主线程


# t1开始
# 当前线程:Thread1
# 当前线程所在的进程ID:37420
# t2开始
# 当前线程:Thread2
# 当前线程所在的进程ID:37420
# t1结束
# 线程t1是否存活:False
# 线程t2是否存活:True
# 线程t1的ID:45360
# 线程t2的ID:73396
# 当前线程所在的进程ID:37420
# 存活线程数:2
# t2结束

知识点:

1.获取线程名:t.namet.getName()

2.查看当前进程下有几个线程存活:active_count()

3.查看当前线程是否存活:t1.is_alive()

4.查看线程ID:t1.ident

5.查看当前线程所在的进程ID:os.getpid()

七:守护线程

from threading import Thread, current_thread, active_count
import time


def task(t_name, n):
    print(f'{t_name}开始')
    time.sleep(n)
    print(f'{t_name}结束')


if __name__ == '__main__':
    t1 = Thread(target=task, name='Thread1', args=('Thread1', 10))
    t2 = Thread(target=task, name='Thread2', args=('Thread2', 4))

    t1.setDaemon(True)

    t1.start()
    t2.start()

    print('主线程')


# Thread1开始
# Thread2开始
# 主线程
# Thread2结束

八:线程互斥锁

from threading import Thread, Lock
import time

money = 99


def task(n, mutex):
    global money
    mutex.acquire()     # 在修改数据的时候,加锁
    temp = money
    # time.sleep(random.randint(1, 2))
    time.sleep(0.1)
    money = temp - 1
    mutex.release()     # 修改完之后,释放锁,其他线程就可以抢到锁


if __name__ == '__main__':
    ll = []
    mutex = Lock()
    for i in range(10):
        t = Thread(target=task, args=(i, mutex))
        t.start()
        # t.join()    # 不能在这里join,会变成串行
        ll.append(t)

    for i in ll:
        i.join()

    print(money)


# 89

九:GIL 全局解释器锁

1.什么是GIL?

官网解释:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

在CPython中,全局解释器锁(GIL)是一个互斥体,它阻止多个本机线程同时执行Python字节码。cpyth的线程管理不是必需的。(然而,由于GIL的存在,其他特性也越来越依赖于它所执行的保证。)

GILGlobal Interpreter Lock)全局解释器锁

首先需要明确的一点GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念

同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,然而因为CPython是大部分环境下默认的Python执行环境

所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷,所有GIL并不是python的特性,仅仅是因为历史原因在Cpython解释器中难以移除

2.GIL存在的原因

CPython在执行多线程的时候并不是线程安全的,所以为了程序的稳定性,加一把全局解释锁,能够确保任何时候都只有一个Python线程执行。

②因为垃圾回收机制的存在,如果一个定义了一个x=2,如果在一种较为极端的情况下,x已经生成,但是2还没赋值给x,此时的x就会被垃圾回收机制当成垃圾 被回收,为了避免这种情况出现,加了一把全局解释器锁。

3.线程释放GIL锁的2种情况

①.遇到I/O操作

例如:发出了HTTP请求,需要等待响应读写文件...

如果是

Time Tick 到期

Time Tick规定了线程的最长执行时间超过时间自动释放GIL锁

3.单核与多核情况下的多线程任务调度

单核:

由此可以发现:由于GIL机制的存在,单核CPU在同一时刻,只有一个线程在运行。

当线程遇到了I/O操作或者Time Tick到期的时候,就会释放GIL锁release GIL

此时,其他2个线程去抢这把锁,抢到之后,线程才能运行。

虽然出现I/O操作Time Tick到期都会释放GIL锁,但是这2种情况是不一样的:

出现I/O:

如果Thread1出现了IO操作,释放了GIL锁,Thread1会主动放弃抢锁,不再参与这场竞争。

Time Tick到期:

如果Thread1出现了TimeTick到期,释放了GIL锁,Thread1会和其他线程竞争,同时抢这把GIL锁

  • 所以,在单核的情况下,CPU的利用率是很高的。

多核:

由此可以发现:Thread1在CPU1上运行,Thread2在CPU2上运行。

GIL是全局的,CPU2上的Thread2需要等待CPU1上的Thread1让出GIL锁,才有可能执行。

如果在多次竞争中,Thread2都胜出,Thread1没有得到GIL锁,意味着CPU1一直是闲置的,无法发挥多核的优势。

所以,有了GIL的存在,CPython的的多线程 其实就是 单线程。

4.针对与cpython解释器,有哪些注意点?

①针对IO密集型:开多线程

②针对计算密集型:开多进程

5.既然GIL有缺陷,为什么还有这么多人用呢?

①.python的库多,库都是基于cpython写起来的,其他解释器没有那么多的第三方库(海纳百川,有容乃大)。

②.虽然有这个历史诟病,但是丝毫不影响我们敲代码(金无足赤,人无完人)。

③.代码是一成不变的,但是我们可以利用它的优势,优化它的劣势(取其精华,去其糟粕

④.Python牛逼!(人生苦短,我用Python

6.总结

cpython解释器中只有一个全局锁(GIL),线程必须获取到GIL才能执行,我们开的多线程不管有几个CPU,同一时刻只能有一个线程在执行(python的多线程,不能利用多核优势)

posted @ 2020-08-25 15:53  轻描丨淡写  阅读(253)  评论(0编辑  收藏  举报