协程,GIL全局解释器,互斥锁,线程池,Concurrent模块

news2024/12/28 10:16:12

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


Python对并发编程的支持


(1)多线程:threading,利用CPU和IO可以同时执行的原理,让CPU不会干巴巴等待IO完成。
(2)多进程:multiprocessing,利用多核CPU的能力,真正的并行执行任务。
(3)异步IO:asyncio,在单线程利用CPU和IO同时执行的原理,实现函数异步执行。
(4)使用Lock对资源加锁,防止冲突访问。
(5)使用Queue实现不同线程/进程之间的数据通信,实现生产者-消费者模式。
(6)使用线程池Pool/进程池Pool,简化线程/进程的任务提交、等待结束、获取结果。
(7)使用subprocess启动外部程序的进程,并进行输入输出交互。

Python并发编程有三种方式


多线程Thread、多进程Process、多协程Coroutine。

为什么要引入并发编程?


场景1:一个网络爬虫,按顺序爬取花了1小时,采用并发下载减少到20分钟!
场景2:一个APP应用,优化前每次打开页面需要3秒,采用异步并发提升到每次200毫秒!
引入并发,就是为了提升程序运行速度。

多线程、多进程、多协程的对比

怎样根据任务选择对应技术?

 

GIL全局解释器锁

        GIL全局解释器锁(Global Interpreter Lock)是一种在Python解释器中使用的机制,它的主要作用是防止同一时间内多个线程同时执行 Python 代码。

        在 Python 中,由于存在 GIL 锁的机制,因此在多线程执行 Python 代码时,同一时间只有一个线程能够占用 CPU 执行 Python 代码,其他线程将一直处于等待状态。

        这种机制有利于保证 Python 代码的稳定性和线程安全,但也带来了一定的性能损耗。因此,对于 CPU 密集型的 Python 应用程序,多线程并不能提高其运行速度。相反,对于 I/O 密集型的应用程序,多线程可以有效地提升其运行效率。

GIL步骤

在多线程环境中,Python 解释器按以下方式执行:

  1. 设置 GIL;
  2. 切换到一个线程去运行;
  3. 运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0));
  4. 把线程设置为睡眠状态;
  5. 解锁 GIL;
  6. 再次重复以上所有步骤。

在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。

GIL全局解释器相关背景

GIL锁就是保证在统一时刻只有一个线程执行,所有的线程必须拿到GIL锁才有执行权限

1. Python代码运行在解释器上嘛,有解释器来执行或者解释
2. Python解释器的种类:
	1、CPython  2、IPython 3、PyPy  4、Jython  5、IronPython
3. 当前市场使用的最多(95%)的解释器就是CPython解释器
4. GIL全局解释器锁是存在于CPython中
5. 结论是同一时刻只有一个线程在执行? 想避免的问题是,出现多个线程抢夺资源的情况
	比如:现在起一个线程,来回收垃圾数据,回收a=1这个变量,另外一个线程也要使用这个变量a,当垃圾回收线程还没没有把变量a回收完毕,另一个线程就来抢夺这个变量a使用。
    怎么避免的这个问题,那就是在Python这门语言设计之处,就直接在解释器上添加了一把锁,这把锁就是为了让统一时刻只有一个线程在执行,言外之意就是哪个线程想执行,就必须先拿到这把锁(GIL), 只有等到这个线程把GIL锁释放掉,别的线程才能拿到,然后具备了执行权限.

GIL全局解释器需要注意的问题

1. python有GIL锁的原因,同一个进程下多个线程实际上同一时刻,只有一个线程在执行

2. 只有在python上开进程用的多,其他语言一般不开多进程,只开多线程就够了

3. cpython解释器开多线程不能利用多核优势,只有开多进程才能利用多核优势,其他语言不存在这个问题

4. 8核cpu电脑,充分利用起我这个8核,至少起8个线程,8条线程全是计算--->计算机cpu使用率是100%

5. 如果不存在GIL锁,一个进程下,开启8个线程,它就能够充分利用cpu资源,跑满cpu

6. cpython解释器中好多代码,模块都是基于GIL锁机制写起来的,改不了了---》我们不能有8个核,但我现在只能用1核,----》开启多进程---》每个进程下开启的线程,可以被多个cpu调度执行

