【Linux】激情讨论线程安全 AND 各种锁

2024-04-01 00:12

本文主要是介绍【Linux】激情讨论线程安全 AND 各种锁,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1.STL中的容器是否是线程安全的?
  • 2.智能指针是否是线程安全的?
  • 3.其他常见的各种锁
    • 3.0理解为什么有这么多种锁
    • 3.1悲观锁(Pessimistic Lock)
    • 3.2乐观锁(Optimistic Lock)
    • 3.3CAS操作(Compare-and-Swap)
    • 3.4自旋锁:
    • 3.5公平锁:
    • 3.6非公平锁:
    • 3.7读写锁(ReadWriteLock)
      • 1.读者写者场景
      • 2.待分析情况
      • 3.读写锁的介绍
  • 4.读写锁的场景
    • 4.1模拟案例1
    • 4.2模拟案例2【看一下即可】

1.STL中的容器是否是线程安全的?

不是.
STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全,,会对性能造成巨大的影响。对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶).
STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行维护线程安全。

C++标准模板库(STL)中的容器本身不是线程安全的。这意味着在没有适当的外部同步机制的情况下,从多个线程同时访问同一个STL容器可能会导致数据竞争和不可预测的行为。

如果多个线程仅仅是读取STL容器的数据,而没有任何写入操作,这通常是安全的。但是,如果至少有一个线程在修改容器(如添加、删除元素),而其他线程正在读取或写入同一个容器,则必须使用适当的同步机制(如互斥锁)来保护对容器的访问。

在某些情况下,可以使用专为并发设计的容器,如C++ 11及以上版本中的std::atomic或std::shared_mutex,或者使用其他库提供的线程安全容器。此外,程序员还可以通过在使用容器前获取锁并在操作完成后释放锁,来防止多个线程同时修改容器。

总的来说,当涉及到多线程环境中的STL容器时,程序员需要负责确保线程安全性。

2.智能指针是否是线程安全的?

unique_ptr, 由于只是在当前代码块范围内生效, 不涉及线程安全问题,但是我们使用指针通常是用来指向对象的,调用的对象的方法可能不是线程安全的。
shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效地进行原子的操作引用计数。
**智能指针的线程安全性取决于其使用方式和上下文。**智能指针本身,如std::shared_ptr和std::unique_ptr,具体来说,对智能指针对象的多个线程同时读写操作可能会导致数据竞争和不一致的行为。

std::shared_ptr的引用计数操作是线程安全的。这意味着多个线程可以同时增加或减少同一个std::shared_ptr实例的引用计数,而不会出现竞态条件。这是因为std::shared_ptr的引用计数操作内部使用了原子操作,确保了线程安全。

线程安全并不意味着可以随意地在多线程环境中使用智能指针。即使引用计数是线程安全的,使用智能指针访问它所指向的对象或资源可能并不是线程安全的。如果多个线程同时访问或修改同一个对象,而没有适当的同步机制(如互斥锁),那么仍然可能出现数据竞争和不一致的情况。

因此,在使用智能指针时,需要谨慎考虑多线程环境下的访问和修改操作。如果需要确保线程安全,应该使用适当的同步机制来保护对共享资源的访问。

总结来说,智能指针的线程安全性是一个复杂的问题,取决于具体的使用方式和上下文。虽然std::shared_ptr的引用计数操作是线程安全的,但使用智能指针访问和修改共享资源仍然需要谨慎处理,以确保线程安全。

3.其他常见的各种锁

3.0理解为什么有这么多种锁

  1. 锁是为了解决【线程安全】问题的,【线程安全】问题是一个复杂的问题,他又各种各样的场景。
  2. 设计者为了尽可能地提升OS地效率,尽量把能优化地地方尽量优化。【这种思想是应用与任何事情的,所以经常有人惊叹计算机这个“神物”的优质设计】
  3. 每种锁都有其适用的场景和优缺点,使用时需要根据具体的业务需求和系统环境进行选择。

3.1悲观锁(Pessimistic Lock)

每次获取数据的时候,都会担心数据被修改,因此每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改。使用完成后,数据会被解锁。由于数据被加锁,期间对该数据进行读写的其他线程都会进行等待。悲观锁比较适合写入操作比较频繁的场景。【之前学的互斥锁/信号量都属于这个范畴,在访问临界资源前由于比较“悲观”,都先去申请锁】

3.2乐观锁(Optimistic Lock)

