【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

相关文章

Oracle数据库定时备份脚本方式(Linux)

《Oracle数据库定时备份脚本方式(Linux)》文章介绍Oracle数据库自动备份方案,包含主机备份传输与备机解压导入流程,强调需提前全量删除原库数据避免报错,并需配置无密传输、定时任务及验证脚本... 目录说明主机脚本备机上自动导库脚本整个自动备份oracle数据库的过程(建议全程用root用户)总结

Linux如何查看文件权限的命令

《Linux如何查看文件权限的命令》Linux中使用ls-R命令递归查看指定目录及子目录下所有文件和文件夹的权限信息,以列表形式展示权限位、所有者、组等详细内容... 目录linux China编程查看文件权限命令输出结果示例这里是查看tomcat文件夹总结Linux 查看文件权限命令ls -l 文件或文件夹

idea的终端(Terminal)cmd的命令换成linux的命令详解

《idea的终端(Terminal)cmd的命令换成linux的命令详解》本文介绍IDEA配置Git的步骤:安装Git、修改终端设置并重启IDEA,强调顺序,作为个人经验分享,希望提供参考并支持脚本之... 目录一编程、设置前二、前置条件三、android设置四、设置后总结一、php设置前二、前置条件

Linux系统中查询JDK安装目录的几种常用方法

《Linux系统中查询JDK安装目录的几种常用方法》:本文主要介绍Linux系统中查询JDK安装目录的几种常用方法,方法分别是通过update-alternatives、Java命令、环境变量及目... 目录方法 1:通过update-alternatives查询(推荐)方法 2:检查所有已安装的 JDK方

Linux系统之lvcreate命令使用解读

《Linux系统之lvcreate命令使用解读》lvcreate是LVM中创建逻辑卷的核心命令,支持线性、条带化、RAID、镜像、快照、瘦池和缓存池等多种类型,实现灵活存储资源管理,需注意空间分配、R... 目录lvcreate命令详解一、命令概述二、语法格式三、核心功能四、选项详解五、使用示例1. 创建逻

Linux下在线安装启动VNC教程

《Linux下在线安装启动VNC教程》本文指导在CentOS7上在线安装VNC,包含安装、配置密码、启动/停止、清理重启步骤及注意事项,强调需安装VNC桌面以避免黑屏,并解决端口冲突和目录权限问题... 目录描述安装VNC安装 VNC 桌面可能遇到的问题总结描js述linux中的VNC就类似于Window

linux下shell脚本启动jar包实现过程

《linux下shell脚本启动jar包实现过程》确保APP_NAME和LOG_FILE位于目录内,首次启动前需手动创建log文件夹,否则报错,此为个人经验,供参考,欢迎支持脚本之家... 目录linux下shell脚本启动jar包样例1样例2总结linux下shell脚本启动jar包样例1#!/bin

Linux之platform平台设备驱动详解

《Linux之platform平台设备驱动详解》Linux设备驱动模型中,Platform总线作为虚拟总线统一管理无物理总线依赖的嵌入式设备,通过platform_driver和platform_de... 目录platform驱动注册platform设备注册设备树Platform驱动和设备的关系总结在 l

linux批量替换文件内容的实现方式

《linux批量替换文件内容的实现方式》本文总结了Linux中批量替换文件内容的几种方法,包括使用sed替换文件夹内所有文件、单个文件内容及逐行字符串,强调使用反引号和绝对路径,并分享个人经验供参考... 目录一、linux批量替换文件内容 二、替换文件内所有匹配的字符串 三、替换每一行中全部str1为st

Java中的xxl-job调度器线程池工作机制

《Java中的xxl-job调度器线程池工作机制》xxl-job通过快慢线程池分离短时与长时任务,动态降级超时任务至慢池,结合异步触发和资源隔离机制,提升高频调度的性能与稳定性,支撑高并发场景下的可靠... 目录⚙️ 一、调度器线程池的核心设计 二、线程池的工作流程 三、线程池配置参数与优化 四、总结:线程