• 线程同步—互斥量


    线程同步

      当多个控制线程共享相同的内存时,就需要确保每个线程访问到的数据都是一致的。如果每个线程使用到的变量都是其他线程不会读取或者修改的话,那么就不存在一致性问题。同样,如果变量是只读的,多个线程读取该变量也不会有一致性问题。但是。当一个线程可以修改的变量,其他线程也可以读取或者修改的时候,我们就需要对这些线程进行同步,确保它们在访问变量的存储内容时不会访问到无效的值。

      当一个线程修改变量时,其他线程在读取这个变量时可能会看到一个不一致的值。在变量修改时间多于一个存储器访问周期的处理器架构中,当存储器读与存储区写这两个周期交叉时,这种不一致就会出现。

      例1,两个线程A和B,线程先读取变量然后给这个变量赋一个新值,但是写操作需要两个存储器周期。当线程B在在这两个存储器写周期中间时刻读取这个变量时,线程B就会得到不一致的值。为了解决这个问题,我们需要对线程使用锁,即同一时间只允许一个线程访问该变量。不管是线程A还是线程B,要想访问该变量,必须首先要获取锁。线程B在线程A释放锁之前就不能读取该变量。

      例2,两个或者多个线程在同一时间修改同一变量时,也需要进行同步。以变量自增操作的情况为例,增量操作可以分解为以下3步:

    (1) 从内存单元读入到寄存器中;

    (2) 在寄存器中做增量操作;

    (3) 把新的值写回到内存单元中。

    如果两个线程试图同时对同一个变量作增量操作而不进行同步的话,结果就可能出现不一致,变量可能比原来增加1,也可能比原来增加2,具体是增加1还是增加2取决于第2个线程开始操作时获取的数值。

    1、如果线程B执行步骤1要比线程A执行步骤3要早,那线程B读取的值与线程A读到的值是一样的,变量增加1,然后写回到内存,最终变量比原来只是增加了1。

    2、如果线程B执行步骤1要比线程A执行步骤3要晚,那线程B读取的值就是线程A做自增操作后的值了,变量增加1,然后写回到内存,最终变量比原来增加了2。

    问题:什么是线程同步?

    当多个线程对共享资源进行操作时,需要通过特定的设置来控制线程之间的先后执行顺序的过程,称为线程同步。POSIX线程同步机制主要有5种方式:

    • 互斥量
    • 读写锁
    • 条件变量
    • 自旋锁
    • 屏障

    <注意> 线程同步,这里的同步不是同时执行的意思,而是协调,协助,互相配合,即协调线程的先后执行顺序。

    互斥量

      前面的内容提到,当多个线程访问共享资源的时候,比如说共享内存,全局变量等,如果没有合理的线程同步机制的话,就会出现数据不一致的现象。为了实现同步机制,POSIX线程提供了多种方式,其中一种方式为互斥量mutex(也被称为互斥锁)。

    互斥量具体实现方式:每个线程在访问共享资源前先尝试对互斥量进行设置(加锁),成功加锁后才可以对共享资源进行读写操作,操作结束后释放互斥量(解锁)。

      对互斥量进行加锁之后,任何其他试图再次对互斥量进行加锁的线程都会被阻塞直到当前线程释放该互斥锁,此时被阻塞的线程处于阻塞状态。如果释放互斥量时有一个以上的线程处于阻塞状态,那么所有该锁上的阻塞线程都会变成可运行状态,第1个变为运行态的线程就可以对互斥量加锁,其他线程就会检测到互斥量依然是锁住的,只能再次回到可运行状态,等待互斥量重新变为可以。使用互斥量进行线程同步的方式下,每次只有获得互斥量的那个线程可以向前执行。

      互斥量不是为了消除竞争,实际上,资源还是共享的,线程间也还是竞争的,只不过通过这种“加锁/解锁”机制将共享资源的访问变成互斥操作,也就是说当某一个线程访问这个共享资源时,其它线程无法访问它,从而消除与时间有关的错误。从互斥量的实现机制我们可以看出,同一时刻,只能有一个线程持有该锁。还有一点需要注意的是,所有访问该共享资源的线程必须采用相同的互斥量(锁),而不能使用多个不同互斥量。

    互斥量API函数

    //头文件
    #include <pthread.h>
    #include <time.h>
    【说明】time.h头文件定义了struct timespec结构体。 //互斥量API函数 int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); int pthread_mutex_destroy(pthread_mutex_t *mutex); int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
    【返回值】上面的函数返回值:成功,返回0;失败,返回错误码。
    【说明】在Linux命令行下查看函数用法:man <函数名>,如man pthread_mutex_init。

    互斥量初始化

      互斥变量使用pthread_mutex_t数据类型来表示。在使用互斥变量之前首先需要对它进行初始化。对于静态分配的互斥量可以使用常量PTHREAD_MUTEX_INITIALIZER,也可以调用pthread_mutex_init函数进行初始化。如果动态分配互斥量(例如,通过malloc函数),在释放内存前需要调用pthread_mutex_destroy函数。

    int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
    【参数】
    mutex:指向互斥变量的指针。
    attr: 互斥量属性。如果设置为NULL,表示使用默认的属性初始化互斥量。
    【返回值】成功,返回0;失败,返回错误码。可以使用strerror(err)的方式打印错误码对应的错误信息。
    【说明】pthread_mutex_init函数用来初始化动态分配互斥量(例如,通过调用malloc函数)。而静态分配互斥量的方法如下:
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    【参数】mutex: 指向互斥量的指针。
    【返回值】成功,返回0;失败,返回错误码。可以使用strerror(err)的方式打印错误码对应的错误信息。
    【说明】pthread_mutex_destroy函数用来销毁mutex指向的互斥变量。

    互斥量加锁/解锁

      对互斥量加锁需要使用pthread_mutex_lock函数。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要调用pthread_mutex_unlock函数。如果线程不希望被阻塞,它可以使用pthread_mutex_trylock函数对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未加锁状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞,函数返回值为0,否则,pthread_mutex_trylock函数就会加锁失败,返回EBUSY错误码。

    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    【返回值】成功,返回0;失败,返回错误码。

    实例1:使用一个互斥量来保护数据结构的某个成员变量。

    mutex1.c
     1 #include <stdlib.h>
     2 #include <pthread.h>
     3 
     4 struct foo {
     5     int             f_count; //对象的引用计数(需要保护的成员变量)
     6     pthread_mutex_t f_lock;  //对象互斥量
     7     int             f_id;
     8     /* ... more stuff here ... */
     9 };
    10 
    11 struct foo *
    12 foo_alloc(int id) /* allocate the object */
    13 {
    14     struct foo *fp;
    15     
    16     //动态分配结构体对象fp
    17     if ((fp = malloc(sizeof(struct foo))) != NULL) {
    18         fp->f_count = 1;
    19         fp->f_id = id;
    20         //动态分配互斥量
    21         if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
    22             free(fp);
    23             return(NULL);
    24         }
    25         /* ... continue initialization ... */
    26     }
    27     return(fp);
    28 }
    29 
    30 void
    31 foo_hold(struct foo *fp) /* add a reference to the object */
    32 {
    33     pthread_mutex_lock(&fp->f_lock); //对互斥量加锁
    34     fp->f_count++;
    35     pthread_mutex_unlock(&fp->f_lock); //对互斥量解锁
    36 }
    37 
    38 void
    39 foo_rele(struct foo *fp) /* release a reference to the object */
    40 {
    41     pthread_mutex_lock(&fp->f_lock);
    42     if (--fp->f_count == 0) { /* last reference */
    43         pthread_mutex_unlock(&fp->f_lock);
    44         pthread_mutex_destroy(&fp->f_lock); //销毁互斥量
    45         free(fp);
    46     } else {
    47         pthread_mutex_unlock(&fp->f_lock);
    48     }
    49 }

    死锁

    产生死锁的情况:

    例1、如果一个线程试图对同一个互斥量加锁两次,那么它自身就会陷入死锁状态。

    例2、当程序中使用一个以上的互斥量时,如果一个线程一直占有着第1个互斥量,并且在试图锁住第2个互斥量时处于阻塞状态,与此同时,一直占有第2个互斥量的线程也在试图锁住第1个互斥量。因为两个线程都在互相请求另一个线程拥有的资源,所以这两个线程都无法向前运行,于是就产生了死锁。

    问题:什么是死锁?死锁产生的原因?

    从线程的角度来回答死锁现象,就是指两个或者两个以上的线程互相持有对方所需的资源,并且都在互相等待对方释放资源,导致这些线程都处于无限等待状态,无法继续向前运行,从而导致死锁的发生。

    造成死锁的原因可以概括为如下3点:

    • 当前线程拥有其他线程所需的资源
    • 当前线程在等待其他线程已占有的资源
    • 线程都不主动放弃自己已占有的资源。

    死锁产生的4个必要条件(只要发生了死锁,4个条件都成立,只要有一个不成立,就不会发生死锁)

    (1)互斥条件:一个资源每次只能被一个进程/线程使用。
    (2)请求与保持条件:一个进程/线程因请求资源而阻塞时,对已拥有的资源保持不放。
    (3)不可剥夺条件:进程/线程已获得的资源,在末使用完之前,不能被强行剥夺。
    (4)循环等待条件:若干进程/线程之间形成一种头尾相接的循环等待资源的关系。
    这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,这也为我们实际应用中定位死锁问题提供了参考。

    避免死锁

    1、控制互斥量加锁的顺序来避免死锁的发生。
      例如,需要对两个互斥量A和B同时加锁,如果所有线程总是在对互斥量B加锁之前先锁住互斥量A,那么使用这两个互斥量就不会产生死锁。类似地,如果所有线程总是在锁住互斥量A之前锁住互斥量B,那么也不会发生死锁。可能出现的死锁只会发生在一个线程试图锁住另一个线程以相反的顺序锁住的互斥量。比如说,线程1先锁住了互斥量A,然后试图锁住互斥量B,但此时互斥量B已被线程2锁住了,同时线程2试图锁住互斥量A,线程1因加锁互斥量B失败而阻塞,线程2因加锁互斥量A失败而阻塞,两个线程就会永久阻塞下去,最终导致了死锁的发生。
    2、使用定时锁。
      当线程试图获取已加锁的互斥量时,为了避免线程永久阻塞,可以使用pthread_mutex_timedlock函数来对互斥量进行加锁操作。该函数允许设置线程阻塞时间,在达到超时时间值时,pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码ETIMEOUT
    //头文件
    #include <pthread.h>
    #include <time.h>
    int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
    【参数】
    mutex:指向互斥量的指针。
    abs_timeout:指定愿意等待的超时时间,这个时间是绝对时间而不是相对时间(与相对时间对比而言,指定的时间在X之前可以阻塞等待,而不是说愿意阻塞等待X秒)。这个超时时间使用 struct timespec结构体来表示的,
    它用秒和纳秒来描述时间。
    struct timespec 结构体定义: typedef long time_t; #ifndef _TIMESPEC #define _TIMESPEC struct timespec { time_t tv_sec; // seconds(秒) long tv_nsec; //nanoseconds(纳秒) }; #endif 【说明】当线程试图对一个已加锁的互斥量进行加锁时,pthread_mutex_timedlock函数允许设置线程阻塞时间。pthread_mutex_timedlock与pthread_mutex_lock基本是等价的,但是在达到超时时间时,
    pthread_mutex_timedlock不会对互斥量进行加锁,而是返回错误码ETIMEDOUT。

    实例2:两个互斥量的使用。其中一个互斥量hashlock是静态互斥量,另一个互斥量f_lock是动态互斥量。

    mutex2.c
      1 #include <stdlib.h>
      2 #include <pthread.h>
      3 
      4 #define NHASH 29
      5 #define HASH(id) (((unsigned long)id)%NHASH) //获取哈希表的keyID
      6 
      7 struct foo *fh[NHASH]; //全局结构体对象指针数组
      8 
      9 //对静态互斥量初始化,维护着用于跟踪foo数据结构体的散列列表fh(该散列列表是静态指针数组)
     10 pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
     11 
     12 struct foo {
     13     int             f_count; //对象的引用计数(需要保护的成员变量)
     14     pthread_mutex_t f_lock;  //对象互斥量成员保护f_count成员变量的访问
     15     int             f_id;    //对象ID
     16     struct foo     *f_next; /* protected by hashlock */
     17     /* ... more stuff here ... */
     18 };
     19 
     20 struct foo *
     21 foo_alloc(int id) /* allocate the object */
     22 {
     23     struct foo    *fp;
     24     int            idx;
     25 
     26     if ((fp = malloc(sizeof(struct foo))) != NULL) {
     27         fp->f_count = 1;
     28         fp->f_id = id;
     29         if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
     30             free(fp);
     31             return(NULL);
     32         }
     33         idx = HASH(id);
     34         pthread_mutex_lock(&hashlock);
     35         fp->f_next = fh[idx];
     36         fh[idx] = fp;
     37         pthread_mutex_lock(&fp->f_lock);
     38         pthread_mutex_unlock(&hashlock);
     39         /* ... continue initialization ... */
     40         pthread_mutex_unlock(&fp->f_lock);
     41     }
     42     return(fp);
     43 }
     44 
     45 void
     46 foo_hold(struct foo *fp) /* add a reference to the object */
     47 {
     48     //访问foo结构中的f_count成员变量时进行加锁保护
     49     pthread_mutex_lock(&fp->f_lock);
     50     fp->f_count++;
     51     pthread_mutex_unlock(&fp->f_lock);
     52 }
     53 
     54 struct foo *
     55 foo_find(int id) /* find an existing object */
     56 {
     57     struct foo    *fp;
     58     
     59     //在散列列表fn中查找对象ID值=id的对象,需要进行加锁保护
     60     pthread_mutex_lock(&hashlock);
     61     for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
     62         if (fp->f_id == id) {
     63             foo_hold(fp);
     64             break;
     65         }
     66     }
     67     pthread_mutex_unlock(&hashlock);
     68     return(fp);
     69 }
     70 
     71 void
     72 foo_rele(struct foo *fp) /* release a reference to the object */
     73 {
     74     struct foo    *tfp;
     75     int            idx;
     76 
     77     pthread_mutex_lock(&fp->f_lock);
     78     if (fp->f_count == 1) { /* last reference */
     79         pthread_mutex_unlock(&fp->f_lock);
     80         pthread_mutex_lock(&hashlock);
     81         pthread_mutex_lock(&fp->f_lock);
     82         /* need to recheck the condition */
     83         if (fp->f_count != 1) {
     84             fp->f_count--;
     85             pthread_mutex_unlock(&fp->f_lock);
     86             pthread_mutex_unlock(&hashlock);
     87             return;
     88         }
     89         /* remove from list */
     90         idx = HASH(fp->f_id);
     91         tfp = fh[idx];
     92         if (tfp == fp) {//如果ftp指向的fh[idx]散列链表的首结点就是目标结点
     93             fh[idx] = fp->f_next; //将目标结点fp的后继结点作为fh[idx]散列链表新的首结点
     94         } else {
     95             while (tfp->f_next != fp) //如果ftp当前指向的结点的后继结点不是目标结点
     96                 tfp = tfp->f_next; //ftp指向当前结点的后继结点
     97             //当找到ftp当前指向的结点的后继结点就是目标结点时退出while循环
     98             //将tfp当前指向的结点的后继节点改为目标结点的后继节点
     99             tfp->f_next = fp->f_next;
    100         }
    101         pthread_mutex_unlock(&hashlock);
    102         pthread_mutex_unlock(&fp->f_lock);
    103         pthread_mutex_destroy(&fp->f_lock);
    104         free(fp); //free目标结点
    105     } else {
    106         fp->f_count--;
    107         pthread_mutex_unlock(&fp->f_lock);
    108     }
    109 }

    【分析】在同时需要两个互斥量时,总是让它们以相同的顺序加锁,这样就可以避免死锁。互斥量hashlock用于保护静态散列列表fh,互斥量f_lock用于保护动态分配的foo对象中的f_count成员变量。

    实例3:实例2中的锁方法太复杂,我们需要重新审视之前的互斥锁设计,进行互斥锁的优化。

    mutex3.c
     1 #include <stdlib.h>
     2 #include <pthread.h>
     3 
     4 #define NHASH 29
     5 #define HASH(id) (((unsigned long)id)%NHASH)
     6 
     7 struct foo *fh[NHASH];
     8 pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER;
     9 
    10 struct foo {
    11     int             f_count; /* protected by hashlock */
    12     pthread_mutex_t f_lock;
    13     int             f_id;
    14     struct foo     *f_next; /* protected by hashlock */
    15     /* ... more stuff here ... */
    16 };
    17 
    18 struct foo *
    19 foo_alloc(int id) /* allocate the object */
    20 {
    21     struct foo    *fp;
    22     int            idx;
    23 
    24     if ((fp = malloc(sizeof(struct foo))) != NULL) {
    25         fp->f_count = 1;
    26         fp->f_id = id;
    27         if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
    28             free(fp);
    29             return(NULL);
    30         }
    31         idx = HASH(id);
    32         pthread_mutex_lock(&hashlock);
    33         fp->f_next = fh[idx];
    34         fh[idx] = fp;
    35         pthread_mutex_lock(&fp->f_lock);
    36         pthread_mutex_unlock(&hashlock);
    37         /* ... continue initialization ... */
    38         pthread_mutex_unlock(&fp->f_lock);
    39     }
    40     return(fp);
    41 }
    42 
    43 void
    44 foo_hold(struct foo *fp) /* add a reference to the object */
    45 {
    46     pthread_mutex_lock(&hashlock);
    47     fp->f_count++;
    48     pthread_mutex_unlock(&hashlock);
    49 }
    50 
    51 struct foo *
    52 foo_find(int id) /* find an existing object */
    53 {
    54     struct foo    *fp;
    55 
    56     pthread_mutex_lock(&hashlock);
    57     for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) {
    58         if (fp->f_id == id) {
    59             fp->f_count++;
    60             break;
    61         }
    62     }
    63     pthread_mutex_unlock(&hashlock);
    64     return(fp);
    65 }
    66 
    67 void
    68 foo_rele(struct foo *fp) /* release a reference to the object */
    69 {
    70     struct foo    *tfp;
    71     int            idx;
    72 
    73     pthread_mutex_lock(&hashlock);
    74     if (--fp->f_count == 0) { /* last reference, remove from list */
    75         idx = HASH(fp->f_id);
    76         tfp = fh[idx];
    77         if (tfp == fp) {
    78             fh[idx] = fp->f_next;
    79         } else {
    80             while (tfp->f_next != fp)
    81                 tfp = tfp->f_next;
    82             tfp->f_next = fp->f_next;
    83         }
    84         pthread_mutex_unlock(&hashlock);
    85         pthread_mutex_destroy(&fp->f_lock);
    86         free(fp);
    87     } else {
    88         pthread_mutex_unlock(&hashlock);
    89     }
    90 }

    【分析】我们也可以使用散列列表锁来保护foo结构的引用计数成员变量f_count,而结构互斥量可以用来保护foo结构中的其他东西,即hashlock互斥锁既可以用来保护散列列表fn,又可以用来保护foo结构中的引用计数成员变量f_count,这样就不存在锁的排序问题了。

    【总结】多线程程序设计过程中,会用到互斥量来同步线程。如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁,那么程序的并发性就会受影响。如果锁的粒度太细,那么过多的锁开销又会使系统的性能受到影响,而且代码逻辑会变得复杂。作为一个程序员,需要在满足锁需求的情况下,在代码复杂性和程序性能之间找到正确的平衡。

    锁的粒度:指的是互斥锁(量)保护的临界资源的范围。

    临界资源:一次仅允许一个进程/线程使用的资源称为临界资源。

    实例4:使用pthread_mutex_timedlock()函数对互斥量加锁避免永久阻塞。

     1 /**
     2 编译: gcc mutex_timedlock.c -o timedlock -lpthread -lrt
     3 <说明> clock_gettime()函数是在librt库中实现的,所以需要加上-lrt库链接
     4 */
     5 #include <stdio.h>
     6 #include <time.h>
     7 #include <pthread.h>
     8 #include <string.h>
     9 
    10 int main(int argc, char *argv[])
    11 {
    12     int err;
    13     struct timespec tout;
    14     struct tm *tmp;
    15     char buf[64] = {0};
    16     //初始化静态互斥量lock
    17     pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    18     
    19     pthread_mutex_lock(&lock); //对互斥量lock加锁
    20     printf("mutex is locked
    ");
    21     clock_gettime(CLOCK_REALTIME, &tout); //获取时间,存放在tout结构变量中
    22     tmp = localtime(&tout.tv_sec); //返回struct tm结构的时间格式
    23     strftime(buf, sizeof(buf), "%r", tmp); //将struct tm结构的时间格式转化成时间字符串
    24     printf("current time is: %s
    ", buf);
    25     tout.tv_sec += 10;
    26     err = pthread_mutex_timedlock(&lock, &tout); //尝试绑定互斥量lock,如果成功返回0;如果超时就返回错误码ETIMEDOUT
    27     clock_gettime(CLOCK_REALTIME, &tout);
    28     tmp = localtime(&tout.tv_sec);
    29     strftime(buf, sizeof(buf), "%r", tmp);
    30     printf("the time now is: %s
    ", buf);
    31     if(err == 0)
    32         printf("mutex lock again!
    ");
    33     else
    34         printf("can`t lock again: %s
    ", strerror(err));
    35     return 0;
    36 }

    【运行结果】

    mutex is locked
    current time is: 10:05:32 AM
    the time now is: 10:05:42 AM
    can`t lock again: Connection timed out

    【结果分析】对已经加锁的互斥量lock调用pthread_mutex_timedlock函数重新进行加锁时,设置了超时时间10s,当达到超时时间时,线程不会继续阻塞,而是返回错误码ETIMEDOUT,对应的错误信息是:Connection timed out。线程就可以继续往下执行,这样就避免了线程因加锁失败而一直阻塞。

     
  • 相关阅读:
    数据科学面试应关注的6个要点
    Python3.9的7个特性
    一种超参数优化技术-Hyperopt
    梯度下降算法在机器学习中的工作原理
    MQ(消息队列)功能介绍
    D. The Number of Pairs 数学
    F. Triangular Paths 思维
    D. XOR-gun 思维和 + 前缀
    C. Basic Diplomacy 思维
    D. Playlist 思维
  • 原文地址:https://www.cnblogs.com/yunfan1024/p/11300810.html
Copyright © 2020-2023  润新知