7. cpython解释器:io密集型使用多线程,计算密集型使用多进程

I / O密集型: 遇到io操作会切换cpu,假设你开了8个线程,8个线程都有io操作---》io操作不消耗cpu---》一段时间内看上去,其实8个线程都执行了, 选多线程好一些

计算密集型: 消耗cpu,如果开了8个线程,第一个线程会一直占着cpu,而不会调度到其他线程执行,其他7个线程根本没执行,所以我们开8个进程,每个进程有一个线程,8个进程下的线程会被8个cpu执行,从而效率高.


互斥锁

        互斥锁的作用:在多线程的情况下,同时执行一个数据,会发生数据错乱的问题,互斥锁可以防止这种情况发生。

n = 10
from threading import Lock
import time

def task(lock):
    lock.acquire()
    global n
    temp = n
    time.sleep(0.5)
    n = temp - 1
    lock.release()


"""拿时间换空间,空间换时间 时间复杂度"""

from threading import Thread

if __name__ == '__main__':

    tt = []
    lock=Lock()
    for i in range(10):
        t = Thread(target=task, args=(lock, ))
        t.start()
        tt.append(t)
    for j in tt:
        j.join()

    print("主", n)

GIL锁,互斥锁 面试题

面试题:既然有了GIL锁,为什么还要互斥锁? (多线程下)


       举例比如:我起了2个线程,来执行a=a+1,a一开始是0
       1. 第一个线程来了,拿到a=0,开始执行a=a+1,这个时候结果a就是1了
       2. 第一个线程得到的结果1还没有赋值回去给a,这个时候,第二个线程来了,拿到的a是             0,继续执行, a=a+1结果还是1
       3. 加了互斥锁,就能够解决多线程下操作同一个数据,发生错乱的问题

线程队列(线程里使用队列)

为什么线程中还有使用队列?


        同一个进程下多个线程数据是共享的,为什么先同一个进程下还会去使用队列呢
因为队列是管道 + 锁,所以用队列还是为了保证数据的安全
 

程队列:
	1. 先进先出
    2. 后进先出
    3. 优先级的队列
    
from multiprocessing import Queue

"""线程队列"""

import queue
queue.Queue()

# queue.Queue 的缺点是它的实现涉及到多个锁和条件变量,因此可能会影响性能和内存效率。
import queue

q=queue.Queue() # 无限大、
q.put('first')
q.put('second')
q.put('third')
q.put('third')


print(q.get())
print(q.get())
print(q.get())
 
## 后进先出
import queue

# Lifo:last in first out
q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())

## 优先级队列
import queue

q=queue.PriorityQueue()
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get())
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''

进程池和线程池的使用

:池子、容器类型,可以盛放多个元素

进程池:提前定义好一个池子,然后,往这个池子里面添加进程,以后,只需要往这个进程池里面丢任务就行了,然后,有这个进程池里面的任意一个进程来执行任务

线程池:提前定义好一个池子,然后,往这个池子里面添加线程,以后,只需要往这个线程池里面丢任务就行了,然后,有这个线程池里面的任意一个线程来执行任务

def task(n, m):
    return n+m

def task1():
    return {'username':'kevin', 'password':123}
"""开进程池"""
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor


def callback(res):
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # 3

def callback1(res):
    print(res) # Future at 0x1ed5a5e5610 state=finished returned int>
    print(res.result()) # {'username': 'kevin', 'password': 123}
    print(res.result().get('username'))
if __name__ == '__main__':
    pool=ProcessPoolExecutor(3) # 定义一个进程池,里面有3个进程
    ## 2. 往池子里面丢任务

    pool.submit(task, m=1, n=2).add_done_callback(callback)
    pool.submit(task1).add_done_callback(callback1)
    pool.shutdown()  # join + close
    print(123)

进程池和线程池有什么好处呢? 

(1)降低资源消耗。通过重复利用已创建的线程降低线程创建、销毁线程造成的消耗。
(2)提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控。


Concurrent.futures模块(爬虫)

模块介绍

concurrent.futures模块提供了高度封装的异步调用接口

