美文网首页
多线程互斥和同步的区别与联系

多线程互斥和同步的区别与联系

作者: XDgbh | 来源:发表于2018-08-07 16:06 被阅读23次

    总结:

    1. 互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
    2. 同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
    3. 同步其实已经实现了互斥,所以同步是一种更为复杂的互斥。
    4. 同步是一种特殊的互斥。特殊在要求各线程访问资源有序。

    一、同步与互斥的区别

    1. 同步

    同步,又称直接制约关系,是指多个线程(或进程)为了合作完成任务,必须严格按照规定的 某种先后次序来运行。

    例如,线程 T2 中的语句 y 要使用线程 T1 中的语句 x 的运行结果,所以只有当语句 x 执行完成之后语句 y 才可以执行。我们可以使用信号量进行同步:

    semaphore S=0;   // 初始化信号量
    
    T1() {
        ...
        x;           // 语句x
        V(S);        // 告诉线程T2,语句x已经完成
        ...
    }
    
    T2() {
        ...
        P(S);        // 检查语句x是否运行完成
        y;           // 检查无误,运行y语句
        ...
    }
    

    2. 互斥

    互斥,又称间接制约关系,是指系统中的某些共享资源,一次只允许一个线程访问。当一个线程正在访问该临界资源时,其它线程必须等待。

    例如,打印机就是一种临界资源,而访问打印机的代码片段称为临界区,故每次只允许一个线程进入临界区。—— 我们同样可以使用信号量解决互斥问题,只需把临界区置于 P(S) 和 V(S) 之间,即可实现两线程对临界资源的互斥访问。

    semaphore S=1;   // 初始化信号量
    
    T1() {
        ...
        P(S);
        线程T1的临界区;  // 访问临界资源
        V(S);
        ...
    }
    
    T2() {
        ...
        P(S);
        线程T2的临界区;  // 访问临界资源
        V(S);
        ...
    }
    

    二、一个同步的例子

    如下图,为了求出 1 到 n 的平均值,需要三个线程协调它们的工作次序来完成,这就是同步:


    为了使多个线程按顺序正确执行,应设置若干个初始值为 0 的信号量:

    #include<iostream>
    #include<pthread.h>
    #include"semaphore.hpp"
    using namespace std;
    
    int sem1, sem2;
    int n = 10;  /*1...n的平均值*/
    int sum = 0;
    double average = 0;
    
    void* t1(void* arg)
    {
        for(int i=1; i<=n; ++i)
            sum += i;
        sem_v(sem1);  /*V操作,通知t2求和已完成*/ 
    }
    
    void* t2(void* arg)
    {
        sem_p(sem1);  /*P操作,等待t1完成*/    
        average = (double)sum/n;
        sem_v(sem2);  /*V操作,通知main求平均已完成*/
    }
    
    int main()
    {
        sem1 = creat_sem("/" , 0); /*创建信号量*/
        sem2 = creat_sem("/home", 0);
    
        pthread_t id[2];
        pthread_create(&id[0], NULL, t1, NULL);
        pthread_create(&id[1], NULL, t2, NULL);
    
        sem_p(sem2);  /*P操作,等待t2完成*/
        cout << "The sum is: " << sum << endl;
        cout << "The average is: " << average << endl;
    
        del_sem(sem1); /*删除信号量*/
        del_sem(sem2);
        return 0;
    }
    

    下面是信号量的相关函数,详见《进程间通信——信号量》。

    // semaphore.hpp
    #include<cstdio>
    #include<cstdlib>
    #include<sys/sem.h>
    
    // 联合体,用于semctl初始化
    union semun
    {
        int              val; /*for SETVAL*/
        struct semid_ds *buf;
        unsigned short  *array;
    };
    
    // 初始化信号量
    int init_sem(int sem_id, int value)
    {
        union semun tmp;
        tmp.val = value;
        if(semctl(sem_id, 0, SETVAL, tmp) == -1)
        {
            perror("Init Semaphore Error");
            return -1;
        }
        return 0;
    }
    
    // P操作:
    //  若信号量值为1,获取资源并将信号量值-1 
    //  若信号量值为0,进程挂起等待
    int sem_p(int sem_id)
    {
        struct sembuf sbuf;
        sbuf.sem_num = 0; /*序号*/
        sbuf.sem_op = -1; /*P操作*/
        sbuf.sem_flg = SEM_UNDO;
    
        if(semop(sem_id, &sbuf, 1) == -1)
        {
            perror("P operation Error");
            return -1;
        }
        return 0;
    }
    
    // V操作:
    //  释放资源并将信号量值+1
    //  如果有进程正在挂起等待,则唤醒它们
    int sem_v(int sem_id)
    {
        struct sembuf sbuf;
        sbuf.sem_num = 0; /*序号*/
        sbuf.sem_op = 1;  /*V操作*/
        sbuf.sem_flg = SEM_UNDO;
    
        if(semop(sem_id, &sbuf, 1) == -1)
        {
            perror("V operation Error");
            return -1;
        }
        return 0;
    }
    
    // 删除信号量集
    int del_sem(int sem_id)
    {
        union semun tmp;
        if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
        {
            perror("Delete Semaphore Error");
            return -1;
        }
        return 0;
    }
    
    // 创建信号量,返回其ID
    int creat_sem(const char* path, int value)
    {
        int sem_id;  /*信号量集ID*/
        key_t key;
        /*获取key值*/
        if((key = ftok(path, 'z')) < 0)
        {
            perror("ftok error");
            exit(1);
        }
    
        /*创建信号量集,其中只有一个信号量*/
        if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
        {
            perror("semget error");
            exit(1);
        }
    
        init_sem(sem_id, value);
        return sem_id;
    }
    

    线程 t2 需要等待线程 t1 (求和)完成以后才能够执行;主线程 main 需要等待线程 t2 (求平均)完成以后才能够执行输出。编译运行结果如下:

    $ g++ -lpthread -o synchronized synchronized.cpp 
    $ ./synchronized 
    The sum is: 55
    The average is: 5.5
    

    谈谈ftok函数对信号量键值获取的帮助:

    系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID键值。通常情况下,该id值通过ftok函数得到。

    #include <sys/types.h>
    #include <sys/ipc.h>
    函数原型:
    key_t ftok( const char * fname, int id )
    fname就是你指定的文件名(已经存在的文件名),一般使用当前目录,如:
    key_t key;
    key = ftok(".", 1); 这样就是将fname设为当前目录。
    

    id是子序号。虽然是int类型,但是只使用8bits(1-255)。
    在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
    如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
    查询文件索引节点号的方法是: ls -i
    当删除重建文件后,索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同。
    如果要确保key_t值不变,要么确保ftok的文件不被删除,要么不用ftok,指定一个固定的key_t值,比如:

    #define IPCKEY 0x111
    char path[256];
    sprintf( path, "%s/etc/config.ini", (char*)getenv("HOME") );
    msgid=ftok( path, IPCKEY );
    

    同一段程序,用于保证两个不同用户下的两组相同程序获得互不干扰的IPC键值。
    由于etc/config.ini(假定)为应用系统的关键配置文件,因此不存在被轻易删除的问题——即使被删,也会很快被发现并重建(此时应用系统也将被重启)。
    ftok()的设计目的也在于此。

    相关文章

      网友评论

          本文标题:多线程互斥和同步的区别与联系

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