美文网首页
多线程与同步, since 2020-11-03

多线程与同步, since 2020-11-03

作者: Mc杰夫 | 来源:发表于2020-11-03 22:42 被阅读0次

    (2020.11.03 Tues)

    并发Concurrency

    并发指的是在单个系统里同时执行多个独立的活动。单核计算机中,同一时刻只能真正执行一个任务,通过任务切换(task switch),看起来像是任务在并行发生,这样的系统仍然被称为并发(concurrency),因为任务切换的太快,以至于无法分辨任务在何时被挂起而切换到另一个任务,任务通过分时(time-sharing)的方式使用计算核心。切换任务的过程被称作上下文切换(context switch)。

    当计算机有多个核心,可以真正实现了并行运行超过一个任务,在物理层面实现并发,称为硬件并发(hardware concurrency)。

    并发与并行(parallel)的对比

    多进程并发:进程间的保护机制使得进程间通信设置复杂,速度慢。运行多个进程的故有开销大,比如启动进程需要时间,操作系统须投入内部资源来管理进程。
    但因为同样的原因,进程间的并发比线程并发更加安全。充分利用了多核特点。
    多线程并发:轻量级,每个线程像是轻量级的进程,相互独立运行,分别运行不同的指令序列。共享相同的地址空间,比如程序段、栈和全局数据。使用多线程的开销远小于进程并发。开发者必须确保每个线程访问时所看到的数据是一致的。
    多线程并发只在一个核上。
    线程是有限的资源,线程越多就会消耗更多的系统资源。程序创建一个新的线程,必须为这个线程创建一个新的栈,每个栈对应一个线程。当某个栈执行到全部弹出时,对应线程完成任务。多线程的进程在内存中有多个栈,栈间用空白区域隔开,以备栈的增长,而任何一个空白区域被填满都可能导致栈溢出的问题。对于有限内存的系统来说这是个问题,需要控制线程数量
    运行越多的线程,系统需要做越多的context switch。每次switch都需要耗费时间,有时候增加一个线程实际上会降低而非提高程序的整体性能。
    总的来说,多线程thread交流成本 < 多进程process交流成本

    • 进程描述符记录每个线程的相关信息,i.e.,状态和进度。
    • 系统需要把适当的计算时间分配给进程,内核调度器在分配计算时间时,必须把各个线程考虑在内。
    • 进程空间必须有多个栈。栈记录着函数调用的顺序,最下方的栈是唯一一个激活函数。多线程中有多个函数处于激活状态,并同时运行。下面是多线程程序。
    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h> // for sleep
    void *func1(void) 
    {
        int i;
        for (i = 0; i < 5; i++) 
        {
            printf('func1 is running %d\n',i);
            sleep(1);
        }
        return NULL;
    }
    void *func2(void) 
    {
        int i;
        for (i = 0; i < 5; i++) 
        {
            printf('func2 is running %d\n',i);
            sleep(1);
        }
        return NULL;
    }
    void *func3(void) 
    {
        int i;
        for (i = 0; i < 3; i++) 
        {
            printf('func3 is running %d\n',i);
            sleep(1);
        }
        return NULL;
    }
    
    int main() 
    {
        int i=0, ret = 0;
        pthread_t func1_id, func2_id, func3_id;
        ret = pthread_create(&func1_id,NULL, (void *)func1, NULL);
        if (ret)
        { 
            printf('cannot create func1.\n')
            return 1;
        }
        ret = pthread_create(&func2_id,NULL, (void *)func2, NULL);
        if (ret)
        { 
            printf('cannot create func2.\n')
            return 1;
        }
        ret = pthread_create(&func3_id,NULL, (void *)func3, NULL);
        if (ret)
        { 
            printf('cannot create func3.\n')
            return 1;
        }
        //wait for func3
        pthread_join(func3_id, NULL);
        printf('Main thread exists.\n');
        return 0;
    }
    

    另一个并发的代码例子。

    #include <iostream>
    #include <thread>
    void hello()
    {
        std::cout <<'hello concurrent world\n';
    }
    int main() 
    {
        std::thread t(hello);
        t.join();
    }
    

    竞态条件Race condition

    多个任务可以共享数据,特别是可以同时修改某个数据时,就有可能发生竞态条件。在并发系统中,如果运行结果依赖于不同线程执行的先后顺序,也会造成竞态条件。

    多线程同步Synchronisation

    同步,指的是在一个时间内只允许某一个任务访问某个资源。同步可以解决竞态条件问题。

    多线程同步就在一定的时间内只允许某一个线程访问某个资源,可通过互斥锁(mutex, mutual exclusion)、条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。

    互斥锁Mutex

    mutex是一个特殊变量,一般被设置为全局变量,它有锁上和解锁两个状态。打开的mutex可由某个线程获得。一旦获得,mutex就会锁上,只有该线程有权打开。其余线程想使用该mutex,只能等到下一次mutex打开。每个线程必须遵守上述规则才能保证mutex发挥作用。如果有线程不获得互斥锁而直接修改变量,则mutex失去了保护意义。其效力在于多线程共同遵守规则,它本身并不能硬性阻止线程对变量的修改。总之mutex需要开发者写出完善的程序来发挥其作用,其他同步方式也是一样。

    while (1) {
        mutex_lock(mu); /*无限循环*/
        if (i != 0) i = i-1;
        else {
            printf('no more tickets');
            exit();
        }
        mutex_unlock(mu); /*释放mutex*/
    }
    

    另一个case

    #include <list>
    #include <mutex>
    #include <algorithm>
    std::list<int> some_list; //全局变量
    std::mutex some_mutex; //全局守护
    void add_to_list(in new_value)
    {
        std::lock_guard<std::mutex> guard(some_mutex);
        some_list.push_back(new_value);
    }
    bool list_contains(int value_to_find)
    {
        std::lock_guard<std::mutex> guard(some_mutex);
        return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
    }
    //使用了lock_guard意味着这两个函数中的访问是互斥的
    
    互斥锁的死锁问题(deadlock)

    死锁是多个线程需要锁定两个或更多mutex以执行操作时的最大问题。比如线程1锁定了变量A,并等待变量B,而线程2锁定了变量B,并等待变量A。两个线程均不会放弃各自已经持有的mutex。
    解决方案:如果线程需要获取多个mutex,则在每个线程中以相同的顺序获取他们。或者可以避免嵌套锁,也就是一个线程一旦获得一个mutex,就不再后去mutex。

    条件变量condition variable

    常常被保存为全局变量,与mutex合作。条件变量特别适用于多个线程共同等待某个条件发生的情况。这种情况下也可以使用mutex,但是每个进程就需要不断尝试获得mutex并检查条件是否发生,浪费了系统资源。

    mutex_lock(mu);
    num = num +1; //该工人建造房间。因前一步已经锁定mu,所以在unlock之前num都不会被别的线程改变。
    if (num <= 10) { //该工人是前10个完成的
        cond_wait(mu, cond); 
        //该函数做两件事,1释放mutex mu,2等待条件变量cond的通知。符合条件的线程开始等待 ,
        //当“第10个房间已经 建好”的通知到达,cond_wait()会再次锁上mu。线程恢复运行,
        //执行下一句drink beer指令。而从这里到mutex_unlock(),就构成了 另一个mutex结构。
        printf('drink beer');
    }
    else if (num = 11) {
        cond_broadcast(mu, cond);
    }
    mutex_unlock(mu);
    
    读写锁

    与mutex相似,只是对读写做出了区分。当共享资源只有读取而没有写入,则多个任务可以同时读取,不会存在race condition。一旦有线程开始写入,其他读写该资源的进程都要等待。包含了两把锁,读锁(R)和写锁(W)。

    R锁控制读取。多个进程(线程?)可以同时读取同一资源。W锁控制写入,同一时间只能有一个线程获得W锁。不过在获得W锁之前,线程必须等待所有持有共享资源R锁的线程释放掉各自的R锁,以免自己的写入操作干扰到其他线程的R。

    Reference

    1 Vamei, 周梓昕著,树莓派开始玩转Linux,中国工信出版集团,电子工业出版社
    2 Anthony Williams著,周全等译,C++并发编程实战(C++ Concurrency in Action),人民邮电出版社

    相关文章

      网友评论

          本文标题:多线程与同步, since 2020-11-03

          本文链接:https://www.haomeiwen.com/subject/mygivktx.html