精华内容
下载资源
问答
  • 临界区,互斥竞争条件 公共资源 可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。 临界区 程序要想使用某些资源,必然通过一些指令去访问这些资源,若多个任务都访问同一公共资源,那么各...

    临界区,互斥,竞争条件

    公共资源

    可以是公共内存、公共文件、公共硬件等,总之是被所有任务共享的一套资源。

    临界区

    程序要想使用某些资源,必然通过一些指令去访问这些资源,若多个任务都访问同一公共资源,那么各任务中访问公共资源的指令代码组成的区域就称为临界区。怕有同学看得不仔细,强调一下,临界区是指程序中那些访问公共资源的指令代码,即临界区是指令,并不是受访的静态公共资源

    互斥

    互斥也可称为排他, 是指某一时刻公共资源只能被 1 个任务独享, 即不允许多个任务同时出现在自己的临界区中。公共资源在任意时刻只能被一个任务访问,即只能有一个任务在自己的临界区中执行,其他任务想访问公共资源时,必须等待当前公共资源的访问者完全执行完他自己的临界区代码后(使用完资源后)再开始访问。

    竞争条件

    竞争条件是指多个任务以非互斥的方式同时进入临界区, 大家对公共资源的访问是以竞争的方式并行进行的,因此公共资源的最终状态依赖于这些任务的临界区中的微操作执行次序。

    非互斥条件下产生的错误行为

    当多个任务“同时”读写公共资源时,也就是多个任务“同时”执行它们各自临界区中的代码时,它们以混杂并行的方式访问同一资源, 因此后面任务会将前一任务的结果覆盖, 最终公共资源的结果取决于所有任务的执行时序。这里所说的“同时”也可以指多任务伪并行,总之是指一个任务在自己的临界区中读写公共资源,还没来得及出来(彻底执行完临界区所有代码),另一个任务也进入了它自己的临界区去访问同一资源。

    内容出自 郑钢 – 《操作系统真象还原》,详细了解具体内容还是得看原书,本人只是将部分内容摘录为学习笔记为用。

    展开全文
  • 竞争条件与互斥

    2018-04-03 22:30:05
    本文主要讲解多线程(进程)编程时出现竞争条件的解决方案及代码实现。 竞争条件 协同进程可能共享一些彼此都能够读写的公用存储区(例如打印机脱机系统),也可能是一些共享文件,当两个或多个进程读写某些...

    本文主要讲解多线程(进程)编程时出现竞争条件的解决方案及代码实现。

    竞争条件

    协同进程可能共享一些彼此都能够读写的公用存储区(例如打印机脱机系统),也可能是一些共享文件,当两个或多个进程读写某些共享数据,而最后的结果却绝育进程运行的精确时序,就称为竞争条件(race condition)

    如果程序之间有竞争条件,也许大部分的运行结果都很好,但在极少数情况下会发生一些难以解释的事情。

    互斥(mutual exclusion)

    要避免这种错误,关键是要找出某种途径防止多个进程在使用一个共享变量或文件时,其他进程不能做同样的事。

    互斥的实现有很多种,在UNIX编程中,总体来说有两种大的方案:

    1. 忙等待形式的互斥
      • 优势在于被等待的进程(线程)不需要context switch(no extra overhead),提高了效率
      • 但若等待时间较长,会浪费CPU资源。
      • 会造成优先级反转问题(priority inversion problem)
    2. 睡眠等待
      • CPU利用率较高,但会造成context switch的overhead。

    临界区

    把对共享内存进行访问的程序片段称为临界区或者临界段(critical region)。

    如果能够进行适当安排,使得两个进程不可能同时处于临界区,则能够避免竞争条件。

    我们认为一个好的方案应该能解决竞争条件的同时,依然高效地进行操作,满足以下四个条件:

    1. 任何两个进程不能同时处于临界区。
    2. 不应该对CPU的速度和树木做任何假设。
    3. 临界区外的进程不得阻塞其他进程。
    4. 不能使进程在临界区外无休止的等待。

    忙等待的互斥

    关闭中断

    这是最简单也最直接的方案,使得每个进程在进入临界区后先关闭中断,在离开之前再打开中断。

    中断被关闭后,时钟中断也会关闭。因此CPU在做完临界区之前都不会发生进程切换。

    缺点

    1. 把关闭中断的权利交给用户进程是不明智的。可能会造成系统终止。
    2. 不适用于多CPU情形。
    3. 在实际中很少采用。

    关闭中断对于操作系统是一项很有用的技术,但对于用户进程不是一种合适的通用互斥机制。

    锁变量

    设想有一个共享锁变量,在进程想要进入临界区时,先测试这把锁。

    但可以想象,如果锁变量依然是普通类型(不是原子类型),则依然会发生竞争条件。

    严格交替法

    首先看示意代码:

    /// process 0 ////
    while(true){  
        while(turn!=0);  
        critical_region();  
        turn=1;  
        noncritical_region();  
    }  
    /// process 1 ////
    while(true){  
        while(turn!=1);  
        critical_region();  
        turn=0;  
        noncritical_region();  
    }  

    process 0 必须在 turn 变量等于0时才会进入临界区,process 1 必须在 turn 变量等于1时才能进入临界区。

    假设turn变量初始化为0,则process 1会一直持续地检测一个变量,直到为1才执行下面的代码。这种等待为忙等待。一个适用于忙等待的锁称为自旋锁(spin lock)

    仔细观察以上代码,两个进程互相依靠对方提供的turn变量才能继续下去。若一个进程的noncritical_region() 很长,另一个必须等它完成后才停止while循环。因此,即使其代码在非临界区中,也会阻塞其他进程。

    因此,该方案违反了条件3:进程被一个临界区之外的进程阻塞,所以不能作为一个很好的方案。

    peterson解法

    结合了锁变量和轮换法的思想:

    #define N 2  
    #define FALSE 0  
    #define TRUE 1  
    int turn;  
    int interest[N]={FALSE};//所有值初始为0   
    void enter_region(int process){  
        interest[process] = TRUE;  
        turn = process;  
        int other = 1-process;  
        while(turn == process && interest[other] == TRUE);  
    }  
    void leave_region(int process){  
        interest[process] = FALSE;  
    }  

    我们主要关注enter_region可能发生的情况:

    1. 一个进程进入后,没有进程中断它,那么一直执行,不会发生空转;
    2. 若在turn = process;之后执行了下一个线程,那么下一个线程不会空转,直接执行;

    可以这样理解条件判断语句:

    • turn = process;不满足,则说明另一个进程一定处于等待中(本进程的interest为TRUE),因此可以进入临界区。
    • turn == process满足而 interest[other] == TRUE不满足,则说明这时候没有其他进程在等待进入,因此可以进入临界区。

    TSL上锁

    如果能有一种硬件解决方案,使得我们拿到了变量值,就不间断的更改这个值(原子操作),那将会是更有效的方法。

    现在的计算机都支持这种方式,并有一个特殊的指令TSL

    TSL RX, LOCK

    将一个存储器字读到寄存器中,然后在该内存的地址上存一个非零值。

    读数和写数操作保证是不可分割的,即该指令结束之前其他处理机均不允许访问该存储器字。

    使用这条指令来防止两个进程进入临界区的方案如下:

    52276162520

    程序将LOCK原来的值复制到寄存器中,并将LOCK值置为1,随后这个原先的值与0比较。若非0,则说明之前已经上锁,从而程序一直空转。

    并且,在现代操作系统中规定,拿到锁的进程即使没有时间片也要立即执行,这样会防止CPU的效率进一步下降。

    优先级反转问题

    例如H进程优先级高,L进程优先级低 ,假设L处于临界区中,H这时转到就绪态想进入临界区,H需要忙等待直到L退出临界区,但是H就绪时L不能调度,L由于不能调度无法退出临界区,所以H永远等待下去。

    因此,我们想知道,是否有其他方法,既能使CPU效率提高,也能解决优先级反转问题。

    睡眠等待

    考虑通信原语(primitive):sleep 和 wakeup。

    • sleep系统调用会引起进程阻塞,直到另一进程将其唤醒。
    • wakeup调用即将被唤醒的进程。

    生产者消费者问题

    两个进程共享一个公共的固定大小的缓冲区,其中一个是生产者,负责将信息放入缓冲区;一个是消费者,负责从缓冲区中读取信息。但如果我们使用常规的count变量记录缓冲区数量时,还是会出现两个进程永远睡眠的情况:

    #define N 100  
    int count = 0;  
    void producer(void)  
    {  
        int item;  
        while(TRUE)  
        {  
            item = produce_item();  
            if(count == N)                  //如果缓冲区满就休眠  
            sleep();  
            insert_item(item);  
            count = count + 1;              //缓冲区数据项计数加1  
            if(count == 1)  
            wakeup(consumer);  
        }  
    }  
    
    void consumer(void)  
    {  
        int item;  
        while(TRUE)  
        {  
            if(count == 0)              //如果缓冲区空就休眠  
                sleep();  
            item = remove_item();  
            count = count - 1;          //缓冲区数据项计数减1  
            if(count == N - 1)  
                wakeup(producer);  
            consume_item(item);  
        }  

    一种解决方案是增加一个唤醒等待位,当一个清醒的进程发送一个唤醒信号时,将该位置设为1;当程序要睡眠时,如果唤醒等待位为1,则清零,但不会睡眠。

    信号量

    引入一个整型变量来累计唤醒次数,称为信号量(semaphore)。一个信号量为非负数。

    常用的为binary semaphore:down 和 up。

    • down 操作是检查其值是否大于0,若为真,则减一,若为0,则进程将睡眠,并且,检查数值,改变数值以及可能发生的睡眠操作是单一的原子操作(atomic action)
    • up 操作是递增信号量的值。对于一个进程,若有睡眠的进程,则信号量执行一次up操作后,信号量依然为0,但在其上的睡眠进程减少一个(唤醒一个)。
    #define N 100
    typedef int semaphore;
    semaphore mutex = 1;
    semaphore empty = N;
    semaphore full = 0;
    void producer(void)
    {
        int item;
        while(TRUE)
        {
            item = produce_item();
            down(&empty);               //空槽数目减1,相当于P(empty)
            down(&mutex);               //进入临界区,相当于P(mutex)
            insert_item(item);          //将新数据放到缓冲区中
            up(&mutex);             //离开临界区,相当于V(mutex)
            up(&full);              //满槽数目加1,相当于V(full)
        }
    }
    void consumer(void)
    {
        int item;
        while(TRUE)
        {
            down(&full);                //将满槽数目减1,相当于P(full)
            down(&mutex);               //进入临界区,相当于P(mutex)
            item = remove_item();            //从缓冲区中取出数据
            up(&mutex);             //离开临界区,相当于V(mutex)     
            up(&empty);             //将空槽数目加1 ,相当于V(empty)
            consume_item(item);         //处理取出的数据项
        }
    }

    我们使用两种不同的方法来使用信号量。

    • 信号量mutex用于互斥。保证任意时刻只有一个进程读写缓冲区和相关的变量
    • 信号量full与empty用于保证一定的事件顺序发生或不发生。用于同步

    互斥(mutex)

    若不需要信号量的计数能力,可以用于互斥:是一个处于两种变量之间(解锁和加锁)的变量。

    适用于两个过程:

    1. 当一个进程需要进入临界区时,调用mutex_lock,如果此时互斥是解锁的,那么调用进程可以进入临界区。
    2. 若该互斥已经加锁,调用者被阻塞,等待在临界区中的进程完成操作并调用mutex_unlock退出为止。

    多线程实现

    这里以银行汇款为例子,讲解在竞争条件或互斥下的不同状态。若A和B都有10000元,考虑同时汇款的情形(thread1、thread2),我们可以写出以下代码:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <pthread.h>
    
    int sharedi = 0;
    int A = 10000;
    int B = 10000;
    
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    void thread1()
    {
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            //pthread_mutex_lock(&mutex);
            A += 100;
            sleep(1);
            B -= 100;
            printf("thread1: A  = %d , B = %d\n", A, B);
            //pthread_mutex_unlock(&mutex);
        }
    }
    
    void thread2()
    {
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            //pthread_mutex_lock(&mutex);
            A += 100;
            sleep(1);
            B -= 100;
            printf("thread2: A  = %d , B = %d\n", A, B);
            //pthread_mutex_unlock(&mutex);
        }
    }
    
    int main()
    {
        int ret;
        pthread_t thrd1, thrd2;
    
        ret = pthread_create(&thrd1, NULL, (void *)thread1, NULL);
        ret = pthread_create(&thrd2, NULL, (void *)thread2, NULL);
        pthread_join(thrd1, NULL);
        pthread_join(thrd2, NULL);
    
        return 0;
    }

    若直接运行,我们会得到很奇怪的答案:

    会发现每一次汇款后,钱的总数不再等于20000,因此发生了竞争条件。

    我们使用POSIX提供的mutex函数加锁后:

    这样就得到了我们想要的答案。

    参考资料

    1. 忙等待的互斥
    2. Operating System:Design and Implementation,Third Edition
    3. 多线程的同步和互斥

    欢迎关注我的个人博客

    展开全文
  • 使用互斥锁解决线程资源竞争: 什么是线程? 可以简单理解为同一进程中有多个计数器,每个线程的执行时间不确定,而每个进程的时间片相等,线程是操作系统调度执行的最小单位. 线程的创建步骤: import ...

      使用互斥锁解决线程资源竞争:

      什么是线程?

      可以简单理解为同一进程中有多个计数器,每个线程的执行时间不确定,而每个进程的时间片相等,线程是操作系统调度执行的最小单位.

     线程的创建步骤:

    import threading
    # 创建一个线程对象
    t1 = threading.Thread(target=func_name, args=(num,), name=”子线程名字”)
    # 创建一个线程并启动
    t1.start()
    # 等待子线程执行完毕之后再继续向下执行主线程
    t1.join()

     注:主线程会等待子线程结束之后才会结束,主线程一死,子线程也会死。线程的调度是随机的,并没有先后顺序。

     

     死锁的概念:在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁,尽管死锁很少发生,但一旦发生就会造成应用的停止响应。

    互斥锁的运用:由于多线程之间共享全局变量就会导致出现资源竞争的问题,为了避免这种竞争出现,利用互斥锁可以实现线程同步。

    # 创建锁
    Mutex = threading.Lock()
    # 加锁
    Mutex.acquire()
    # 释放锁
    Mutex.release()

       使用互斥锁解决线程资源竞争案例分析: 

     

    import threading
    import time
    
    g_num = 100
    
    
    def work1(num):
        global g_num
        mutex.acquire()
        for i in range(num):
            g_num += 1
        mutex.release()
        print("work1 g_num is:%d" % g_num)
    
    
    def work2(num):
        global g_num
        mutex.acquire()
        for i in range(num):
            g_num += 1
        mutex.release()
        print("work2 g_num is:%d" % g_num)
    
    
    # 创建一个锁
    mutex = threading.Lock()
    
    
    def main():
        t1 = threading.Thread(target=work1, args=(1000000,))
        t2 = threading.Thread(target=work2, args=(1000000,))
        t1.start()
        t2.start()
        # 等上面两个线程结束
        time.sleep(3)
        print("main g_num is: %d" % g_num)
    
    
    if __name__ == '__main__':
        main()

    运行结果:

    展开全文
  • 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁 某个线程要更改共享数据时,现将其锁定,此时资源的状态为“锁定”,其他线程不能更改,直到该线程释放,将资源的状态改为“非锁定”,...

    接上一篇

    一.互斥锁

    互斥锁: 为资源引入一个状态,锁定/非锁定

    • 当多个线程几乎同时修改一个共享数据时,需要进行同步控制
    • 线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁
    • 某个线程要更改共享数据时,现将其锁定,此时资源的状态为“锁定”,其他线程不能更改,直到该线程释放,将资源的状态改为“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

    二.使用互斥锁解决资源竞争问题

    方法1:

     	import threading
        import time
        
        g_num = 0
        # 添加互斥锁
        mutex = threading.Lock()
        
        
        def test1(num):
            global g_num
            # 上锁,如果之前没有被上锁,那么此时上锁成功
            # 如果上锁之前已经被上锁则会堵塞,直到这个锁被解开
            mutex.acquire()
            for i in range(num):
                g_num += 1
            mutex.release()
            print("----in test1 g_num=%d" % g_num)
        
        
        def test2(num):
            global g_num
            mutex.acquire()
            for i in range(num):
                g_num += 1
            mutex.release()
            print("----in test2 g_num=%d" % g_num)
        
        
        def main():
            t1 = threading.Thread(target=test1,args=(1000000,))
            t2 = threading.Thread(target=test2,args=(1000000,))
        
            t1.start()
            t2.start()
        
            time.sleep(2)
        
            print("---in main Thread g_num=%d" % g_num)
        
        
        if __name__ == '__main__':
            main()
    

    在这里插入图片描述
    方法2:原则:互斥锁的范围越小越好

    import threading
    import time
    
    g_num = 0
    # 添加互斥锁
    mutex = threading.Lock()
    
    
    def test1(num):
        global g_num
        for i in range(num):
            mutex.acquire()
            g_num += 1
            mutex.release()
        print("----in test1 g_num=%d" % g_num)
    
    
    def test2(num):
        global g_num
        for i in range(num):
            mutex.acquire()
            g_num += 1
            mutex.release()
        print("----in test2 g_num=%d" % g_num)
    
    
    def main():
        t1 = threading.Thread(target=test1,args=(1000000,))
        t2 = threading.Thread(target=test2,args=(1000000,))
    
        t1.start()
        t2.start()
    	time.sleep(2)
    
    	print("---in main Thread g_num=%d" % g_num)
    
    if __name__ == '__main__':
        main()
    

    在这里插入图片描述

    三.死锁

    避免死锁:

    • 程序设计时要尽量避免(银行家算法)
    • 添加超时时间

    银行家算法:
    银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。

    算法原理

    我们可以把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款。
    为保证资金的安全,银行家规定:
    (1) 当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
    (2) 顾客可以分期贷款,但贷款的总数不能超过最大需求量;
    (3) 当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
    (4) 当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金.
    操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能满足则按当前的申请量分配资源,否则也要推迟分配。

    展开全文
  • 一、同步、竞争互斥 当多个线程同时访问其共享的资源时,需要相互协调,以防止出现数据不一致、不完整的问题,能达到这种状态线程同步。 而有些资源在同一时刻只有一个线程访问,对于这种资源的访问需要竞争。 当...
  • Python写的多线程使用互斥锁解决资源竞争的问题的代码,可以直接运行,并且带中文注释,方便初学者学习和使用。
  • ​ 由于线程之间的绝大多数资源都是共享的,当多个线程同时访问一个资源时,可能会出现数据不完整、不一致的情况,此时就需要多个线程协调访问资源(竞争访问,需要用于互斥技术),最终达到数据一致、完整(同步)。...
  • 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确, 程序示例: import threading import time num1 = 0 def demo1(a): global num1 for i in range(a): num1 += 1 print("在...
  • -----test1----num:100 -----test1----num:200 ----main----num:200
  • 使用互斥锁解决资源竞争 同步概念 同步就是协同步调,按预定的先后次序进行运行,如:你说完,我再说。 ‘同’字从字面上容易理解为一起动作 其实不是,‘同’字应是指协同,协助,互相配合。 互斥锁 当多个...
  • 代码来源自百度传课教学视频:https://chuanke.baidu.com/v3891329-172880-843280.html 意看注释。 #include <iostream> #include <thread> #include <mutex> #include <......
  • [align=center][size=large]线程间的竞争关系与线程互斥[/size][/align] [size=large]1.线程间的竞争关系[/size] 同一个进程中的多个线程由系统调度而并发执行时,彼此之间没有直接联系,并不知道其他线程的存在...
  • 1 互斥体简介 自旋锁和互斥体都是解决互斥问题的...如果竞争失败,会发生进程上下文切换,当前进程进入睡眠状态, CPU将运行其他进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间较长时,用互斥
  • 竞争条件,临界区,忙等待的互斥

    千次阅读 2018-05-01 15:50:16
    一、竞争条件和临界区在同一个应用程序中运行多个线程本身并不会引起问题。当多个线程访问相同的资源时才会出现问题。比如多个线程访问同一块内存区域(变量、数组、或对象)、系统(数据库、 web 服务等)或文件。...
  • 互斥

    2019-11-08 22:22:22
    资源还是共享的,线程也还是竞争的; 但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。 但即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。 主要应用函数: ...
  • 在本文中,我们将讨论如何使用互斥锁来保护多线程环境中的共享数据并避免竞争情况。 为了解决多线程环境中的竞争条件,我们需要互斥锁,即每个线程都需要在修改或读取共享数据之前锁定互斥锁,并且在修改数据之后,...
  • 多线程竞争 同一个进程里线程是数据共享的,当各个线程访问数据资源时会出现竞争状态,数据几乎同步会被多个线程占用,造成数据混乱。 锁 Python提供的对线程控制的对象。 锁的好处: 确保了某段关键代码只能由一个...
  • 互斥

    2019-10-23 10:59:17
    1、互斥锁基本原理: 互斥锁是一个二元变量,其状态为开锁(允许0)和上锁(禁止1),将某个共享资源与某个特定互斥锁在逻辑上绑定(要申请该资源必须先获取锁)。 (1)访问公共资源前,必须申请该互斥锁,若处于开锁状态...
  • 为了解决多线程环境中的资源竞争,我们需要互斥锁,即每个线程都需要在修改或读取共享数据之前锁定互斥锁,并且在修改数据之后,每个线程都应解锁互斥锁。 5.1 std::mutex 在C++11线程库中,互斥锁位于mutex头文件中
  • java资源竞争问题(线程互斥)

    千次阅读 2014-05-22 13:31:56
    资源竞争 (线程互斥)  1、什么是资源竞争   有这样一种资源,在某一时刻只能被一个线程所使用:比如打印机、某个文件等等,如果多个线程不加控制的同时使用这类资源,必然会导至错误。   下面的例子模拟了一...
  • 使用互斥锁解决多任务之间资源竞争问题 问题的提出 当我们使用多任务编程的时候,假如多个线程对同一个变量进行调用,由于操作系统对于 各个线程调用的顺序和频率是不同的,当单核CPU执行某个线程到一半的时候...
  • 一、概念(1)竞争状态(简称竟态);(2)临界段(某一段代码,该代码有可能造成并发,因此应该添加锁。这段代码应该尽可能地短)、互斥锁、死锁(由于互斥锁使用不对,导致的后果);(3)同步与并发(多CPU、(单...
  • 5.同步互斥机制

    2017-07-13 22:05:50
    5.1进程并发执行 并发是所有问题产生的基础,并发是操作系统设计的基础。 从进程的特征出发: ...竞争条件:两个或多个进程在读写某些共享数据的时候,而最后的结果取决于进程运行的精确时序。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 62,111
精华内容 24,844
关键字:

互斥竞争