ThreadPoolExecutor:线程池,提供异步调用

ProcessPoolExecutor:进程池,提供异步调用

Both implement the same interface, which is defined by the abstract Executor class.

基本方法

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():取消某个任务

ThreadPoolExecutor线程池


常用函数


        将函数提交到线程池里面运行的时候,会自动创建Future对象并返回。这个Future对象里面就包含了函数的执行状态(比如此时是处于暂停、运行中还是完成等)。并且函数在执行完毕之后,还会调用future.set_result将自身的返回值设置进去。
        (1)创建一个线程池,可以指定max_workers参数,表示最多创建多少个线程。如果不指定,那么每提交一个函数,都会为其创建一个线程。

在启动线程池的时候,肯定是需要设置容量的,不然处理几千个函数要开启几千个线程。

        (2)通过submit即可将函数提交到线程池,一旦提交,就会立刻运行。因为开启了一个新的线程,主线程会继续往下执行。至于submit的参数,按照函数名,对应参数提交即可。

        (3)future相当于一个容器,包含了内部函数的执行状态。

        (4)函数执行完毕时,会将返回值设置在future里,也就是说一旦执行了 future.set_result,那么就表示函数执行完毕了,然后外界可以调用result拿到返回值。
 

from concurrent.futures import ThreadPoolExecutor
import time


def task(name, n):
    time.sleep(n)
    return f"{name} 睡了 {n} 秒"


executor = ThreadPoolExecutor()
future = executor.submit(task, "屏幕前的你", 3)

print(future)  # <Future at 0x7fbf701726d0 state=running
print(future.running())  # 函数是否正在运行中True
print(future.done())  # 函数是否执行完毕False

time.sleep(3)  # 主程序也sleep 3秒,显然此时函数已经执行完毕了

print(future)  # <Future at 0x7fbf701726d0 state=finished returned str>返回值类型是str
print(future.running())  # False
print(future.done())  # True

print(future.result())

多线程爬取网页

import requests

def get_page(url):
    res=requests.get(url)
    name=url.rsplit('/')[-1]+'.html'
    return {'name':name,'text':res.content}

def call_back(fut):
    print(fut.result()['name'])
    with open(fut.result()['name'],'wb') as f:
        f.write(fut.result()['text'])


if __name__ == '__main__':
    pool=ThreadPoolExecutor(2)
    urls=['http://www.baidu.com','http://www.cnblogs.com','http://www.taobao.com']
    for url in urls:
        pool.submit(get_page,url).add_done_callback(call_back)

协程理论

核心理解:切换是程序员级别的切换,我们自己切,不是操作系统切的

协程的本质:最大效率的利用计算机的CPU资源,欺骗计算机,让计算机cpu一直保持工作状态

协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。

需要强调的是:

  1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
  2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)

对比操作系统控制线程的切换,用户在单线程内控制协程的切换。

优点如下:

  1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
  2. 单线程内就可以实现并发的效果,最大限度地利用cpu

缺点如下:

  1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
  2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程

总结协程特点

  1. 必须在只有一个单线程里实现并发
  2. 修改共享数据不需加锁
  3. 用户程序里自己保存多个控制流的上下文栈
  4. 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))

协程之greenlet模块

一、安装模块

安装:pip3 install greenlet

二、greenlet实现状态切换

from greenlet import greenlet

def eat(name):
    print('%s eat 1' %name)
    g2.switch('nick')
    print('%s eat 2' %name)
    g2.switch()
def play(name):
    print('%s play 1' %name)
    g1.switch()
    print('%s play 2' %name)

g1=greenlet(eat)
g2=greenlet(play)

g1.switch('nick')#可以在第一次switch时传入参数,以后都不需要

单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度。

三、效率对比

#顺序执行
import time
def f1():
    res=1
    for i in range(100000000):
        res+=i

def f2():
    res=1
    for i in range(100000000):
        res*=i

start=time.time()
f1()
f2()
stop=time.time()
print('run time is %s' %(stop-start)) #10.985628366470337

#切换
from greenlet import greenlet
import time
def f1():
    res=1
    for i in range(100000000):
        res+=i
        g2.switch()

def f2():
    res=1
    for i in range(100000000):
        res*=i
        g1.switch()

