【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脚本(shell)的使用方式

《Linux脚本(shell)的使用方式》:本文主要介绍Linux脚本(shell)的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录概述语法详解数学运算表达式Shell变量变量分类环境变量Shell内部变量自定义变量:定义、赋值自定义变量:引用、修改、删

Java中实现线程的创建和启动的方法

《Java中实现线程的创建和启动的方法》在Java中,实现线程的创建和启动是两个不同但紧密相关的概念,理解为什么要启动线程(调用start()方法)而非直接调用run()方法,是掌握多线程编程的关键,... 目录1. 线程的生命周期2. start() vs run() 的本质区别3. 为什么必须通过 st

Linux链表操作方式

《Linux链表操作方式》:本文主要介绍Linux链表操作方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、链表基础概念与内核链表优势二、内核链表结构与宏解析三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势六、典型应用场景七、调试技巧与

详解Linux中常见环境变量的特点与设置

《详解Linux中常见环境变量的特点与设置》环境变量是操作系统和用户设置的一些动态键值对,为运行的程序提供配置信息,理解环境变量对于系统管理、软件开发都很重要,下面小编就为大家详细介绍一下吧... 目录前言一、环境变量的概念二、常见的环境变量三、环境变量特点及其相关指令3.1 环境变量的全局性3.2、环境变

Linux系统中的firewall-offline-cmd详解(收藏版)

《Linux系统中的firewall-offline-cmd详解(收藏版)》firewall-offline-cmd是firewalld的一个命令行工具,专门设计用于在没有运行firewalld服务的... 目录主要用途基本语法选项1. 状态管理2. 区域管理3. 服务管理4. 端口管理5. ICMP 阻断

Linux实现线程同步的多种方式汇总

《Linux实现线程同步的多种方式汇总》本文详细介绍了Linux下线程同步的多种方法,包括互斥锁、自旋锁、信号量以及它们的使用示例,通过这些同步机制,可以解决线程安全问题,防止资源竞争导致的错误,示例... 目录什么是线程同步?一、互斥锁(单人洗手间规则)适用场景:特点:二、条件变量(咖啡厅取餐系统)工作流

Java中常见队列举例详解(非线程安全)

《Java中常见队列举例详解(非线程安全)》队列用于模拟队列这种数据结构,队列通常是指先进先出的容器,:本文主要介绍Java中常见队列(非线程安全)的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一.队列定义 二.常见接口 三.常见实现类3.1 ArrayDeque3.1.1 实现原理3.1.2

SpringBoot3中使用虚拟线程的完整步骤

《SpringBoot3中使用虚拟线程的完整步骤》在SpringBoot3中使用Java21+的虚拟线程(VirtualThreads)可以显著提升I/O密集型应用的并发能力,这篇文章为大家介绍了详细... 目录1. 环境准备2. 配置虚拟线程方式一:全局启用虚拟线程(Tomcat/Jetty)方式二:异步

Linux中修改Apache HTTP Server(httpd)默认端口的完整指南

《Linux中修改ApacheHTTPServer(httpd)默认端口的完整指南》ApacheHTTPServer(简称httpd)是Linux系统中最常用的Web服务器之一,本文将详细介绍如何... 目录一、修改 httpd 默认端口的步骤1. 查找 httpd 配置文件路径2. 编辑配置文件3. 保存

Linux使用scp进行远程目录文件复制的详细步骤和示例

《Linux使用scp进行远程目录文件复制的详细步骤和示例》在Linux系统中,scp(安全复制协议)是一个使用SSH(安全外壳协议)进行文件和目录安全传输的命令,它允许在远程主机之间复制文件和目录,... 目录1. 什么是scp?2. 语法3. 示例示例 1: 复制本地目录到远程主机示例 2: 复制远程主