欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

多线程以及python的多线程

程序员文章站 2022-03-16 09:03:25
...

1.1 线程和进程

进程:系统分配资源最小单位,进程指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。
线程:系统调度最小单位,线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

1.1.1什么是多线程呢?

即就是一个程序中有多个线程在同时执行。
单线程:多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。
多线程:多个任务可以同时执行。

1.2 线程运行原理

分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

1.2.1 抢占式调度详解

大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。
实际上,CPU(*处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

1.3 python 多线程

先明确一个问题,Python中的多线程是假的多线程! 为什么这么说,我们先明确一个概念,全局解释器锁(GIL)。
Python代码的执行由Python虚拟机(解释器)来控制。Python在设计之初就考虑要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器可以运行多个线程,只有一个线程在解释器中运行。
对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同时只有一个线程在运行。在多线程环境中,Python虚拟机按照以下方式执行。
1.设置GIL。
2.切换到一个线程去执行。
3.运行。
4.把线程设置为睡眠状态。
5.解锁GIL。
6.再次重复以上步骤。

Python多线程相当于单核多线程,多线程有两个好处:CPU并行,IO并行,单核多线程相当于自断一臂。所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

1.4 计算密集型和IO密集型

计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

1.5 python多线程使用场景

python多线程不适合计算密集型任务,使用多线程可能会考虑全局变量,为了防止线程之间抢占问题会加锁,大大增加了消耗,所以python多线程只适合io密集型的并发上:
单线程:

import time
start_time = time.time()
start_num = 500
n = 1
res = 10000
res_num = 0
while 1:
    res_num += start_num/n
    # print("n:{} res_num={}".format(n, res_num))
    if int(res_num) == res:
        print("n:{} res_num={}".format(n, res_num))
        print("res: {}".format(n))
        break
    else:
        n += 1
    # time.sleep(0.001)
print("spend_time:{}".format(time.time()-start_time))
# n:272400600 res_num=10000.000000810323
# spend_time:63.84967088699341

多线程:
这里慢的原因有两点:1、上下文切换 2、全局变量加锁每次加锁和释放

# 线程中使用全局变量导致效率大打折扣
import threading
import time

start_time = time.time()
threads_num = 10
lock = threading.Lock()
start_num = 500
n = 1
res = 10000
res_num = 0


def demo(a):
    global n, res_num
    time.sleep(1)
    while 1:
        # time.sleep(0.5)
        lock.acquire()
        res_num += start_num / n
        try:
            # print("{}: n:{} res_num={}".format(a, n, res_num))
            if int(res_num) == res:
                print("{}: n:{} res_num={}".format(a, n, res_num))
                break
            else:
                n += 1
        finally:
            lock.release()


threads = []
for i in range(threads_num):
    t1 = threading.Thread(target=demo, args=(i,))
    t1.daemon = True
    threads.append(t1)

for task in threads:
    task.start()
for task in threads:
    task.join()

print("n:{} res_num={}".format(n, res_num))
print("spend_time:{}".format(time.time() - start_time))
# n:272400600 res_num=10000.000017330105
# spend_time:122.03608107566833

多进程:
多进程慢的原因

import time
import multiprocessing

start_num = 500
res = 10000


def demo(a, n, res_num, lock):
    while True:
        with lock:
            res_num.value += start_num / n.value
            # print("{}: n:{} res_num={}".format(a, n.value, res_num.value))
            # time.sleep(0.001)
            if int(res_num.value) >= res:
                print("{}: n:{} res_num={}".format(a, n.value, res_num.value))
                break
            else:
                n.value += 1


if __name__ == '__main__':
    start_time = time.time()
    multiproces_num = 2
    multiproces_list = []
    n = multiprocessing.Value('i', 1)
    res_num = multiprocessing.Value('d', 0)
    lock = multiprocessing.Lock()
    for i in range(multiproces_num):
        p1 = multiprocessing.Process(target=demo, args=(i, n, res_num, lock,))
        multiproces_list.append(p1)
    for task in multiproces_list:
        task.start()
    for task in multiproces_list:
        task.join()
    print("multiproces_num:{}  spend_time:{}".format(multiproces_num, time.time() - start_time))
# 时间太长了超过了5分钟不等了

总结:对于运算简单的函数/方法,使用单线程即可最高效率发挥性能优势,使用多线程/多进程由于全局变量/共享变量需要加锁防止抢占,徒增了锁的开销,反而会大大降低运行效率增加性能开销

多线程适用的场景:
单线程:只能先:吃饭(10s),再:看电影(10s),最后:听音乐(5s)

import time
start_time = time.time()
print("start eat...")
time.sleep(10)
print("end eat")
print("start movie...")
time.sleep(10)
print("end movie")
print("start music...")
time.sleep(5)
print("end movie")
print("spend time:{}".format(time.time()-start_time))
# spend time:25.008626222610474

多线程:吃饭(10s)、看电影(10s)、听音乐(5s)同时进行

import threading
import time

start_time = time.time()


def eat():
    print("start eat...")
    time.sleep(10)
    print("end eat")


def movie():
    print("start movie...")
    time.sleep(10)
    print("end movie")


def music():
    print("start music...")
    time.sleep(10)
    print("end music")


threads = []
t1 = threading.Thread(target=eat)
t1.daemon = True
threads.append(t1)
t1 = threading.Thread(target=movie)
t1.daemon = True
threads.append(t1)
t1 = threading.Thread(target=music)
t1.daemon = True
threads.append(t1)

for task in threads:
    task.start()
for task in threads:
    task.join()

print("spend_time:{}".format(time.time() - start_time))
# spend_time:10.004291772842407

另外,切勿使用python进行cpu密集型计算,性能太差,附上上述例子go的简单实现
部分原因是:python中所有皆为对象,每次计算出来的值都要重新赋值,内存地址每次都会发生改变

package main

import (
	"fmt"
	"time"
)

var timeUnixNano = time.Now().UnixNano()//单位纳秒
var start_num float64 = 500
var n float64 = 1
var res float64 = 10000
var res_num float64 = 0
var add_num float64

func main(){
	for true{
		add_num = start_num/n
		res_num = res_num + add_num
		//fmt.Println("n=",n)
		//fmt.Println("res_num=",res_num)
		if res_num >= res{
			fmt.Println("n=",n)
			fmt.Println("res_num=",res_num)
			fmt.Println("time spend:",(time.Now().UnixNano() - timeUnixNano)/ 1e6)
			break
		}else{
			n ++
		}

	}
}

// n= 2.724006e+08
// res_num= 10000.000000810323
// time spend: 612  毫秒