start=time.time()
g1=greenlet(f1)
g2=greenlet(f2)
g1.switch()
stop=time.time()
print('run time is %s' %(stop-start)) # 52.763017892837524

greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2…如此,才能提高效率,这就用到了Gevent模块。


协程之gevent模块

1 猴子补丁

1,这个词原来为Guerrilla Patch,杂牌军、游击队,说明这部分不是原装的,在英文里guerilla发音和gorllia(猩猩)相似,再后来就写了monkey(猴子)。

2,还有一种解释是说由于这种方式将原来的代码弄乱了(messing with it),在英文里叫monkeying about(顽皮的),所以叫做Monkey Patch。

1.1 猴子补丁的功能(一切皆对象)

        拥有在模块运行时替换的功能, 例如: 一个函数对象赋值给另外一个函数对象(把函数原本的执行的功能给替换了)

class Monkey():
    def play(self):
        print('猴子在玩')

class Dog():
    def play(self):
        print('狗子在玩')
m=Monkey()
m.play()
m.play=Dog().play
m.play()

1.2 monkey patch的应用场景

        这里有一个比较实用的例子,很多用到import json, 后来发现ujson性能更高,如果觉得把每个文件的import json改成import ujson as json成本较高, 或者说想测试一下ujson替换是否符合预期, 只需要在入口加上:

import json
import ujson

def monkey_patch_json():
    json.__name__ = 'ujson'
    json.dumps = ujson.dumps
    json.loads = ujson.loads
monkey_patch_json()
aa=json.dumps({'name':'lqz','age':19})
print(aa)

1.3 Gevent介绍

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。 

 用法

#用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的

g2=gevent.spawn(func2)

g1.join() #等待g1结束

g2.join() #等待g2结束

#或者上述两步合作一步:gevent.joinall([g1,g2])

g1.value#拿到func1的返回值

示例1(遇到io自动切)

import gevent
def eat(name):
    print('%s eat 1' %name)
    gevent.sleep(2)
    print('%s eat 2' %name)

def play(name):
    print('%s play 1' %name)
    gevent.sleep(1)
    print('%s play 2' %name)


g1=gevent.spawn(eat,'lqz')
g2=gevent.spawn(play,name='lqz')
g1.join()
g2.join()
#或者gevent.joinall([g1,g2])
print('主')

示例二 

'''
上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,

而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了

from gevent import monkey;monkey.patch_all()必须放到被打补丁者的前面,如time,socket模块之前

或者我们干脆记忆成:要用gevent,需要将from gevent import monkey;monkey.patch_all()放到文件的开头
'''
from gevent import monkey;monkey.patch_all()

import gevent
import time
def eat():
    print('eat food 1')
    time.sleep(2)
    print('eat food 2')

def play():
    print('play 1')
    time.sleep(1)
    print('play 2')

g1=gevent.spawn(eat)
g2=gevent.spawn(play_phone)
gevent.joinall([g1,g2])
print('主')

# 我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程

协程实现高并发

服务端:

服务端:
from gevent import monkey;

monkey.patch_all()
import gevent
from socket import socket
# from multiprocessing import Process
from threading import Thread


def talk(conn):
    while True:
        try:
            data = conn.recv(1024)
            if len(data) == 0: break
            print(data)
            conn.send(data.upper())
        except Exception as e:
            print(e)
    conn.close()


def server(ip, port):
    server = socket()
    server.bind((ip, port))
    server.listen(5)
    while True:
        conn, addr = server.accept()
        # t=Process(target=talk,args=(conn,))
        # t=Thread(target=talk,args=(conn,))
        # t.start()
        gevent.spawn(talk, conn)


if __name__ == '__main__':
    g1 = gevent.spawn(server, '127.0.0.1', 8080)
    g1.join()

客户端:

客户端:
	import socket
from threading import current_thread, Thread


def socket_client():
    cli = socket.socket()
    cli.connect(('127.0.0.1', 8080))
    while True:
        ss = '%s say hello' % current_thread().getName()
        cli.send(ss.encode('utf-8'))
        data = cli.recv(1024)
        print(data)


for i in range(5000):
    t = Thread(target=socket_client)
    t.start()

