Linux之线程互斥

打印 上一主题 下一主题

主题 504|帖子 504|积分 1512

目录

一、题目引入
二、线程互斥
1、相干概念
2、加锁保护
1、静态分配
2、动态分配
3、锁的原理
4、死锁
三、可重入与线程安全
1、概念
2、常见的线程不安全的环境
3、常见的线程安全的环境
4、常见不可重入的环境
5、常见可重入的环境
6、可重入与线程安全联系
7、可重入与线程安全区别


一、题目引入

大部门环境,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种环境,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操纵共享变量,会带来一些题目。
我们来看看下面的多线程抢票系统的代码:
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <cerrno>
  4. #include <cstring>
  5. #include <pthread.h>
  6. using namespace std;
  7. int ticket = 100;
  8. void *getticket(void *arg)
  9. {
  10.     char *name = (char *)arg;
  11.     while (true)
  12.     {
  13.         if (ticket > 0)
  14.         {
  15.             usleep(1000);
  16.             cout << name << ":"
  17.                  << " " << ticket << endl;
  18.             ticket--;
  19.         }
  20.         else
  21.             break;
  22.     }
  23. }
  24. int main()
  25. {
  26.     pthread_t tid1, tid2, tid3, tid4;
  27.     pthread_create(&tid1, nullptr, getticket, (void *)"thread 1");
  28.     pthread_create(&tid2, nullptr, getticket, (void *)"thread 2");
  29.     pthread_create(&tid3, nullptr, getticket, (void *)"thread 3");
  30.     pthread_create(&tid4, nullptr, getticket, (void *)"thread 4");
  31.     pthread_join(tid1, nullptr);
  32.     pthread_join(tid2, nullptr);
  33.     pthread_join(tid3, nullptr);
  34.     pthread_join(tid4, nullptr);
  35.     return 0;
  36. }
复制代码

这里的ticket变量是一个全局变量,那么它就会被全部线程共享。创建线程后,全部线程访问getticket函数,对其举行了重入,访问ticket并对ticket--。但是,我们发现,票数出现了负数,这完全不符合我们的代码逻辑和想要的效果。这是为什么呢?
起首,步伐在编译的时候会被编译成汇编代码, 而在汇编代码中,ticket--操纵在我们看来只有一行代码,但是在汇编中它实在分为了三步:1、将ticket值拷入到CPU寄存器中;2、CPU对其举行--操纵;3、将效果写回内存。
而我们知道进程是有时间片的,在实验完上面任意一步时,线程可能由于时间片到了而被切换。而这就会造成一些题目。如下图:

线程A先辈入,在完成第二步 -- 操纵后,由于时间片到了,要被切换出去,99作为上下文数据被生存起来随A一起被切换。线程B进入,由于B的时间片比较长,他把ticket值减到了50并写回了内存后,时间片到了,被切换。线程A再次进入CPU,把上下文恢复,然后接着第3步实验,直接把99写到了内存里面。
线程B明明已经让ticket的值减到了50,效果你个线程A又直接把效果改成了99。这样就出现了数据庞杂的现象。
在我们对ticket举行并发访问的时候,由于ticket- - 操纵并不是原子的,以是出现了数据不一致的环境。这种环境怎么办理呢?我们接着往下讲。
二、线程互斥

1、相干概念

   1、临界资源:多线程实验流共享的资源就叫做临界资源。
2、临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
3、互斥:任何时候,互斥保证有且只有一个实验流进入临界区,访问临界资源,通常对临界资源起保护作用。
4、原子性:不会被任何调度机制打断的操纵,该操纵只有两态,要么完成,要么未完成。
  2、加锁保护

为了办理上面代码的数据不一致的题目,需要做到三点:
1、代码必须要有互斥举动:今世码进入临界区实验时,不答应其他线程进入该临界区。
2、如果多个线程同时要求实验临界区的代码,并且临界区没有线程在实验,那么只能答应一个线程进入该临界区。
3、如果线程不在临界区中实验,那么该线程不能制止其他线程进入临界区。
而其中最简朴的一种方法就是对临界资源举行加锁保护。以到达下面的效果:

