【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换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

Linux系统配置NAT网络模式的详细步骤(附图文)

《Linux系统配置NAT网络模式的详细步骤(附图文)》本文详细指导如何在VMware环境下配置NAT网络模式,包括设置主机和虚拟机的IP地址、网关,以及针对Linux和Windows系统的具体步骤,... 目录一、配置NAT网络模式二、设置虚拟机交换机网关2.1 打开虚拟机2.2 管理员授权2.3 设置子

Linux系统中卸载与安装JDK的详细教程

《Linux系统中卸载与安装JDK的详细教程》本文详细介绍了如何在Linux系统中通过Xshell和Xftp工具连接与传输文件,然后进行JDK的安装与卸载,安装步骤包括连接Linux、传输JDK安装包... 目录1、卸载1.1 linux删除自带的JDK1.2 Linux上卸载自己安装的JDK2、安装2.1

Linux卸载自带jdk并安装新jdk版本的图文教程

《Linux卸载自带jdk并安装新jdk版本的图文教程》在Linux系统中,有时需要卸载预装的OpenJDK并安装特定版本的JDK,例如JDK1.8,所以本文给大家详细介绍了Linux卸载自带jdk并... 目录Ⅰ、卸载自带jdkⅡ、安装新版jdkⅠ、卸载自带jdk1、输入命令查看旧jdkrpm -qa

Linux samba共享慢的原因及解决方案

《Linuxsamba共享慢的原因及解决方案》:本文主要介绍Linuxsamba共享慢的原因及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux samba共享慢原因及解决问题表现原因解决办法总结Linandroidux samba共享慢原因及解决

Spring Boot3虚拟线程的使用步骤详解

《SpringBoot3虚拟线程的使用步骤详解》虚拟线程是Java19中引入的一个新特性,旨在通过简化线程管理来提升应用程序的并发性能,:本文主要介绍SpringBoot3虚拟线程的使用步骤,... 目录问题根源分析解决方案验证验证实验实验1:未启用keep-alive实验2:启用keep-alive扩展建

新特性抢先看! Ubuntu 25.04 Beta 发布:Linux 6.14 内核

《新特性抢先看!Ubuntu25.04Beta发布:Linux6.14内核》Canonical公司近日发布了Ubuntu25.04Beta版,这一版本被赋予了一个活泼的代号——“Plu... Canonical 昨日(3 月 27 日)放出了 Beta 版 Ubuntu 25.04 系统镜像,代号“Pluc

Linux安装MySQL的教程

《Linux安装MySQL的教程》:本文主要介绍Linux安装MySQL的教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux安装mysql1.Mysql官网2.我的存放路径3.解压mysql文件到当前目录4.重命名一下5.创建mysql用户组和用户并修

Linux上设置Ollama服务配置(常用环境变量)

《Linux上设置Ollama服务配置(常用环境变量)》本文主要介绍了Linux上设置Ollama服务配置(常用环境变量),Ollama提供了多种环境变量供配置,如调试模式、模型目录等,下面就来介绍一... 目录在 linux 上设置环境变量配置 OllamPOgxSRJfa手动安装安装特定版本查看日志在

Linux系统之主机网络配置方式

《Linux系统之主机网络配置方式》:本文主要介绍Linux系统之主机网络配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、查看主机的网络参数1、查看主机名2、查看IP地址3、查看网关4、查看DNS二、配置网卡1、修改网卡配置文件2、nmcli工具【通用