END


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1110364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nvcc -V和nvidia-smi的关系

nvcc -V 和 nvidia-smi 都与NVIDIA GPU相关&#xff0c;但它们提供的信息和功能有所不同。 nvcc -V: nvcc 是 NVIDIA CUDA 编译器的命令&#xff0c;用于获取CUDA工具包的版本信息。CUDA&#xff08;Compute Unified Device Architecture&#xff09;是一种用于并行计算的GPU编…

阿里云短信服务

文章目录 了解阿里云用户权限操作开通阿里云短信服务添加短信模板添加签名编写测试代码编写可重复的微服务接口&#xff0c;实现验证码的发送&#xff01; 了解阿里云用户权限操作 模型 去阿里云个人中心查看授权码等&#xff1a; 点击开始使用用户的AccessKey 创建用户组&…

用户登录管理中的Bug修复与技术思考

目录 1 前言2 问题提出3 问题分析和解决4 技术分析和改进5 结语 1 前言 在开发管理软件平台为美术馆时&#xff0c;我们致力于提供一个多系统集成平台&#xff0c;其中包括艺术品管理、志愿者管理和数字资产管理等子系统。为了确保用户享有流畅的体验&#xff0c;我们采用了一…

面向对象设计原则之单一职责原则

目录 定义作用及影响示例 面向对象设计原则之开-闭原则 面向对象设计原则之里式替换原则 面向对象设计原则之依赖倒置原则 面向对象设计原则之单一职责原则 定义 单一职责原则 / 单一功能原则 &#xff08;Single Responsibility Principle&#xff0c;SRP&#xff09;&#x…

实验2.2.1 交换机VLAN的划分

实验2.2.1 交换机VLAN的划分 一、任务描述二、任务分析三、实验拓扑四、具体要求五、任务实施1.重命名交换机&#xff0c;关闭干扰信息&#xff0c;并创建vlan。2.通过display vlan查看vlan相关信息3.配置Access接口及分配vlan接口。4.查看vlan的相关信息。 六、任务验收七、任…

【PXIE301-211】青翼科技基于PXIE总线的16路并行LVDS数据采集、1路光纤数据收发处理平台

板卡概述 PXIE301-211是一款基于PXIE总线架构的16路并行LVDS数据采集、1路光纤收发处理平台&#xff0c;该板卡采用Xilinx的高性能Kintex 7系列FPGA XC7K325T作为实时处理器&#xff0c;实现各个接口之间的互联。板载1组64位的DDR3 SDRAM用作数据缓存。板卡具有1个FMC&#xf…

婚纱摄影行业如何利用软文精准获客

婚纱摄影在整个结婚流程中处于中上游&#xff0c;因此婚摄环节是整个婚庆的重要环节&#xff0c;市场的强烈需求也使整个行业的规模不断扩张&#xff0c;那么在激烈的市场竞争中&#xff0c;婚纱摄影行业应该如何获得源源不断的客户呢&#xff0c;可以试试软文&#xff0c;接下…

浏览器调试模式获取链接信息(获取京东cookie为例)

通过浏览器的调试模式&#xff0c;获取京东cookie变量pt_pin和pt_key。 一、登录 1&#xff09;打开网页 浏览器打开手机版京东网页&#xff1a;m.jd.com 2&#xff09;登录账号 点击【登录】按钮&#xff0c;输入账号密码登录 二、调试模式 1&#xff09;停留在要调试的…

计算机基础知识35

进程和线程的比较 1. 进程的开销比线程的开销大很多 2. 进程之间的数据是隔离的&#xff0c;但是&#xff0c;线程之间的数据不隔离 3. 多个进程间的线程数据不共享----->让进程通信(IPC)---->进程下的线程也通信了---->队列 GIL全局解释器锁(重要理论) # 虽然一个进程…

“智慧工地”施工现场管理一体化云平台,支持多端展示(PC端、手机端、平板端)

智慧工地平台源码&#xff0c;微服务架构JavaSpring Cloud UniApp MySql 支持多端展示&#xff08;PC端、手机端、平板端&#xff09; 智慧工地是什么&#xff1f; 智慧工地主要围绕绿色施工、安全管控、劳务管理、智能管理、集成总控等方面&#xff0c;帮助工地解决运营、管理…

