Python 개발의 프로세스 및 스레드 개요

零下一度
풀어 주다: 2017-06-25 10:20:29
원래의
1555명이 탐색했습니다.

머리말

프로세스 및 스레드 개요:

많은 학생들은 Mac OS X, UNIX, Linux, Windows 등과 같은 최신 운영 체제가 모두 "멀티태스킹"을 지원한다고 들었습니다. "운영 체제.

 "멀티태스킹"이란 무엇입니까? 간단히 말해서 운영 체제는 동시에 여러 작업을 실행할 수 있습니다. 예를 들어, 브라우저를 사용하여 인터넷을 서핑하고, MP3 플레이어를 듣고, Word에서 숙제를 하고 있습니다. 이는 동시에 세 가지 작업이 실행되는 것입니다. 동시에 백그라운드에서 조용하게 실행되는 많은 작업이 있지만 데스크탑에는 표시되지 않습니다.

요즘에는 멀티코어 CPU가 큰 인기를 얻었지만 과거의 싱글코어 CPU도 멀티태스킹을 수행할 수 있습니다. CPU 실행 코드는 순차적으로 실행되므로 단일 코어 CPU는 어떻게 여러 작업을 수행합니까?

운영 체제는 각 작업을 교대로 교대로 실행한다는 것입니다. 작업 1은 0.01초 동안 실행되고, 작업 2로 전환되고, 작업 2는 0.01초 동안 실행된 다음, 작업 3으로 전환되고, 0.01초 동안 실행됩니다. ... 등등. 표면적으로는 각 작업이 번갈아 가며 실행되지만, CPU의 실행 속도가 너무 빠르기 때문에 우리는 마치 모든 작업이 동시에 실행되는 것처럼 느껴집니다.

  진정한 병렬멀티 태스킹 실행멀티 코어 CPU에서만 달성할 수 있습니다. 그러나 작업 수가 CPU 코어 수보다 훨씬 많기 때문에 운영 체제도 마찬가지입니다. 각 코어에서 차례로 실행되는 많은 작업을 자동으로 예약합니다.

운영 체제의 경우 작업은 프로세스(프로세스)입니다. 예를 들어 브라우저를 열면 브라우저 프로세스가 시작되고, 메모장을 열면 메모장 프로세스가 시작되고, 메모장 두 개를 열면 두 개의 메모장 프로세스가 생성됩니다. Word를 열면 Word 프로세스가 시작됩니다.

입력, 맞춤법 검사, 인쇄 등을 동시에 수행할 수 있는 Word와 같은 일부 프로세스는 두 개 이상의 작업을 동시에 수행할 수 있습니다. 프로세스 내에서 여러 작업을 동시에 수행하려면 동시에 여러 "하위 작업"을 실행해야 합니다. 프로세스 내에서는 이러한 "하위 작업"을 스레드(스레드)이라고 합니다.

각 프로세스는 최소한 한 가지 작업을 수행해야 하므로 프로세스 에는 최소한 하나의 스레드 가 있어야 합니다. 물론, 워드와 같은 복잡한 프로세스는 여러 개의 스레드를 가질 수 있고, 여러 스레드가 동시에 실행될 수 있습니다. 멀티스레딩의 실행 방법은 멀티프로세스와 동일하며, 운영체제도 여러 스레드 사이를 빠르게 전환합니다. , 각 스레드가 잠시 교대로 실행되어 동시에 실행되는 것처럼 보입니다. 물론 실제로 여러 스레드를 동시에 실행하려면 멀티 코어 CPU가 필요합니다. 앞서 작성한 모든 Python 프로그램은 단일 작업을 수행하는 프로세스입니다. 즉, 스레드가 하나만 있습니다. 여러 작업을 동시에 수행하고 싶다면 어떻게 해야 할까요?

 

두 가지 해결 방법이 있습니다.

 하나는 여러 프로세스를 시작하는 것입니다. 각 프로세스에는 하나의 스레드만 있지만 여러 프로세스가 함께 여러 작업을 수행할 수 있습니다.

또 다른 방법은 프로세스를 시작하고 한 프로세스에서 여러 스레드를 시작하여 여러 스레드가 여러 작업을 함께 수행할 수 있도록 하는 것입니다.

물론 세 번째 방법이 있습니다. 여러 프로세스를 시작하는 것인데, 각 프로세스는 여러 스레드를 시작하여 동시에 더 많은 작업을 실행할 수 있습니다. 물론 이 모델은 더 복잡하고 거의 사용되지 않습니다. 실제로 .

