【Linux】线程互斥

您所在的位置:网站首页 pthread_cond_init 【Linux】线程互斥

【Linux】线程互斥

2023-03-15 11:05| 来源: 网络整理| 查看: 265

​🌠 作者:@阿亮joy. 🎆专栏:《学会Linux》 🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根 在这里插入图片描述

目录 👉线程互斥👈互斥相关概念互斥量互斥锁的实现原理 👉可重入和线程安全👈概念常见的线程不安全的情况常见的线程安全的情况常见不可重入的情况常见可重入的情况可重入与线程安全的联系 👉死锁👈概念死锁的四个必要条件避免死锁避免死锁算法 👉线程同步👈概念条件变量 👉总结👈

👉线程互斥👈 互斥相关概念 临界资源:多线程执行流共享且每次只能一个执行流访问的资源叫做临界资源临界区:每个线程内部访问临界资源的代码就是临界区互斥:任何时刻,互斥保证有且只有一个执行流进入临界区访问临界资源,通常对临界资源起保护作用原子性:原子性是指一个操作要么做完,要么就没做,没有中间状态 #include #include #include #include using namespace std; // 多个线程访问同一个全局变量,并对它进行数据计算,多线程之间会相互影响 // 在并发访问的时候,因为调度时序问题,导致数据不一致的问题 int tickets = 10000; // getTickets函数会被多个执行流冲入,getTickets函数是冲入函数 void* getTickets(void* args) { (void)args; while(true) { if(tickets > 0) // 判断的本质也是计算 { usleep(1000); printf("%p: %d\n", pthread_self(), tickets--); } else break; } return nullptr; } int main() { // 多线程抢票 pthread_t t1, t2, t3; pthread_create(&t1, nullptr, getTickets, nullptr); pthread_create(&t2, nullptr, getTickets, nullptr); pthread_create(&t3, nullptr, getTickets, nullptr); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); return 0; }

在这里插入图片描述 上放的代码是模拟实现多线程抢票的场景,但是我们会发现票数 tickets 居然抢到了 -1,这是不允许存在的。那为什么会出现这种情况呢?其实是多线程访问同一个全局变量且对其进行计算时,如果不加以保护,线程之间是会相互影响的。

出现问题的原因: tickets > 0 判断的本质也是计算,计算就需要将内存中的数据读取到 CPU 的寄存器中,而将数据读取到 CPU 的寄存器中的本质就是将数据读取到当前执行流的上下文中。这样就有可能出现多个执行流进行判断是都没有小于 0,符合判断条件,然后多个执行流进行减减操作将 tickets 减到负数,那么就出现了 tickets 出现负数的情况了。除了这个原因,还会因为调度时序的问题,导致数据不一样的问题!

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

那如何解决这个问题呢?为了解决这个问题,我们需要进行加锁保护!

互斥量

要解决上面的问题,需要做到一下三点:

代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁,Linux 系统提供的这把锁叫互斥量。

在这里插入图片描述

在这里插入图片描述 在这里插入图片描述 销毁互斥量需要注意:

使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁不要销毁一个已经加锁的互斥量已经销毁的互斥量,要确保后面不会有线程再尝试加锁

调用 pthread_mutex_lock 时,可能会遇到以下情况:

互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么 pthread_mutex_lock 调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

注:pthread_mutex_trylock 申请锁时,如果申请成功,则返回 0;如果申请失败,则立即返回错误码,并不会阻塞等待锁释放。

全局互斥锁