cario库——C++画图

文章目录 RGBA1. 多个&#xff08;x,y&#xff09;坐标点&#xff0c;连成线2. 画圆3. 填充颜色4. 曲线图 RGBA rgb:红绿蓝 rgb(0,0,0)&#xff1a;黑色rgb(255,255,255)&#xff1a;白色 rgba:红绿蓝透明度&#xff08;0&#xff1a;完全透明&#xff0c;1&#xff1a;完全不…

如何挑选多用户商城源码?

数字化时代&#xff0c;电子商务已经成为了商业发展的重要方向。无论是大型企业还是个人创业者&#xff0c;都希望能够通过搭建一个多用户商城来拓展自己的业务&#xff0c;并与更多的消费者建立联系。 对于大多数人来说&#xff0c;从零开始开发一个多用户商城是一项巨大的挑战…

PTA 小字辈(树)

题目 本题给定一个庞大家族的家谱&#xff0c;要请你给出最小一辈的名单。 输入格式&#xff1a; 输入在第一行给出家族人口总数 N&#xff08;不超过 100 000 的正整数&#xff09; —— 简单起见&#xff0c;我们把家族成员从 1 到 N 编号。随后第二行给出 N 个编号&#…

minikube创建一个pod并暴露端口(使用docker驱动安装)

因为minikube使用service暴露端口是使用nodeIP:nodePort 而不是 localhost:nodePort 公开访问。我们只能使用kubectl的端口转发功能或者使用iptables的转发功能来实现外网服务暴露。 我这里使用shiro来举例 apiVersion: apps/v1 kind: Deployment metadata:name: shiro550 spe…

财务对账-财务收发存-业务收发存

务对账是指将公司的账目与银行等第三方提供的相关账单进行核对比对&#xff0c;以确定公司记录的交易是否与银行或其他第三方的记录一致。对账的具体步骤通常包括以下几个方面&#xff1a; 收集资料&#xff1a;首先需要收集公司的财务记录&#xff0c;包括公司银行账户的流水…

学员分享| 一个普通学员的HCIE-DATACOM备考之路!

大家好&#xff0c;我是G-LAB IT实验室的周同学&#xff0c;在这篇文章中&#xff0c;我将分享我的备考HCIE数通方向的心路历程。我的备考之路&#x1f447; ——备考理论—— 我从一年前开始了HCIE数通方向的备考。一开始&#xff0c;我并没有完全了解这个认证的难度和复杂性…

免费Scrum管理工具-Leangoo领歌

Leangoo领歌是一款永久免费的专业的敏捷开发管理工具&#xff0c;提供端到端敏捷研发管理解决方案&#xff0c;涵盖敏捷需求管理、任务协同、进展跟踪、统计度量等。 
 Leangoo领歌上手快、实施成本低&#xff0c;可帮助企业快速落地敏捷&#xff0c;提质增效、缩短周期、加速…

vue 插槽-默认插槽

vue 插槽-默认插槽 **创建 工程&#xff1a; H:\java_work\java_springboot\vue_study ctrl按住不放 右键 悬着 powershell H:\java_work\java_springboot\js_study\Vue2_3入门到实战-配套资料\01-随堂代码素材\day05\准备代码\07-插槽-默认插槽 vue --version vue create…

CMMI软件能力成熟度认证指南来了

CMMI能力成熟度模型集成&#xff0c;是一种评估或认证体系。其核心理念是&#xff1a;过程决定质量&#xff0c;这六个字能够让大家对CMMI有了一个大概的了解。是的&#xff0c;重点是过程&#xff0c;CMMI评估的核心内容也是过程。主要是CMMI研究院主任评估员根据CMMI模型检查…

torch版本对应的torch_geometric与torch-sprse/cluster/scatter库的正确安装

torch_geometric官网&#xff1a; Installation — pytorch_geometric documentation 使用上述标红命令即可快速安装需要的包&#xff08;确定自己环境中安装的pytorch版本以及cuda版本&#xff0c;使用对应的命令即可&#xff09; 如安装的pytorch为1.60&#xff0c;cuda为1…