• Linux/Unix 线程同步技术之互斥量(1)


    众所周知,互斥量(mutex)是同步线程对共享资源访问的技术,用来防止下面这种情况:线程A试图访问某个共享资源时,线程B正在对其进行修改,从而造成资源状态不一致。与之相关的一个术语临界区(critical section)是指访问某一共享资源的代码片段,并且这段代码的执行为原子(atomic)操作,即同时访问同一共享资源的其他线程不应中断该片段的执行。

    我们先来看看不使用临界区技术保护共享资源的例子,该例子使用2个线程来同时递增同一个全局变量。

    代码示例1:不使用临界区技术访问共享资源

     1 #include <pthread.h>
     2 #include <stdio.h>
     3 #include <stdlib.h>
     4 
     5 static int g_n = 0;
     6 
     7 static void *
     8 thread_routine(void *arg)
     9 {
    10     int n_loops = (int)(arg);
    11     int loc;
    12     int j;
    13     
    14     for (j = 0; j < n_loops; j++)
    15     {
    16         loc = g_n;
    17         loc++;
    18         g_n = loc;
    19     }
    20     
    21     return 0;
    22 }
    23 
    24 int
    25 main(int argc, char *argv[])
    26 {
    27     int n_loops, s;
    28     pthread_t t1, t2;
    29     void *args[2];
    30     
    31     n_loops = (argc > 1) ? atoi(argv[1]) : 10000000;
    32     
    33     args[0] = (void *)n_loops;
    34     s = pthread_create(&t1, 0, thread_routine, &args);
    35     if (s != 0)
    36     {
    37         perror("error pthread_create.
    ");
    38         exit(EXIT_FAILURE);
    39     }
    40     
    41     s = pthread_create(&t2, 0, thread_routine, &args);
    42     if (s != 0)
    43     {
    44         perror("error pthread_create.
    ");
    45         exit(EXIT_FAILURE);
    46     }
    47     
    48     s = pthread_join(t1, 0);
    49     if (s != 0)
    50     {
    51         perror("error pthread_join.
    ");
    52         exit(EXIT_FAILURE);
    53     }
    54     
    55     s = pthread_join(t2, 0);
    56     if (s != 0)
    57     {
    58         perror("error pthread_join.
    ");
    59         exit(EXIT_FAILURE);
    60     }
    61 
    62     printf("Loops [%d] times by 2 threads without critical section.
    ", n_loops);
    63     printf("Var g_n is [%d].
    ", g_n);
    64     exit(EXIT_SUCCESS);
    65 }

    运行以上代码生成的程序,若循环次数较少,比如每个线程都对全局变量g_n递增1000次,结果看起来很正常:

    $ ./thdincr_nosync 1000
    Loops [1000] times by 2 threads without critical section.
    Var g_n is [2000].

    如果加大每个线程的循环次数,结果将大不相同:

    $ ./thdincr_nosync 10000000
    Loops [10000000] times by 2 threads without critical section.
    Var g_n is [18655665].

    造成以上问题的原因在于下面的执行序列:

    1. 线程1将g_n的值赋给局部变量loc。假设g_n的当前值为1000。

    2. 线程1的时间片用尽,线程2开始执行。

    3. 线程2执行多次循环:将g_n的值改为其他的值,例如3000,线程2的时间片用尽。

    4. 线程1重新获得时间片,并从上次停止处恢复执行。线程1在上次运行时,已将g_n的值(1000)赋给loc,现在递增loc,再将loc的值1001赋给g_n。此时线程2之前递增操作的结果遭到覆盖。

    如果使用上面同样的命令行参数运行该程序多次,g_n的值会出现很大波动:

    $ ./thdincr_nosync 10000000
    Loops [10000000] times by 2 threads without critical section.
    Var g_n is [14085995].

    $ ./thdincr_nosync 10000000
    Loops [10000000] times by 2 threads without critical section.
    Var g_n is [13590133].

    $ ./thdincr_nosync 10000000
    Loops [10000000] times by 2 threads without critical section.
    Var g_n is [20000000].

    $ ./thdincr_nosync 10000000
    Loops [10000000] times by 2 threads without critical section.
    Var g_n is [16550684].

    这一行为结果的不确定性,原因在于内核CPU调度顺序的不可预测性。若在复杂的程序中发生这种不确定结果的行为,意味着此类错误将偶尔发作,难以复现,因此也很难发现。如果使用如下语句:

    g_n++;        /* 或者: ++g_n */

    来替换thread_routine内for循环中的3条语句,似乎可以解决这一问题,不过在很多硬件架构上,编译器在将这条语句转换成机器码时,其效果仍等同于原先thread_routine内for循环中的3条语句。即换成一条语句并非意味着该操作就是原子操作。

    为了避免上述同一行为的结果不确定性,必须使用某种技术来确保同一时刻只有一个线程可以访问共享资源,在Linux/Unix系统中,互斥量mutex(mutual exclusion的缩写)就是为这种情况设计的一种线程间同步技术,可以使用互斥量来保证对任意共享资源的原子访问。

    互斥量有两种状态:已锁定和未锁定。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的互斥量再次加锁,将可能阻塞线程或者报错,具体取决于加锁时使用的方法。

    静态分配的互斥量:

    互斥量既可以像静态变量那样分配,也可以在运行时动态创建,例如,通过malloc在堆中分配,或者在栈上的自动变量,下面的语句展示了如何初始化静态分配的互斥量:

    static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

    互斥量的加锁和解锁操作:

    初始化之后,互斥量处于未锁定状态。函数pthread_mutex_lock()可以锁定某一互斥量,而函数pthread_mutex_unlock()可以将一个已经锁定的互斥量解锁。

    #include <pthread.h>
    
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    
    /* 两个函数在成功时返回值为0,失败时返回一个正值代表错误号。 */

    代码示例2:使用静态分配的互斥量保护对全局变量的访问

     1 #include <pthread.h>
     2 #include <stdio.h>
     3 #include <stdlib.h>
     4 
     5 static int g_n = 0;
     6 static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
     7 
     8 static void *
     9 thread_routine(void *arg)
    10 {
    11     int n_loops = *((int *)arg);
    12     int loc;
    13     int j;
    14     int s;
    15     
    16     for (j = 0; j < n_loops; j++)
    17     {
    18         s = pthread_mutex_lock(&mtx);
    19         if (s != 0)
    20         {
    21             perror("error pthread_mutex_lock.
    ");
    22             exit(EXIT_FAILURE);
    23         }
    24         
    25         loc = g_n;
    26         loc++;
    27         g_n = loc;
    28         
    29         s = pthread_mutex_unlock(&mtx);
    30         if (s != 0)
    31         {
    32             perror("error pthread_mutex_unlock.
    ");
    33             exit(EXIT_FAILURE);
    34         }
    35     }
    36     
    37     return 0;
    38 }
    39 
    40 int
    41 main(int argc, char *argv[])
    42 {
    43     pthread_t t1, t2;
    44     int n_loops, s;
    45     
    46     n_loops = (argc > 1) ? atoi(argv[1]) : 10000000;
    47     
    48     s = pthread_create(&t1, 0, thread_routine, &n_loops);
    49     if (s != 0)
    50     {
    51         perror("error pthread_create.
    ");
    52         exit(EXIT_FAILURE);
    53     }
    54     
    55     s = pthread_create(&t2, 0, thread_routine, &n_loops);
    56     if (s != 0)
    57     {
    58         perror("error pthread_create.
    ");
    59         exit(EXIT_FAILURE);
    60     }
    61     
    62     s = pthread_join(t1, 0);
    63     if (s != 0)
    64     {
    65         perror("error pthread_join.
    ");
    66         exit(EXIT_FAILURE);
    67     }
    68     
    69     s = pthread_join(t2, 0);
    70     if (s != 0)
    71     {
    72         perror("error pthread_join.
    ");
    73         exit(EXIT_FAILURE);
    74     }
    75 
    76     printf("Var g_n is [%d].
    ", g_n);
    77     exit(EXIT_SUCCESS);
    78 }

    运行此示例代码生成的程序,从结果中可以看出对g_n的递增操作总能保持正确:

    $ ./thdincr_mutex 10000000
    Var g_n is [20000000].
    $ ./thdincr_mutex 10000000
    Var g_n is [20000000].
    $ ./thdincr_mutex 10000000
    Var g_n is [20000000].
    $ ./thdincr_mutex 10000000
    Var g_n is [20000000].
    $ ./thdincr_mutex 10000000
    Var g_n is [20000000].

    代码示例3:使用动态分配的互斥量保护对全局变量的访问

      1 #include <pthread.h>
      2 #include <stdio.h>
      3 #include <stdlib.h>
      4 #include <errno.h>
      5 
      6 static int g_n = 0;
      7 
      8 static void *
      9 thread_routine(void *arg)
     10 {
     11     void **args = (void **)arg;
     12     int n_loops = (int)(args[0]);
     13     int loc;
     14     int j;
     15     int s;
     16     pthread_mutex_t *mtx = (pthread_mutex_t *)(args[1]);
     17     
     18     for (j = 0; j < n_loops; j++)
     19     {
     20         s = pthread_mutex_lock(mtx);
     21         if (s != 0)
     22         {
     23             printf("error pthread_mutex_lock. return:[%d] errno:[%d]
    ", s, errno);
     24             exit(EXIT_FAILURE);
     25         }
     26         
     27         loc = g_n;
     28         loc++;
     29         g_n = loc;
     30         
     31         s = pthread_mutex_unlock(mtx);
     32         if (s != 0)
     33         {
     34             perror("error pthread_mutex_unlock.
    ");
     35             exit(EXIT_FAILURE);
     36         }
     37     }
     38     
     39     return 0;
     40 }
     41 
     42 int
     43 main(int argc, char *argv[])
     44 {
     45     int n_loops, s;
     46     pthread_t t1, t2;
     47     pthread_mutex_t mtx;
     48     pthread_mutexattr_t mtx_attr;
     49     void *args[2];
     50     
     51     s = pthread_mutexattr_init(&mtx_attr);
     52     if (s != 0)
     53     {
     54         perror("error pthread_mutexattr_init.
    ");
     55         exit(EXIT_FAILURE);
     56     }
     57     
     58     s = pthread_mutexattr_settype(&mtx_attr, PTHREAD_MUTEX_ERRORCHECK);
     59     if (s != 0)
     60     {
     61         perror("error pthread_mutexattr_settype.
    ");
     62         exit(EXIT_FAILURE);
     63     }
     64     
     65     s = pthread_mutex_init(&mtx, &mtx_attr);
     66     if (s != 0)
     67     {
     68         perror("error pthread_mutex_init.
    ");
     69         exit(EXIT_FAILURE);
     70     }
     71     
     72     s = pthread_mutexattr_destroy(&mtx_attr);
     73     if (s != 0)
     74     {
     75         perror("error pthread_mutexattr_destroy.
    ");
     76         exit(EXIT_FAILURE);
     77     }
     78     
     79     n_loops = (argc > 1) ? atoi(argv[1]) : 10000000;
     80     
     81     args[0] = (void *)n_loops;
     82     args[1] = (void *)&mtx;
     83     s = pthread_create(&t1, 0, thread_routine, &args);
     84     if (s != 0)
     85     {
     86         perror("error pthread_create.
    ");
     87         exit(EXIT_FAILURE);
     88     }
     89     
     90     s = pthread_create(&t2, 0, thread_routine, &args);
     91     if (s != 0)
     92     {
     93         perror("error pthread_create.
    ");
     94         exit(EXIT_FAILURE);
     95     }
     96     
     97     s = pthread_join(t1, 0);
     98     if (s != 0)
     99     {
    100         perror("error pthread_join.
    ");
    101         exit(EXIT_FAILURE);
    102     }
    103     
    104     s = pthread_join(t2, 0);
    105     if (s != 0)
    106     {
    107         perror("error pthread_join.
    ");
    108         exit(EXIT_FAILURE);
    109     }
    110     
    111     s = pthread_mutex_destroy(&mtx);
    112     if (s != 0)
    113     {
    114         perror("error pthread_mutex_destroy.
    ");
    115         exit(EXIT_FAILURE);
    116     }
    117     
    118     printf("Var g_n is [%d].
    ", g_n);
    119     exit(EXIT_SUCCESS);
    120 }

    多次运行示例3代码生成的程序会看到与示例2代码的程序同样的结果。

    本文展示了Linux/Unix线程间同步技术---互斥量的基本功能和基础使用方法,在后面的文章中将会讨论互斥量的其他内容,如锁定互斥量的另外2个API: pthread_mutex_trylock()和pthread_mutex_timedlock() ,互斥量的性能,互斥量的死锁等。欢迎大家参与讨论。

    本文参考了Michael Kerrisk的著作《The Linux Programming Interface》(中文版名为:Linux/Unix系统编程手册)第30章的内容,版权相关的问题请联系作者或者相应的出版社。

  • 相关阅读:
    修改用户密码,权限
    域渗透
    跨域
    目录
    C中的extern和static
    XSS进阶
    SQL injection
    双重指针学习笔记
    文件上传学习笔记
    Hello Python
  • 原文地址:https://www.cnblogs.com/technic-emotion/p/3618742.html
Copyright © 2020-2023  润新知