并发编程之多线程

线程

  • 一个程序的运行过程是一个进程,每个进程自带一个控制线程
  • 进程是一个资源单位,线程是具体的执行单位,即一个进程内可有多个线程。
  • 多线程(多个控制线程):在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
# 进程和线程
进程: 资源单位(起一个进程是在内存空间中开辟一块独立的空间)
线程: 执行单位(真正被干活的是进程里面的线程,线程在执行中所需要使用到的资源都找所在的进程索要)
# 程和线程都是虚拟单位,只是为了我们更加方便的描述问题

进程线程比较

  • 开进程:申请内存空间,资源消耗大
  • 开线程:进程内开多个线程,无需再次申请内存空间操作,消耗小
  • 开线程的开销要远远的小于进程的开销。同一个进程下的多个线程数据是共享的
- Threads share the address space of the process that created it; processes have their own address space.
- Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
- Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
- New threads are easily created; new processes require duplication of the parent process.
- Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
- Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.

为何要用多线程

如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。

#1 多线程共享一个进程的地址空间
#2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在有大量线程需要动态和快速修改时,这一特性很有用。

#3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。

#4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。
# but, python的多线程无法使用多核优势

开启线程的两种方式

开启进程的方式和开启线程的方式一模一样,只是使用的模块不一样。

总结:

  • 开线程的开销非常下,几乎是代码执行就执行的线程的任务。
  • 开线程可以不写在main判断下。
from threading import Thread


def task():
    print('i am sub_thread')

# 方式1:直接使用Thread实例化线程对象
if __name__ == '__main__':
    t = Thread(target=task)
    t.start()
    print('i am main-thread')
   

# 方式2:继承Therad,自定自己的线程类,重写run方法
class MyThread(Thread):
    def run(self):
        task()

if __name__ == '__main__':
    t = MyThread()
    t.start()
    print('i am main-thread')

实例:TCP服务端多线程实现并发

import socket
from threading import Thread

server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
# 线程的任务:通信循环
def task(conn):
    while 1:
        try:
            data = conn.recv(1024)
            if not data: break
            conn.send(data.upper())
        except Exception as e:
            print(e)
            break
    conn.close()


while 1:
    conn, addr = server.accept()
    t = Thread(target=task, args=(conn, ))	# 来一个连接请求,就开一个线程处理这个连接
    t.start()								# 这种并发方式有缺陷:容易内存溢出(线程个数有限)

线程的join方法

线程对象的join方法,就是让主线程等待子线程,子线程运行结束后再执行主线程。

if __name__ == '__main__':
    t = MyThread()
    t.start()
    t.join()		 # 主线程等待子线程运行结束再执行
    print('i am main-thread')

多线程间共享数据

同一个进程下的多个线程是共享当前进程下的数据资源的

from threading import Thread

n = 100

def func():
    global n
    n = 110
    print('sub_thread', n)		# 110

if __name__ == '__main__':
    t = Thread(target=func)
    t.start()
    # t.join()
    print('main-thread', n)		# 110	两次输出结果一致,都是访问的一个数据

线程相关其他方法

基本和进程对象的差不多,不再赘述。

# 线程对象的
# native_id 获取当前对象的线程id

from threading import Thread, active_count, current_thread

# sctive_count() 		获取当前存活的线程数
# current_thread()		获取当前线程对象,  
# current_thread().name	当前对象的没名字等

# 补充:
active_count 等同于 currentThread, 
current_thread 等同于 activeCount

守护线程

守护线程:设置子线程为守护线程,当主线程(即程序)运行结束时,守护线程随着主线程的结束而结束。

总结:

  • 主线程,就是当前程序,程序一旦运行到最后一行代码就结束。

  • 如果子线程不是守护线程,程序运到最后一行代码时会在原地等待子线程的。

  • 如果主线程结束了,那么守护线程会随之结束。

  • 因为主线程的结束意味着所在的进程的结束

# 主线程之所以在程序最后处等待其他非守护线程的结束,是因为主线程就是当前程序,即进程。
# 当前程序如果结束了,那么这些子线程就没办法继续工作。(车间[进程]倒闭了,流水线[线程]就没有意义了)。

线程互斥锁

线程互斥锁和进程互斥锁概念一致,都是将并发变为串行,牺牲效率,保证数据安全

补充:

  • 互斥锁:局部,并发变串行
  • start后直接join:整体,并发变串行
from threading import Thread,Lock
import time


money = 100
mutex = Lock()


def task():
    global money
    mutex.acquire()
    tmp = money
    time.sleep(0.1)     # 模拟网络延迟
    money = tmp - 1
    mutex.release()


if __name__ == '__main__':

    t_list = []
    for i in range(100):
        t = Thread(target=task)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(money)		# 0, 如果没有锁,那么结果就是99了,大家同时拿到的都是一个数据,100-1=99

死锁和递归锁

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

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

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

详情:参考博客

GIL

全局解释器锁(GIL, Global Interpreter Lock)。

结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势

# 程序的代码需要交给解释器执行,因此该进程内的每个线程都需要将共享的代码交给解释器执行,这就有了竞争。
# 垃圾回收机制GC也是当前进程内的一个干活的线程,GC会和当前进程内其他线程有抢代码数据的竞争,因此有了GIL。
# GIL保证进程内同一时刻只有一个线程在运行,这样做是为了保证内存管理(垃圾回收机制GC)的运行。

总结:

  • GIL不是python语言的特性,它是Cpython解释器引用的一个概念。

  • GIL本质是一把互斥锁,是将线程从并发变为串行,牺牲效率保证数据安全。

  • 保护不同的数据要加不同的互斥锁,GIL保护的是解释器级别的数据安全。

初步图解,思考的不深,待日后补充。

强烈推荐GIL细节

python多线程的应用

首先明确:python多线程无法利用计算机的多核优势

但是python的所线程可以实现并发的,且不是所有的场合都要使用多核的。

场合:

  • 对于计算密集型,多进程优于多线程(多进程的并行计算优势突出了)
  • 对于IO密集型,多线程优于多进程(开进程的开销大缺陷放大了)

结论:python多线程无法使用多核优势,不等于多线程一无是处

posted @ 2020-04-23 18:12  the3times  阅读(202)  评论(0编辑  收藏  举报