定义和初始化锁的函数: 
  1. NAME
  2.        pthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutex
  3. SYNOPSIS
  4.        #include <pthread.h>
  5.        1、int pthread_mutex_destroy(pthread_mutex_t *mutex);
  6.        2、int pthread_mutex_init(pthread_mutex_t *restrict mutex,
  7.               const pthread_mutexattr_t *restrict attr);
  8.        3、pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
复制代码
pthread_mutex_t 是由原生线程库给用户提供的一个数据类型,就是我们常说的锁。上图的 1和2 是对锁举行局部定义时的销毁和初始化操纵,相当于析构函数和构造函数。
上图的 3 是对全局锁或者static静态锁举行初始化的方式。下面我们一一解说。
加锁和解锁函数:
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请锁,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(实验流被挂起),等候互斥量解锁,再去申请锁。
  1. NAME
  2.        pthread_mutex_lock,  pthread_mutex_trylock,  pthread_mutex_unlock  -  lock   and
  3.        unlock a mutex
  4. SYNOPSIS
  5.        #include <pthread.h>
  6.        int pthread_mutex_lock(pthread_mutex_t *mutex);
  7.        int pthread_mutex_trylock(pthread_mutex_t *mutex);
  8.        int pthread_mutex_unlock(pthread_mutex_t *mutex);
复制代码
1、静态分配

静态分配就是我们 3 对应的对锁定义和初始化的方式。我们使用它对抢票代码举行保护。
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <cstring>
  4. #include <time.h>
  5. #include <pthread.h>
  6. using namespace std;
  7. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  8. int ticket = 100;
  9. void *getticket(void *arg)
  10. {
  11.     char *name = (char *)arg;
  12.     while (true)
  13.     {
  14.         pthread_mutex_lock(&mutex); // 加锁保护,其他线程只能在这阻塞等待,直到拿到锁
  15.         if (ticket > 0)             // 这部分代码只能串行执行
  16.         {
  17.             usleep(rand() % 10000);
  18.             cout << name << ":"
  19.                  << " " << ticket << endl;
  20.             ticket--;
  21.             pthread_mutex_unlock(&mutex); // 访问完临界资源,解锁,
  22.             // 让其他线程能够拿锁访问
  23.         }
  24.         else
  25.         {
  26.             pthread_mutex_unlock(&mutex); // 访问完临界资源,解锁
  27.             // 让其他线程能够拿锁访问
  28.             break;
  29.         }
  30.         usleep(rand() % 2000000);
  31.     }
  32.     return nullptr;
  33. }
  34. int main()
  35. {
  36.     srand((unsigned long)time(nullptr) ^ getpid() ^ 433);
  37.     pthread_t tid1, tid2, tid3, tid4;
  38.     pthread_create(&tid1, nullptr, getticket, (void *)"thread 1");
  39.     pthread_create(&tid2, nullptr, getticket, (void *)"thread 2");
  40.     pthread_create(&tid3, nullptr, getticket, (void *)"thread 3");
  41.     pthread_create(&tid4, nullptr, getticket, (void *)"thread 4");
  42.     pthread_join(tid1, nullptr);
  43.     pthread_join(tid2, nullptr);
  44.     pthread_join(tid3, nullptr);
  45.     pthread_join(tid4, nullptr);
  46.     return 0;
  47. }
复制代码

注:加锁的时候,一定要保证加锁粒度越小越好。最好不要让一些非临界区也被加锁保护。
2、动态分配