요약하자면, 멀티태스킹을 구현하는 세 가지 방법이 있습니다:

  • 다중 프로세스 모드;


    • 추가 프로세스 + 다중 스레드 모드.

  동시에 여러 작업을 실행합니다. 일반적으로 작업은 서로 연관되어 있지 않지만 때로는 작업 1을 일시 중지하고 작업 2가 완료될 때까지 기다려야 합니다. 때로는 작업 3과 작업 4를 동시에 실행할 수 없기 때문에 다중 프로세스 및 다중 스레드 프로그램의 복잡성은 이전에 작성한 단일 프로세스 및 단일 스레드 프로그램보다 훨씬 높습니다.

디버깅이 복잡하고 어렵기 때문에 꼭 필요한 경우가 아니면 멀티태스킹을 작성하고 싶지 않습니다. 하지만 멀티태스킹 없이는 불가능할 때가 많습니다. 컴퓨터에서 영화를 보는 경우를 생각해 보세요. 한 스레드는 비디오를 재생하고 다른 스레드는 오디오를 재생해야 합니다. 그렇지 않으면 단일 스레드로 구현된 경우 비디오를 먼저 재생한 다음 오디오를 재생해야 합니다. 그러면 비디오는 분명히 불가능합니다.

 Python은 멀티 프로세싱과 멀티 스레딩을 모두 지원하는 멀티 태스킹 프로그램을 작성하는 방법에 대해 논의하겠습니다.

Process

첫 만남 :

 파이썬 프로그램에서 멀티프로세싱(multiprocessing)을 구현하기 위해서는 먼저 운영체제 관련 지식을 이해해야 합니다. Unix/Linux 운영 체제는 매우 특별한 fork()fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:

import os

print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
    print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
    print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
    
# Process (44587) start...
# I (44587) just created a child process (44588).
# I am child process (44588) and my parent is 44587.
로그인 후 복사

由于Windows没有fork调用,上面的代码在Windows上无法运行。由于Mac系统是基于BSD(Unix的一种)内核,所以,在Mac下运行是没有问题的,推荐大家用Mac学Python!有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。

multiprocessing模块:

  如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。multiprocessing模块提供了一个Process 시스템 호출을 제공합니다. 일반적인 함수 호출은 한 번 호출하고 한 번 반환하지만 fork()는 한 번 호출하고

두 번
을 반환합니다. 왜냐하면 운영 체제가 자동으로(부모 프로세스라고 함) ) 복사본(자식 프로세스라고 함)을 만든 다음 각각 상위 프로세스와 하위 프로세스로 반환됩니다.
하위 프로세스는 항상
0
을 반환하는 반면, 상위 프로세스는 하위 프로세스의 ID

를 반환합니다. 그 이유는 상위 프로세스가 많은 하위 프로세스를 분기할 수 있으므로 상위 프로세스는 각 하위 프로세스의 ID를 기록해야 하고 하위 프로세스는

<만 호출하면 되기 때문입니다. code> getppid()

부모 프로세스의 ID를 얻을 수 있습니다. Python의

os

module은 Python 프로그램에서 하위 프로세스를 쉽게 생성할 수 있는 fork를 포함한 일반적인 시스템 호출을 캡슐화합니다.

import os
import time