#include #include #include #include using namespace std; // pthread_mutex_t是原生线程库提供的数据类型 pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; // 全局的互斥锁 int tickets = 10000; // 临界资源 void* getTickets(void* args) { (void)args; while(true) { // 只有获得锁的线程才能执行进入临界区,访问临界资源 pthread_mutex_lock(&mtx); // 加锁保护 // 加锁和解锁之间的代码就是临界区 if(tickets > 0) { usleep(1000); printf("%p: %d\n", pthread_self(), tickets--); pthread_mutex_unlock(&mtx); // 解锁(释放锁) } else { pthread_mutex_unlock(&mtx); // 解锁(释放锁) break; } // 不能在这里解锁,因为tickets小于0,会跳出while循环 // 无法执行到这里,从而导致锁没有释放,其余线程无法获 // 得锁而阻塞等待 // 抢完票还需要后续的动作 } return nullptr; } int main() { // 多线程抢票 pthread_t t1, t2, t3; pthread_create(&t1, nullptr, getTickets, nullptr); pthread_create(&t2, nullptr, getTickets, nullptr); pthread_create(&t3, nullptr, getTickets, nullptr); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); return 0; }

在这里插入图片描述

如果出现票都被一个线程抢到的情况,可以将票数增多一点或者休眠时间随机一点,以让每个线程都能够抢到票。

在这里插入图片描述 加锁会导致临界区代码串行访问(互斥访问),从而代码执行的效率会降低。所以,进行加锁的时候,要保证加锁的粒度越小越好,不要将不访问临界区资源的代码也加锁。

局部互斥锁

#include #include #include #include #include #include #include using namespace std; #define THREAD_NUM 5 // 线程数量 class ThreadData { public: ThreadData(const string& tname, pthread_mutex_t* pmtx) : _tname(tname) , _pmtx(pmtx) {} string _tname; pthread_mutex_t* _pmtx; }; int tickets = 10000; // 临界资源 void* getTickets(void* args) { ThreadData* td = (ThreadData*)args; while(true) { // 只有获得锁的线程才能执行进入临界区,访问临界资源 int n = pthread_mutex_lock(td->_pmtx); // 加锁 assert(n == 0); (void)n; // 加锁和解锁之间的代码就是临界区 if(tickets > 0) { usleep(rand() % 2000); printf("%s: %d\n", td->_tname.c_str(), tickets--); pthread_mutex_unlock(td->_pmtx); // 解锁 } else { pthread_mutex_unlock(td->_pmtx); break; } // 抢到票后的其它逻辑 usleep(rand() % 2000); } delete td; // 票数为0时,释放ThreadData return nullptr; } int main() { int begin = time(nullptr); pthread_mutex_t mtx; // 第二个参数为锁的属性,设置为nullptr表示不关心 pthread_mutex_init(&mtx, nullptr); // 初始化锁 pthread_t t[THREAD_NUM]; for(int i = 0; i pthread_join(t[i], nullptr); } pthread_mutex_destroy(&mtx); // 销毁锁 int end = time(nullptr); // 输出抢票所消耗的时间 cout } string _name; func_t _func; pthread_mutex_t* _pmtx; pthread_cond_t* _pcon; }; void func1(const string& name, pthread_mutex_t* pmtx, pthread_cond_t* pcon) { while(!quit) { // 检测临界资源是否就绪,需要在加锁和解锁之间检测 // wait等待一定要在加锁和解锁之间进行 pthread_mutex_lock(pmtx); // if(临界资源是否就绪 - 否) pthread_cond_wait(pcon, pmtx); // wait代码被执行,当前线程会被立即被阻塞,也就是线程的状态变成了S,放入到等待队列中 // 线程被唤醒时,会有一定的唤醒顺序 pthread_cond_wait(pcon, pmtx); // 现在不需要关心pmtx,后续会讲解 cout // wait等待一定要在加锁和解锁之间进行 pthread_mutex_lock(pmtx); // wait代码被执行,当前线程会被立即被阻塞 pthread_cond_wait(pcon, pmtx); cout // wait等待一定要在加锁和解锁之间进行 pthread_mutex_lock(pmtx); // wait代码被执行,当前线程会被立即被阻塞 pthread_cond_wait(pcon, pmtx); cout pthread_mutex_t mtx; pthread_cond_t cond; pthread_mutex_init(&mtx, nullptr); pthread_cond_init(&cond, nullptr); pthread_t tids[THREAD_NUM]; func_t funcs[THREAD_NUM] = {func1, func2, func3}; for(int i = 0; i cout


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3