如果我们定义的锁是一个局部变量,那么我们就要像下面的代码这样使用锁:
  1. #include <iostream>
  2. #include <unistd.h>
  3. #include <cstring>
  4. #include <time.h>
  5. #include <pthread.h>
  6. using namespace std;
  7. #define THREAD_NUM 5
  8. class threaddata
  9. {
  10. public:
  11.     threaddata(const string &s, pthread_mutex_t *m)
  12.         : name(s), mtx(m)
  13.     {}
  14. public:
  15.     string name;
  16.     pthread_mutex_t *mtx;
  17. };
  18. int ticket = 100;
  19. void *getticket(void *arg)
  20. {
  21.     threaddata *td = (threaddata *)arg;
  22.     while (true)
  23.     {
  24.         pthread_mutex_lock(td->mtx);
  25.         if (ticket > 0)              
  26.         {
  27.             usleep(rand() % 10000);
  28.             cout << td->name << ":"
  29.                  << " " << ticket << endl;
  30.             ticket--;
  31.             pthread_mutex_unlock(td->mtx);
  32.         }
  33.         else
  34.         {
  35.             pthread_mutex_unlock(td->mtx);
  36.             break;
  37.         }
  38.         usleep(rand() % 2000000);
  39.     }
  40.     delete td;
  41.     return nullptr;
  42. }
  43. int main()
  44. {
  45.     pthread_mutex_t mtx;
  46.     pthread_mutex_init(&mtx, nullptr);
  47.     srand((unsigned long)time(nullptr) ^ getpid() ^ 433);
  48.     pthread_t t[THREAD_NUM];
  49.     for (int i = 0; i < THREAD_NUM; i++)
  50.     {
  51.         string name = "thread ";
  52.         name += to_string(i + 1);
  53.         threaddata *td = new threaddata(name, &mtx);
  54.         pthread_create(t + i, nullptr, getticket, (void *)td);
  55.     }
  56.     for (int i = 0; i < THREAD_NUM; i++)
  57.         pthread_join(t[i], nullptr);
  58.     pthread_mutex_destroy(&mtx);
  59.     return 0;
  60. }
复制代码

3、锁的原理

通过加锁,我们能够保证实验临界资源的操纵是原子的。可是,访问临界资源时,多个线程要申请同一把锁,那么就必须要能够看到同一把锁,那么这个锁不就成了一个临界资源了吗,那锁是怎么保证本身的安全的呢?
为了保证锁的安全,申请和开释锁的操纵也必须是原子的。如何保证呢?
在汇编的角度,如果只有一行汇编语句,我们就以为该汇编语句的实验是原子的。一般来说,是使用swap或exchange指令,以一条汇编语句,将内存和CPU寄存器的数据举行交换。如下图:

线程a是第一个申请锁的。它先将 %al 的内容写成 0,然后交换 %al 和 mutex 的内容,%al 为 1,mutex为0。接着,判断%al的内容 >0,返回,乐成拿到锁。线程a切出,寄存器%al的数据作为上下文随线程a一起切出。(固然,线程a可能在任何时候被切出,这是线程a时间片比较长的环境)。
线程b,接着申请锁。 它也先将 %al 的内容写成 0,然后交换 %al 和 mutex 的内容,%al 为 0,mutex为0。接着,判断%al的内容不大于0,于是线程b挂起等候。只有线程a将锁开释后,才能重新申请锁。
4、死锁


死锁:多线程场景中, 多个实验流相互申请对方的锁资源,并且还不开释本身已申请的锁资源,进而导致实验流无法继承向下实验代码的现象。
产生死锁四个须要条件:
1、互斥条件:一个资源每次只能被一个实验流使用。
2、请求与保持条件:一个实验流因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:一个实验流已获得的资源,在末使用完之前,不能强行剥夺。
4、循环等候条件:若干实验流之间形成一种头尾相接的循环等候资源的关系。
避免产生死锁:
1、粉碎死锁的四个须要条件
2、加锁顺序一致
3、避免锁未开释的场景
4、资源一次性分配
三、可重入与线程安全

1、概念

~ 线程安全:多个线程并发同一段代码时,不会出现不同的效果。常见对全局变量或者静态变量举行操纵,并且没有锁保护的环境下,会出现该题目。
~ 重入:同一个函数被不同的实验流调用,当前一个流程还没有实验完,就有其他的实验流再次进入,我们称之为重入。一个函数在重入的环境下,运行效果不会出现任何不同或者任何题目,则该函数被称为可重入函数,否则,是不可重入函数。
2、常见的线程不安全的环境

1、不保护共享变量的函数。
2、函数状态随着被调用,状态发生变化的函数。
3、返回指向静态变量指针的函数。
4、调用线程不安全函数的函数。
3、常见不可重入的环境

1、调用了malloc/free函数,由于malloc函数是用全局链表来管理堆的。
2、调用了尺度I/O库函数,尺度I/O库的很多实现都以不可重入的方式使用全局数据结构。
3、可重入函数体内使用了静态的数据结构。
4、可重入与线程安全联系

1、函数是可重入的,那就是线程安全的
2、函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全题目
3、如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
5、可重入与线程安全区别

1、可重入函数是线程安全函数的一种
2、线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
3、如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未开释则会产生死锁,因此是不可重入的。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小小小幸运

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表