持有乐观的态度,认为数据冲突发生的概率较低,允许多个任务并行地对数据进行操作,而不加锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。在乐观锁的机制下,对数据的操作不会立即进行冲突检测和加锁,而是在数据提交时通过一种机制来验证是否存在冲突。乐观锁通常通过版本号(也称为时间戳)实现。每次读取数据时,都会获取当前版本号,并将其与修改前的版本号进行比对。如果两个版本号相同,则认为数据没有被其他任务修改,允许当前任务进行修改操作并更新版本号。如果版本号不同,则表示数据已被其他任务修改,此时需要处理冲突。乐观锁有利于提高系统的吞吐量和并发性能,但在高并发的场景下可能面临挑战。

3.3CAS操作(Compare-and-Swap)

是基于内存模型,通过原子操作保证线程安全的一种机制。CAS包含三个操作数:内存值V、预期值A和新值B。当且仅当预期值A和内存值V相同时,才会将内存值修改为B,若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。CAS可以用于实现原子操作,如原子增加、原子减少等,避免多个线程同时访问和修改同一数据导致的数据不一致问题。CAS还可以用于实现分布式系统中的数据一致性。

3.4自旋锁:

是一种特殊类型的锁,当线程尝试获取锁时,如果锁已被其他线程持有,则线程不会立即阻塞,而是会“自旋”等待锁被释放。这通常用于短暂等待的情况,以避免线程上下文切换的开销。纯自旋锁通过在一个变量上自旋等待来实现锁。

引进自旋锁

多线程背景下,A线程成功申请锁,进入临界区访问临界资源,A线程访问临界资源的时间即【进临界区到出临界区的时间】是不确定的,之前讲的互斥锁,当B线程想要访问临界资源时,申请锁失败,进行阻塞等待。如果我们通过 【某种手段/或者自定义等待时间的容忍度】 获取到临界资源被访问的时间段的长短,我们就可以做出如下优化:当时间比较久,使用互斥锁,即申请不到就阻塞等待。当时间比较短,申请不到不阻塞等待而是轮询检测锁的状态,一旦可以申请就去申请。这种方案就是自旋锁。

自旋锁的接口

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

3.5公平锁:

线程在获取锁之前,会查看是否有队列在等待,如果有的话就按照顺序获取锁,先到先得。公平锁单独维护了一个队列,确保所有线程按照请求锁的顺序获取锁。虽然它保证了公平性,但可能会增加线程切换的次数,从而降低性能。

3.6非公平锁:

表示线程获取锁的顺序与线程请求锁的时间早晚无关,先来不一定先获得锁。非公平锁的性能通常比公平锁快5—10倍,因为在没有线程等待时,它允许一个线程直接获取锁,而无需检查队列。

3.7读写锁(ReadWriteLock)

1.读者写者场景

PC模型中,生产者/写者 会生产数据并发送数据到交易场所,消费者/读者 会读取数据并把数据拿走进行处理。而读者写者场景是,写者会生产数据并发送数据到交易场所,读者只读取数据不拿走数据。

2.待分析情况

  1. 写者在写,读者不能读,因为读到的大可能是不完整的数据,造成读到错误数据。
  2. 读者在读,写着不能改,如果二者都发生,会造成数据错误。
  3. 对于数据,可以有多个读者同时读,因为他们只读不拿。但是不能有多个写着同时写,同一时间只能有一个写者写。
  4. 写者写了数据,没有读者读,这部分数据从某种角度来说无意义(假设写者写的数据的意义就是被读者读)。读者要读数据,没有写者写数据,这也是不合理的。— 同步问题。
  5. 读者和写者:互斥与同步 读者与读者:共享关系 写者和写者:互斥
  6. 场景:多个读者要进入交易场所读数据,也有多个写者要进入交易场所写数据,读者优先还是写者优先?a. 读者优先。读者/写者同时到来,让读者先读,读者走完了,再让写者写。b. 写者优先,读者/写者同时到来,让正在读的人退出,让写者先写,写者写完再让读者读。pthread库中的读写锁默认是读者优先。
  7. 通常情况下,数据被读取的频率非常高,而被修改的频率特别低。即读操作远多于写操作。

3.读写锁的介绍

管理一组锁,一个是只读的锁(共享锁),一个是只写的锁(互斥锁)。它允许多个线程同时读数据,但在写入数据时只允许一个线程进行。这有助于提高并发性能,特别是在读操作远多于写操作的场景中。

设置读写优先

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和 PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁

初始化

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t
*restrict attr);

销毁

int pthread_rwlock_destroy(pthread rwlock t *rwlock);

加锁和解锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