# 子进程要执行的代码
def run_proc(name):
    time.sleep(1)
    print(&#39;Run child process %s (%s)...&#39; % (name, os.getpid()))

if __name__==&#39;__main__&#39;:
    print(&#39;Parent process %s.&#39; % os.getpid())
    p = Process(target=run_proc, args=(&#39;test&#39;,))    # args里面为何要用,隔开?
    p.start()                                        # 子进程启动,不加这个子进程不执行
    p.join()             # 等待子进程p的执行完毕后再向下执行,不加此项,主程序执行完毕,子进程依然会继续执行不受影响
    print(&#39;Child process end.&#39;),

# Parent process 8428.
# Run child process test (9392)...
# Child process end.
로그인 후 복사
🎜🎜 Windows에는 fork 호출이 없으므로 위 코드는 Windows에서 실행될 수 없습니다. Mac 시스템은 BSD(Unix의 일종) 커널을 기반으로 하기 때문에 Mac에서 실행하는데 문제가 없습니다. Python을 배우려면 Mac을 사용하는 것이 좋습니다! fork 호출을 사용하면 프로세스는 새 작업을 수신할 때 새 작업을 처리하기 위해 하위 프로세스를 복사할 수 있습니다. request 새로운 http 요청을 처리하기 위해 하위 프로세스가 분기되는 경우입니다. 🎜🎜🎜🎜🎜🎜🎜다중 처리 모듈:🎜🎜🎜🎜 다중 프로세스 서비스 프로그램을 작성하려는 경우 Unix/Linux가 의심할 여지 없이 올바른 선택입니다. Windows에는 🎜fork🎜 호출이 없으므로 Windows에서 Python으로 다중 프로세스 프로그램을 작성하는 것이 불가능합니까? Python은 크로스 플랫폼이므로 자연스럽게 크로스 플랫폼 다중 프로세스 지원을 제공해야 합니다. 🎜다중 처리🎜이 모듈은 다중 프로세스 모듈의 크로스 플랫폼 버전입니다. 🎜multiprocessing🎜 모듈은 프로세스 개체를 나타내는 🎜Process🎜 클래스를 제공합니다. 다음 예에서는 하위 프로세스를 시작하고 끝날 때까지 기다리는 방법을 보여줍니다. 🎜🎜
<span style="font-size: 13px; color: #800000">Process实例化时执行self._args = tuple(args)操作,如果不用,隔开生成的slef._args就是一个个字母了,传入两个参数以上是就不用加,号了,如下:<br/></span>
로그인 후 복사
🎜
    def __init__(self, group=None, target=None, name=None, args=(), kwargs={},                 *, daemon=None):assert group is None, &#39;group argument must be None for now&#39;count = next(_process_counter)
        self._identity = _current_process._identity + (count,)
        self._config = _current_process._config.copy()
        self._parent_pid = os.getpid()
        self._popen = None
        self._target = target
        self._args = tuple(args)

a =(&#39;ers&#39;)
b = tuple(a)print(b)# (&#39;e&#39;, &#39;r&#39;, &#39;s&#39;)a1 =(&#39;ers&#39;,&#39;gte&#39;)
b1 = tuple(a1)print(b1)# (&#39;ers&#39;, &#39;gte&#39;)
로그인 후 복사
🎜🎜🎜🎜
from multiprocessing import Pool,cpu_count
import os, time, random

def long_time_task(name):
    print(&#39;Run task %s (%s)...&#39; % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print(&#39;Task %s runs %0.2f seconds.&#39; % (name, (end - start)))

def Bar(arg):
    print(&#39;-->exec done:&#39;,arg,os.getpid())

if __name__==&#39;__main__&#39;:
    print(&#39;Parent process %s.&#39; % os.getpid())
    p = Pool(cpu_count())               # 获取当前cpu核数,多核cpu的情况下多进程才能实现真正的并发
    for i in range(5):
        # p.apply_async(func=long_time_task, args=(i,), callback=Bar) #callback回调 执行完func后再执行callback 用主程序执行
        p.apply_async(long_time_task, args=(i,))
    print(&#39;Waiting for all subprocesses done...&#39;)
    p.close()
    p.join()              # !等待进程池执行完毕,不然主进程执行完毕后,进程池直接关闭
    print(&#39;All subprocesses done.&#39;)

# Parent process 4492.
# Waiting for all subprocesses done...
# Run task 0 (3108)...
# Run task 1 (7936)...
# Run task 2 (11236)...
# Run task 3 (8284)...
# Task 2 runs 0.86 seconds.
# Run task 4 (11236)...
# Task 0 runs 1.34 seconds.
# Task 1 runs 1.49 seconds.
# Task 3 runs 2.62 seconds.
# Task 4 runs 1.90 seconds.
# All subprocesses done.
로그인 후 복사
🎜🎜프로세스 코드🎜🎜🎜🎜🎜🎜🎜풀 프로세스 풀: 🎜🎜🎜🎜🎜많은 수의 하위 프로세스를 시작하려면 프로세스 풀을 사용하여 일괄적으로 하위 프로세스를 생성할 수 있습니다. : 🎜🎜 🎜🎜 🎜
from multiprocessing import Pool,cpu_countimport os, time, randomdef long_time_task(name):print(&#39;Run task %s (%s)...&#39; % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()print(&#39;Task %s runs %0.2f seconds.&#39; % (name, (end - start)))def count_process():import psutil
    pids = psutil.pids()
    process_name = []for pid in pids:
        p = psutil.Process(pid)
        process_name.append(p.name())       # 获取进程名# process_name.append(p.num_threads())       # 获取进程的线程数# print process_nameprint len(process_name)if __name__==&#39;__main__&#39;:print(&#39;Parent process %s.&#39; % os.getpid())
    p = Pool(4)for i in range(5):
        p.apply_async(long_time_task, args=(i,))print(&#39;Waiting for all subprocesses done...&#39;)
    count_process()        # 进程池开始时进程数(包含系统其他应用进程)    p.close()
    p.join()             
    count_process()        # 进程池关闭时进程数print(&#39;All subprocesses done.&#39;)# Parent process 8860.# Waiting for all subprocesses done...# Run task 0 (2156)...# Run task 1 (1992)...# Run task 2 (10680)...# Run task 3 (11216)...# 109           开始# Task 2 runs 0.93 seconds.# Run task 4 (10680)...# Task 1 runs 1.71 seconds.# Task 3 runs 2.01 seconds.# Task 0 runs 2.31 seconds.# Task 4 runs 2.79 seconds.# 105           结束# All subprocesses done.
로그인 후 복사
로그인 후 복사
🎜

重点:另进程池里的进程执行完毕后,进程关闭自动销毁,不再占用内存,同理,非进程池创建的子进程,执行完毕后也是自动销毁,具体测试如下:

from multiprocessing import Pool,cpu_countimport os, time, randomdef long_time_task(name):print(&#39;Run task %s (%s)...&#39; % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()print(&#39;Task %s runs %0.2f seconds.&#39; % (name, (end - start)))def count_process():import psutil
    pids = psutil.pids()
    process_name = []for pid in pids:
        p = psutil.Process(pid)
        process_name.append(p.name())       # 获取进程名# process_name.append(p.num_threads())       # 获取进程的线程数# print process_nameprint len(process_name)if __name__==&#39;__main__&#39;:print(&#39;Parent process %s.&#39; % os.getpid())
    p = Pool(4)for i in range(5):
        p.apply_async(long_time_task, args=(i,))print(&#39;Waiting for all subprocesses done...&#39;)
    count_process()        # 进程池开始时进程数(包含系统其他应用进程)    p.close()
    p.join()             
    count_process()        # 进程池关闭时进程数print(&#39;All subprocesses done.&#39;)# Parent process 8860.# Waiting for all subprocesses done...# Run task 0 (2156)...# Run task 1 (1992)...# Run task 2 (10680)...# Run task 3 (11216)...# 109           开始# Task 2 runs 0.93 seconds.# Run task 4 (10680)...# Task 1 runs 1.71 seconds.# Task 3 runs 2.01 seconds.# Task 0 runs 2.31 seconds.# Task 4 runs 2.79 seconds.# 105           结束# All subprocesses done.
로그인 후 복사
로그인 후 복사
进程池子进程执行完毕后销毁

代码解读:

Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。

请注意输出的结果,task 0123是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:

p = Pool(5)
로그인 후 복사

就可以同时跑5个进程。

由于Pool的默认大小是CPU的核数,如果你不幸拥有8核CPU,你要提交至少9个子进程才能看到上面的等待效果。

进程间通信:

  Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print(&#39;Process to write: %s&#39; % os.getpid())
    for value in [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;]:
        print(&#39;Put %s to queue...&#39; % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    print(&#39;Process to read: %s&#39; % os.getpid())
    while True:
        value = q.get(True)
        print(&#39;Get %s from queue.&#39; % value)

if __name__==&#39;__main__&#39;:
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()      # 强制关闭子进程

# Process to write: 9472
# Put A to queue...
# Process to read: 3948
# Get A from queue.
# Put B to queue...
# Get B from queue.
# Put C to queue...
# Get C from queue.
로그인 후 복사

在Unix/Linux下,multiprocessing模块封装了fork()调用,使我们不需要关注fork()的细节。由于Windows没有fork调用,因此,multiprocessing需要“模拟”出fork的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所有,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。

进程间共享数据:

有时候我们不仅仅需要进程间数据传输,也需要多进程间进行数据共享,即可以使用同一全局变量;如:为何下面程序的列表输出为空?

from multiprocessing import Process, Manager
import os

# manager = Manager()
vip_list = []
#vip_list = manager.list()

def testFunc(cc):
    vip_list.append(cc)
    print &#39;process id:&#39;, os.getpid()

if __name__ == &#39;__main__&#39;:
    threads = []

    for ll in range(10):
        t = Process(target=testFunc, args=(ll,))
        t.daemon = True
        threads.append(t)

    for i in range(len(threads)):
        threads[i].start()

    for j in range(len(threads)):
        threads[j].join()

    print "------------------------"
    print &#39;process id:&#39;, os.getpid()
    print vip_list

# process id: 9436
# process id: 11120
# process id: 10636
# process id: 1380
# process id: 10976
# process id: 10708
# process id: 2524
# process id: 9392
# process id: 10060
# process id: 8516
# ------------------------
# process id: 9836
# []
로그인 후 복사

如果你了解 python 的多线程模型,GIL 问题,然后了解多线程、多进程原理,上述问题不难回答,不过如果你不知道也没关系,跑一下上面的代码你就知道是什么问题了。因为进程间内存是独立的

正如上面提到的,在进行并发编程时,最好尽可能避免使用共享状态。在使用多个进程时尤其如此。但是,如果您确实需要使用一些共享数据,那么多处理提供了两种方法。

共享内存:

数据可以使用值或数组存储在共享内存映射中。例如,下面的代码:

from multiprocessing import Process, Value, Array

def f(n, a):
    n.value = 3.1415927
    for i in range(len(a)):
        a[i] = -a[i]

if __name__ == &#39;__main__&#39;:
    num = Value(&#39;d&#39;, 0.0)
    arr = Array(&#39;i&#39;, range(10))

    p = Process(target=f, args=(num, arr))
    p.start()
    p.join()

    print num.value
    print arr[:]
    
# 3.1415927
# [0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
로그인 후 복사

在创建num和arr时使用的“i”和“i”参数是数组模块使用的类型的类型:“表示双精度浮点数”,“i”表示一个已签名的整数。这些共享对象将是进程和线程安全的。为了更灵活地使用共享内存,您可以使用多处理。sharedctypes模块支持创建从共享内存中分配的任意类型的ctypes对象。

服务进程:

manager()返回的manager对象控制一个保存Python对象的服务器进程,并允许其他进程使用代理来操作它们。manager()返回的管理器将支持类型列<span class="pre">list</span>, <span class="pre">dict</span>, <span class="pre">Namespace</span>, <span class="pre">Lock</span>, <span class="pre">RLock</span>, <span class="pre">Semaphore</span>, <span class="pre">BoundedSemaphore</span>, <span class="pre">Condition</span>, <span class="pre">Event</span>, <span class="pre">Queue</span>, <span class="pre">Value</span> and <span class="pre">Array</span>。如下:

from multiprocessing import Process, Manager

def f(d, l):
    d[1] = &#39;1&#39;
    d[&#39;2&#39;] = 2
    d[0.25] = None
    l.reverse()

if __name__ == &#39;__main__&#39;:
    manager = Manager()

    d = manager.dict()
    l = manager.list(range(10))

    p = Process(target=f, args=(d, l))
    p.start()
    p.join()

    print d
    print l

# {0.25: None, 1: &#39;1&#39;, &#39;2&#39;: 2}
# [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
로그인 후 복사

服务器进程管理器比使用共享内存对象更灵活,因为它们可以用来支持任意对象类型。另外,单个管理器可以通过网络上不同计算机上的进程共享。但是,它们比使用共享内存要慢。

更多-》》点击

小结

在Unix/Linux下,可以使用fork()调用实现多进程。

要实现跨平台的多进程,可以使用multiprocessing模块。

进程间通信是通过Queue(多进程间)Pipes(两个进程间)等实现的。

补充小知识点-》父进程开辟子进程,子进程开辟子子进程,如果把子进程杀掉,子子进程会被杀死吗?

import timefrom multiprocessing import Processimport osdef count_process():import psutil
    pids = psutil.pids()print len(pids)def test3():
    count_process()for i in range(10):print "test3 %s"%os.getpid()
        time.sleep(0.5)def test1():print "test1 %s"%os.getpid()
    p2 = Process(target=test3, name="protest2")
    p2.start()
    p2.join()if __name__ == &#39;__main__&#39;:
    count_process()
    p1 = Process(target=test1, name="protest1")
    p1.start()
    time.sleep(2)
    p1.terminate()
    time.sleep(2)
    count_process()for i in range(10):print(i)
        time.sleep(1)# 86# test1 9500# 88# test3 3964# test3 3964# test3 3964# test3 3964# test3 3964# test3 3964# test3 3964# test3 3964# 87# 0# test3 3964# test3 3964# 1# 2# 3# 4# 5# 6# 7# 8# 9
로그인 후 복사
子子进程的心路历程

 

위 내용은 Python 개발의 프로세스 및 스레드 개요의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