4.读写锁的场景

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低程序的效率。上面提到,读操作出现的频率更多,如果读者比较多,写者就要一直等吗?那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
在这里插入图片描述

4.1模拟案例1

使用POSIX线程库(pthread)中的读写锁(pthread_rwlock_t)。以下展示了如何使用读写锁来同步对共享资源的访问。

#include <iostream>  
#include <pthread.h>  
#include <unistd.h>  // 共享资源  
int shared_resource = 0;  // 读写锁  
pthread_rwlock_t rwlock;  // 读取共享资源的线程函数  
void* reader(void* arg) {  while (true) {  // 加读锁  pthread_rwlock_rdlock(&rwlock);  std::cout << "Reader: shared_resource = " << shared_resource << std::endl;  // 解锁  pthread_rwlock_unlock(&rwlock);  usleep(100000); // 模拟读取操作耗时  }  return nullptr;  
}  // 写入共享资源的线程函数  
void* writer(void* arg) {  int value = 1;  while (true) {  // 加写锁  pthread_rwlock_wrlock(&rwlock);  shared_resource = value;  std::cout << "Writer: shared_resource set to " << shared_resource << std::endl;  value = (value == 1) ? 0 : 1; // 切换写入值  // 解锁  pthread_rwlock_unlock(&rwlock);  usleep(200000); // 模拟写入操作耗时  }  return nullptr;  
}  int main() {  // 初始化读写锁  if (pthread_rwlock_init(&rwlock, nullptr) != 0) {  std::cerr << "Failed to initialize read-write lock" << std::endl;  return 1;  }  // 创建读取线程和写入线程  pthread_t reader_thread, writer_thread;  if (pthread_create(&reader_thread, nullptr, reader, nullptr) != 0 ||  pthread_create(&writer_thread, nullptr, writer, nullptr) != 0) {  std::cerr << "Failed to create threads" << std::endl;  return 1;  }  // 等待线程结束(这里只是示例,实际应用中可能需要更复杂的线程管理)  pthread_join(reader_thread, nullptr);  pthread_join(writer_thread, nullptr);  // 销毁读写锁  pthread_rwlock_destroy(&rwlock);  return 0;  
}

这个示例中,我们定义了一个共享资源shared_resource和一个读写锁rwlock。我们创建了两个线程函数:reader用于读取共享资源,writer用于写入共享资源。在读取和写入共享资源之前,线程会先获取相应的锁(读锁或写锁),操作完成后释放锁。这样,多个读取线程可以同时访问共享资源,但写入线程在修改共享资源时会阻止其他线程(无论是读取还是写入)访问。

g++ -o readwrite_lock_example readwrite_lock_example.cpp -lpthread  

4.2模拟案例2【看一下即可】

在这里插入图片描述

#include <vector>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <pthread.h>// 线程属性
struct ThreadAttr
{pthread_t tid;std::string threadName;
};volatile int ticket = 1000;
pthread_rwlock_t rwlock;
volatile int g_readerNum = 0;/*
lock();
readerNum++;
unlock();
$$读数据$$
lock();
readerNum--;
unlock();
*/// 读者读票数 不对ticket做操作
void *reader_startRoutine(void *arg)
{char *readerName = (char *)arg;while (true){pthread_rwlock_rdlock(&rwlock);if (ticket <= 0){pthread_rwlock_unlock(&rwlock);break;}g_readerNum++;sleep(1);printf("%s: remain tickets:%d readerNum:%d\n", readerName, ticket,g_readerNum);sleep(1);g_readerNum--;pthread_rwlock_unlock(&rwlock);usleep(1);}return nullptr;
}/*
lock();
if(readerNum > 0)
{unlock();return;
}
$$写数据$$unlock();
*/
// 写者对ticket做操作
void *writer_startRoutine(void *arg)
{char *writerName = (char *)arg;while (true){pthread_rwlock_wrlock(&rwlock);if (g_readerNum > 0){pthread_rwlock_unlock(&rwlock);return nullptr;}if (ticket <= 0){pthread_rwlock_unlock(&rwlock);break;}sleep(1);printf("%s: reduced tickets:%d\n", writerName, --ticket);sleep(1);pthread_rwlock_unlock(&rwlock);usleep(1);}return nullptr;
}// 拼接读者名称
std::string create_readerName(std::size_t index)
{// static const std::ios_base::openmode// std::ios_base::ate = (std::ios_base::openmode)2// Open and seek to end immediately after opening.std::ostringstream oss("thread reader ", std::ios_base::ate);oss << index;return oss.str();
}// 拼接写者名称
std::string create_writerName(std::size_t index)
{std::ostringstream oss("thread writer ", std::ios_base::ate);oss << index;return oss.str();
}// 创建读者线程
void create_readers(std::vector<ThreadAttr> &vec)
{for (std::size_t i = 0; i < vec.size(); ++i){vec[i].threadName = create_readerName(i);pthread_create(&vec[i].tid, nullptr, reader_startRoutine, (void *)vec[i].threadName.c_str());}
}// 创建写者线程
void create_writers(std::vector<ThreadAttr> &vec)
{for (std::size_t i = 0; i < vec.size(); ++i){vec[i].threadName = create_writerName(i);pthread_create(&vec[i].tid, nullptr, writer_startRoutine, (void *)vec[i].threadName.c_str());}
}// 逆序回收线程
void join_threads(std::vector<ThreadAttr> const &vec)
{for (std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin();it != vec.rend(); ++it){pthread_t const &tid = it->tid;pthread_join(tid, nullptr);}
}// 设置读写优先级
void init_rwlock()
{
#ifdef WriteFirst // 写优先pthread_rwlockattr_t attr;pthread_rwlockattr_init(&attr);pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);pthread_rwlock_init(&rwlock, &attr);pthread_rwlockattr_destroy(&attr);
#else // 读优先 会造成写饥饿pthread_rwlock_init(&rwlock, nullptr);
#endif
}int main()
{const std::size_t readerNum = 10;const std::size_t writerNum = 2;std::vector<ThreadAttr> readers(readerNum);std::vector<ThreadAttr> writers(writerNum);init_rwlock();create_readers(readers);sleep(1);create_writers(writers);join_threads(writers);join_threads(readers);pthread_rwlock_destroy(&rwlock);
}

这篇关于【Linux】激情讨论线程安全 AND 各种锁的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/865738

相关文章

Linux之systemV共享内存方式

《Linux之systemV共享内存方式》:本文主要介绍Linux之systemV共享内存方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、工作原理二、系统调用接口1、申请共享内存(一)key的获取(二)共享内存的申请2、将共享内存段连接到进程地址空间3、将

快速修复一个Panic的Linux内核的技巧

《快速修复一个Panic的Linux内核的技巧》Linux系统中运行了不当的mkinitcpio操作导致内核文件不能正常工作,重启的时候,内核启动中止于Panic状态,该怎么解决这个问题呢?下面我们就... 感谢China编程(www.chinasem.cn)网友 鸢一雨音 的投稿写这篇文章是有原因的。为了配置完

JAVA保证HashMap线程安全的几种方式

《JAVA保证HashMap线程安全的几种方式》HashMap是线程不安全的,这意味着如果多个线程并发地访问和修改同一个HashMap实例,可能会导致数据不一致和其他线程安全问题,本文主要介绍了JAV... 目录1. 使用 Collections.synchronizedMap2. 使用 Concurren

Linux命令之firewalld的用法

《Linux命令之firewalld的用法》:本文主要介绍Linux命令之firewalld的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux命令之firewalld1、程序包2、启动firewalld3、配置文件4、firewalld规则定义的九大

Linux之计划任务和调度命令at/cron详解

《Linux之计划任务和调度命令at/cron详解》:本文主要介绍Linux之计划任务和调度命令at/cron的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux计划任务和调度命令at/cron一、计划任务二、命令{at}介绍三、命令语法及功能 :at

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

kali linux 无法登录root的问题及解决方法

《kalilinux无法登录root的问题及解决方法》:本文主要介绍kalilinux无法登录root的问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,... 目录kali linux 无法登录root1、问题描述1.1、本地登录root1.2、ssh远程登录root2、

Linux ls命令操作详解

《Linuxls命令操作详解》通过ls命令,我们可以查看指定目录下的文件和子目录,并结合不同的选项获取详细的文件信息,如权限、大小、修改时间等,:本文主要介绍Linuxls命令详解,需要的朋友可... 目录1. 命令简介2. 命令的基本语法和用法2.1 语法格式2.2 使用示例2.2.1 列出当前目录下的文

Python从零打造高安全密码管理器

《Python从零打造高安全密码管理器》在数字化时代,每人平均需要管理近百个账号密码,本文将带大家深入剖析一个基于Python的高安全性密码管理器实现方案,感兴趣的小伙伴可以参考一下... 目录一、前言:为什么我们需要专属密码管理器二、系统架构设计2.1 安全加密体系2.2 密码强度策略三、核心功能实现详解