数据结构——C++无锁队列

news2024/11/28 6:26:25

数据结构——C++无锁队列

贺志国
2023.7.11

上一篇博客给出了最简单的C++数据结构——堆栈的几种无锁实现方法。队列的挑战与栈的有些不同,因为Push()Pop()函数在队列中操作的不是同一个地方。因此同步的需求就不一样。需要保证对一端的修改是正确的,且对另一端是可见的。因此队列需要两个Node指针:head_tail_。这两个指针都是原子变量,从而可在加锁的情形下,可给多个线程同时访问。首先来分析单生产者/单消费者的情形。

一、单生产者/单消费者模型下的无锁队列

单生产者/单消费者模型就是指,在某一时刻,最多只存在一个线程调用Push()函数,最多只存在一个线程调用Pop()函数。该情形下的代码(文件命名为 lock_free_queue.h)如下:

#pragma once

#include <atomic>
#include <memory>

template <typename T>
class LockFreeQueue {
 public:
  LockFreeQueue() : head_(new Node), tail_(head_.load()) {}
  ~LockFreeQueue() {
    while (Node* old_head = head_.load()) {
      head_.store(old_head->next);
      delete old_head;
    }
  }

  LockFreeQueue(const LockFreeQueue& other) = delete;
  LockFreeQueue& operator=(const LockFreeQueue& other) = delete;

  bool IsEmpty() const { return head_.load() == tail_.load(); }

  void Push(const T& data) {
    auto new_data = std::make_shared<T>(data);
    Node* p = new Node;             // 3
    Node* old_tail = tail_.load();  // 4
    old_tail->data.swap(new_data);  // 5
    old_tail->next = p;             // 6
    tail_.store(p);                 // 7
  }

  std::shared_ptr<T> Pop() {
    Node* old_head = PopHead();
    if (old_head == nullptr) {
      return std::shared_ptr<T>();
    }

    const std::shared_ptr<T> res(old_head->data);  // 2
    delete old_head;
    return res;
  }

 private:
  // If the struct definition of Node is placed in the private data member
  // field where 'head_' is defined, the following compilation error will occur:
  //
  // error: 'Node' has not been declared ...
  //
  // It should be a bug of the compiler. The struct definition of Node is put in
  // front of the private member function `DeleteNodes` to eliminate this error.
  struct Node {
    // std::make_shared does not throw an exception.
    Node() : data(nullptr), next(nullptr) {}

    std::shared_ptr<T> data;
    Node* next;
  };

 private:
  Node* PopHead() {
    Node* old_head = head_.load();
    if (old_head == tail_.load()) {  // 1
      return nullptr;
    }
    head_.store(old_head->next);
    return old_head;
  }

 private:
  std::atomic<Node*> head_;
  std::atomic<Node*> tail_;
};

一眼望去,这个实现没什么毛病,当只有一个线程调用Push()Pop()时,这种情况下队列一点毛病没有。Push()Pop()之间的先行(happens-before )关系非常重要,直接关系到能否安全地获取到队列中的数据。对尾部节点tail_的存储⑦(对应于上述代码片段中的注释// 7,下同)同步(synchronizes with)于对tail_的加载①,存储之前节点的data指针⑤先行(happens-before )于存储tail_。并且,加载tail_先行于加载data指针②,所以对data的存储要先行于加载,一切都没问题。因此,这是一个完美的单生产者/单消费者(SPSC, single-producer, single-consume)队列。
问题在于当多线程对Push()Pop()并发调用。先看一下Push():如果有两个线程并发调用Push(),会新分配两个节点作为虚拟节点③,也会读取到相同的tail_值④,因此也会同时修改同一个节点,同时设置datanext指针⑤⑥,存在明显的数据竞争!
PopHead()函数也有类似的问题。当有两个线程并发的调用这个函数时,这两个线程就会读取到同一个head_,并且会通过next指针去修改旧值。两个线程都能索引到同一个节点——真是一场灾难!不仅要保证只有一个Pop()线程可以访问给定项,还要保证其他线程在读取head_时,可以安全的访问节点中的next,这就是和无锁栈中Pop()一样的问题了。
Pop()的问题解决了,那么Push()呢?问题在于为了获取Push()Pop()间的先行关系,就需要在为虚拟节点设置数据项前,更新tail_指针。并发访问Push()时,因为每个线程所读取到的是同一个tail_,所以线程会进行竞争。

说明
先行(happens-before )与同步(synchronizes with)是原子变量在线程间同步的两个重要关系。
Happens-before(先行)
Regardless of threads, evaluation A happens-before evaluation B if any of the following is true: 1) A is sequenced-before B; 2) A inter-thread happens before B. The implementation is required to ensure that the happens-before relation is acyclic, by introducing additional synchronization if necessary (it can only be necessary if a consume operation is involved). If one evaluation modifies a memory location, and the other reads or modifies the same memory location, and if at least one of the evaluations is not an atomic operation, the behavior of the program is undefined (the program has a data race) unless there exists a happens-before relationship between these two evaluations.
(无关乎线程,若下列任一为真,则求值 A 先发生于求值 B :1) A 先序于 B;2) A 线程间先发生于 B。要求实现确保先发生于关系是非循环的,若有必要则引入额外的同步(若引入消费操作,它才可能为必要)。若一次求值修改一个内存位置,而其他求值读或修改同一内存位置,且至少一个求值不是原子操作,则程序的行为未定义(程序有数据竞争),除非这两个求值之间存在先发生于关系。)
Synchronizes with(同步)
If an atomic store in thread A is a release operation, an atomic load in thread B from the same variable is an acquire operation, and the load in thread B reads a value written by the store in thread A, then the store in thread A synchronizes-with the load in thread B. Also, some library calls may be defined to synchronize-with other library calls on other threads.
(如果在线程A上的一个原子存储是释放操作,在线程B上的对相同变量的一个原子加载是获得操作,且线程B上的加载读取由线程A上的存储写入的值,则线程A上的存储同步于线程B上的加载。此外,某些库调用也可能定义为同步于其它线程上的其它库调用。)

二、多生产者-多消费者无锁队列

下述代码存在bug,需要进一步调试(文件命名为 lock_free_queue.h):

#pragma once

#include <atomic>
#include <memory>

template <typename T>
class LockFreeQueue {
 public:
  LockFreeQueue() : head_(CountedNodePtr(new Node)), tail_(head_.load()) {}

  ~LockFreeQueue() {
    while (Pop()) {
      // Do nothing
    }
  }

  LockFreeQueue(const LockFreeQueue& other) = delete;
  LockFreeQueue& operator=(const LockFreeQueue& other) = delete;

  bool IsEmpty() const { return head_.load().ptr == tail_.load().ptr; }

  void Push(const T& data) {
    auto new_data = std::make_unique<T>(data);
    CountedNodePtr new_next(new Node);
    new_next.external_count = 1;
    CountedNodePtr old_tail = tail_.load();

    while (true) {
      IncreaseExternalCount(&tail_, &old_tail);

      T* old_data = nullptr;
      if (old_tail.ptr->data.compare_exchange_strong(old_data,
                                                     new_data.get())) {
        CountedNodePtr old_next = old_tail.ptr->next.load();
        if (!old_tail.ptr->next.compare_exchange_strong(old_next, new_next)) {
          delete new_next.ptr;
          new_next = old_next;
        }
        SetNewTail(new_next, &old_tail);

        // Release the ownership of the managed object so that the data will not
        // be deleted beyond the scope the unique_ptr<T>.
        new_data.release();
        break;
      } else {
        CountedNodePtr old_next = old_tail.ptr->next.load();
        if (old_tail.ptr->next.compare_exchange_strong(old_next, new_next)) {
          old_next = new_next;
          new_next.ptr = new Node;
        }
        SetNewTail(old_next, &old_tail);
      }
    }
  }

  std::unique_ptr<T> Pop() {
    CountedNodePtr old_head = head_.load();
    while (true) {
      IncreaseExternalCount(&head_, &old_head);
      Node* ptr = old_head.ptr;
      if (ptr == tail_.load().ptr) {
        ptr->ReleaseRef();
        return std::unique_ptr<T>();
      }

      CountedNodePtr next = ptr->next.load();
      if (head_.compare_exchange_strong(old_head, next)) {
        T* res = ptr->data.exchange(nullptr);
        FreeExternalCounter(&old_head);
        return std::unique_ptr<T>(res);
      }

      ptr->ReleaseRef();
    }
  }

 private:
  // Forward class declaration
  struct Node;
  struct CountedNodePtr {
    CountedNodePtr() : ptr(nullptr), external_count(0) {}
    explicit CountedNodePtr(Node* input_ptr)
        : ptr(input_ptr), external_count(0) {}

    Node* ptr;
    int external_count;
  };

  struct NodeCounter {
    NodeCounter() : internal_count(0), external_counters(0) {}

    // external_counters occupies only 2bits, where the maximum value stored
    // is 3. Note that you need only 2 bits for the external_counters because
    // there are at most two such counters. By using a bit field for this and
    // specifying internal_count as a 30-bit value, you keep the total counter
    // size to 32 bits. This gives you plenty of scope for large internal count
    // values while ensuring that the whole structure fits inside a machine word
    // on 32-bit and 64-bit machines. It’s important to update these counts
    // together as a single entity in order to avoid race conditions. Keeping
    // the structure within a machine word makes it more likely that the atomic
    // operations can be lock-free on many platforms.
    unsigned internal_count : 30;
    unsigned external_counters : 2;
  };

  struct Node {
    Node() : data(nullptr), counter(NodeCounter()), next(CountedNodePtr()) {
      NodeCounter new_counter;
      new_counter.internal_count = 0;
      // There are only two counters in Node (counter and next), so the initial
      // value of external_counters is 2.
      new_counter.external_counters = 2;
      counter.store(new_counter);
    }

    void ReleaseRef() {
      NodeCounter old_node = counter.load();
      NodeCounter new_counter;

      do {
        new_counter = old_node;
        --new_counter.internal_count;
      } while (!counter.compare_exchange_strong(old_node, new_counter));

      if (!new_counter.internal_count && !new_counter.external_counters) {
        delete this;
      }
    }

    std::atomic<T*> data;
    std::atomic<NodeCounter> counter;
    std::atomic<CountedNodePtr> next;
  };

 private:
  static void IncreaseExternalCount(std::atomic<CountedNodePtr>* atomic_node,
                                    CountedNodePtr* old_node) {
    CountedNodePtr new_node;

    // If `*old_node` is equal to `*atomic_node`, it means that no other thread
    // changes the `*atomic_node`, update `*atomic_node` to `new_node`. In fact
    // the `*atomic_node` is still the original node, only the `external_count`
    // of it is increased by 1. If `*old_node` is not equal to `*atomic_node`,
    // it means that another thread has changed `*atomic_node`, update
    // `*old_node` to `*atomic_node`, and keep looping until there are no
    // threads changing `*atomic_node`.
    do {
      new_node = *old_node;
      ++new_node.external_count;
    } while (!atomic_node->compare_exchange_strong(*old_node, new_node));

    old_node->external_count = new_node.external_count;
  }

  static void FreeExternalCounter(CountedNodePtr* old_node) {
    Node* ptr = old_node->ptr;
    const int increased_count = old_node->external_count - 2;
    NodeCounter old_counter = ptr->counter.load();
    NodeCounter new_counter;
    do {
      new_counter = old_counter;
      --new_counter.external_counters;
      new_counter.internal_count += increased_count;
    } while (!ptr->counter.compare_exchange_strong(old_counter, new_counter));

    if (!new_counter.internal_count && !new_counter.external_counters) {
      delete ptr;
    }
  }

  void SetNewTail(const CountedNodePtr& new_tail, CountedNodePtr* old_tail) {
    Node* current_tail_ptr = old_tail->ptr;
    while (!tail_.compare_exchange_weak(*old_tail, new_tail) &&
           old_tail->ptr == current_tail_ptr) {
      // Do nothing
    }

    if (old_tail->ptr == current_tail_ptr) {
      FreeExternalCounter(old_tail);
    } else {
      current_tail_ptr->ReleaseRef();
    }
  }

 private:
  std::atomic<CountedNodePtr> head_;
  std::atomic<CountedNodePtr> tail_;
};

三、测试代码

下面给出测试无锁栈工作是否正常的简单测试代码(文件命名为:lock_free_queue.cpp):

#include "lock_free_queue.h"

#include <iostream>
#include <thread>
#include <vector>

namespace {
constexpr size_t kElementNum = 10;
constexpr size_t kThreadNum = 200;
}  // namespace

int main() {
  LockFreeQueue<int> queue;
  for (size_t i = 0; i < kElementNum; ++i) {
    std::cout << "The data " << i << " is pushed in the queue.\n";
    queue.Push(i);
  }

  std::cout << "queue.IsEmpty() == " << std::boolalpha << queue.IsEmpty()
            << std::endl;

  while (auto data = queue.Pop()) {
    std::cout << "Current data is : " << *data << '\n';
  }

  std::vector<std::thread> producers1;
  std::vector<std::thread> producers2;
  std::vector<std::thread> consumers1;
  std::vector<std::thread> consumers2;

  for (size_t i = 0; i < kThreadNum; ++i) {
    producers1.emplace_back(&LockFreeQueue<int>::Push, &queue, i * 10);
    producers2.emplace_back(&LockFreeQueue<int>::Push, &queue, i * 20);
    consumers1.emplace_back(&LockFreeQueue<int>::Pop, &queue);
    consumers2.emplace_back(&LockFreeQueue<int>::Pop, &queue);
  }

  for (size_t i = 0; i < kThreadNum; ++i) {
    producers1[i].join();
    consumers1[i].join();
    producers2[i].join();
    consumers2[i].join();
  }

  return 0;
}

CMake的编译配置文件CMakeLists.txt

cmake_minimum_required(VERSION 3.0.0)
project(lock_free_queue VERSION 0.1.0)
set(CMAKE_CXX_STANDARD 17)

# If the debug option is not given, the program will not have debugging information.
SET(CMAKE_BUILD_TYPE "Debug")

add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp)

find_package(Threads REQUIRED)
# libatomic should be linked to the program.
# Otherwise, the following link errors occured:
# /usr/include/c++/9/atomic:254: undefined reference to `__atomic_load_16'
# /usr/include/c++/9/atomic:292: undefined reference to `__atomic_compare_exchange_16'
target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT} atomic)

include(CTest)
enable_testing()
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)

上述配置中添加了对原子库atomic的链接。因为引用计数的结构体CountedNodePtr包含两个数据成员:int external_count; Node* ptr;,这两个变量占用16字节,而16字节的数据结构需要额外链接原子库atomic,否则会出现链接错误:

/usr/include/c++/9/atomic:254: undefined reference to `__atomic_load_16'
/usr/include/c++/9/atomic:292: undefined reference to `__atomic_compare_exchange_16'

VSCode调试启动配置文件.vscode/launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "cpp_gdb_launch",
            "type": "cppdbg",
            "request": "launch",
            "program": "${workspaceFolder}/build/${workspaceFolderBasename}",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable neat printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                }
            ],
            // "preLaunchTask": "cpp_build_task",
            "miDebuggerPath": "/usr/bin/gdb"
        }
    ]
}

使用CMake的编译命令:

cd lock_free_queue
# 只执行一次
mkdir build
cd build
cmake .. && make

运行结果如下:

./lock_free_queue 
The data 0 is pushed in the queue.
The data 1 is pushed in the queue.
The data 2 is pushed in the queue.
The data 3 is pushed in the queue.
The data 4 is pushed in the queue.
The data 5 is pushed in the queue.
The data 6 is pushed in the queue.
The data 7 is pushed in the queue.
The data 8 is pushed in the queue.
The data 9 is pushed in the queue.
queue.IsEmpty() == false
Current data is : 0
Current data is : 1
Current data is : 2
Current data is : 3
Current data is : 4
Current data is : 5
Current data is : 6
Current data is : 7
Current data is : 8
Current data is : 9

VSCode调试界面如下:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/759508.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(中等)LeetCode 3. 无重复字符到的最长子串 Java

滑动窗口 以示例一为例&#xff0c;找出从每一个字符开始的&#xff0c;不包含重复字符的最长子串&#xff0c;那么&#xff0c;其中最长的那个字符串即为答案。 当我们一次递增地枚举子串的起始位置&#xff0c;会发现子串的结束位置也是递增的&#xff0c;原因在于&#xf…

Django项目创建

Django项目创建 文章目录 Django项目创建&#x1f468;‍&#x1f3eb;方式一&#xff1a;终端命令行方式&#x1f468;‍&#x1f52c;方式二&#xff1a;Pycharm创建 &#x1f468;‍&#x1f3eb;方式一&#xff1a;终端命令行方式 1️⃣cmd打开终端&#xff0c;切换到指定目…

WebSell管理工具--中国蚁剑安装教程以及初始化

简介&#xff1a;中国蚁剑是一款开源的跨平台WebShell网站管理工具 蚁剑的下载安装&#xff1a; GitHub项目地址&#xff1a;https://github.com/AntSwordProject/ Windows下载安装&#xff1a; 百度网盘下载链接&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1A5wK…

超细整理,性能测试-性能指标监控命令详细实战,一篇速通

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能监控命令&…

自动驾驶代客泊车AVP摄像头与ECU交互需求规范

目录 1 文档范围及控制方法... 5 1.1 目的.... 5 1.2 文档授权... 5 1.3 文档变更管理... 5 1.4 缩写.... 5 1.5 术语.... 5 2 系统组成... 6 2.1 系统框图... 6 2.2 电源供应和时序要求... 7 2.2.1 摄像头供电控制... 7 2.2.2 摄像头上电时序要求…

论文(3)——使用ChatGPT快速提高科研能力!!如何快速构建代码?怎么提高自己的科研能力?如何提高自己的生产力?

文章目录 引言问题描述问题解决智能开发软件的方法ChatGPT Plus 代码解释器使用ChatGPT插件功能 代码工具Coplit学生优惠免费申请Coplit和pycharm的结合 NewBing的申请 总结参考引用 引言 chatGPT大模型用于问问题和debug&#xff0c;NewBing用于搜索论文&#xff0c;cpolit用…

简述HashMap的扩容机制

注意&#xff1a;本博客需要对HashMap源码有过一定理解&#xff0c;看过源码比较好&#xff0c;仅供互相学习参考 JDK1.7和JDK1.8对比 1.7版本&#xff1a; (1). 首先生成一个新数组(2). 遍历老数组每个位置中的链表元素(3). 取每个元素的key&#xff0c;重新计算每个元素在…

深度学习ai学习方向如何规划,算法竞赛,机器学习,搭建环境等答疑

目录 1了解人工智能的背景知识 2 补充数学或编程知识 3 熟悉机器学习工具库 4 系统的学习人工智能 5 动手去做一些AI应用 1了解人工智能的背景知识 一些虽然存在但是在研究或者工业上不常用的知识&#xff0c;为自己腾出更多的时间来去学习&#xff0c;研究。 人工智能里…

2023.7.16-约数的枚举

功能&#xff1a;输入一个整数&#xff0c;结果打印出这个整数所有的约数。 程序&#xff1a; #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() {int a0, b;printf("请输入一个整数&#xff1a;");scanf("%d",&a);printf(&qu…

迭代器模式:相比直接遍历集合数据,使用迭代器有哪些优势?

今天&#xff0c;我们学习另外一种行为型设计模式&#xff0c;迭代器模式。它用来遍历集合对象。不过&#xff0c;很多编程语言都将迭代器作为一个基础的类库&#xff0c;直接提供出来了。在平时开发中&#xff0c;特别是业务开发&#xff0c;我们直接使用即可&#xff0c;很少…

前端基础:HTML和CSS简介

目录 1、HTML 简介 &#xff08;1&#xff09;在 HTML 中引入外部 CSS &#xff08;2&#xff09;在 HTML 中引入外部 JavaScript 2、CSS 简介 &#xff08;1&#xff09;CSS 的基本语法 &#xff08;2&#xff09;三种使用 CSS 的方法 2.1 - 外部 CSS 的使用 2.2 - 内…

Redis简介与安装

文章目录 前言一、Redis简介1. Redis是什么2. Redis的特点3. 数据库类型4. Redis 应用场景 二、Redis下载与安装1. Redis安装包下载地址2. 在 windows系统安装 Redis3. 在Linux系统安装Redis 总结 前言 为了巩固所学的知识&#xff0c;作者尝试着开始发布一些学习笔记类的博客…

面向对象习题

创建类Calc,定义2个数属性以及一个符号属性,编写4个方法add、minus、multiply、divide,4个方法分别进行2个小数的加、减、乘、除运算.在主函数里面接受用户输入2个运算数、1个运算符,根据该运算符选择应该调用哪一个方法进行运算。 定义10名学生&#xff0c;循环接收10名学员的…

【Kubernetes运维篇】RBAC认证授权详解(二)

文章目录 一、RBAC认证授权策略1、Role角色2、ClusterRole集群角色3、RoleBinding角色绑定和ClusterRoleBinding集群角色绑定 二、通过API接口授权访问K8S资源三、案例&#xff1a;常见授权策略1、常见的角色授权策略案例2、常见的角色绑定案例3、常见的ServiceAccount授权绑定…

WIN10更改代理设置后无法保存的解决办法

每次更改代理之后保存&#xff0c;推出界面再进来发现还是和原来一样 这应该是代理报错失败解决办法如下 winR&#xff0c;regedit&#xff0c;打开注册表编辑器 找到计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings里面的Pr…

一例flash打包的文件夹病毒的分析

今天分析一例样本&#xff0c;该样本使用flask编写&#xff0c;使用MDM Zinc3打包成exe&#xff0c;使用文件夹图标&#xff0c;会在系统中除了C盘外所有驱动器根目录创建photo目录&#xff0c;将自身拷贝进去&#xff0c;诱导用户点击&#xff0c;会添加开机启动项&#xff0c…

03插值与拟合

9.已知飞机下轮廓线上数据如下&#xff0c;分别用分段线性插值和三次样条插值求x每改变0.1时的y值。 x035791112131415y01.21.72.02.12.01.81.21.01.6 %9.已知飞机下轮廓线上数据如下&#xff0c;分别用分段线性插值和三次样条插值求每改变0.1时的y值。x [0 3 5 7 9 11 12 1…

浮点数的存储

❤️ 作者简介 &#xff1a;对纯音乐情有独钟的阿甘 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识&#xff0c;对纯音乐有独特的喜爱 &#x1f4d7; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;如果你也感兴趣的话欢迎关注博主&#xff0c;期待更新 1. 浮点型…

伙伴来做客,文心千帆大模型平台实操秘籍大公开

7月15日&#xff0c;业界首个大模型实训营——百度智能云文心千帆大模型平台实训营在百度大厦举办。百度智能云携手软通动力、中科软、科蓝、中软国际、天源迪科、世纪互联、宝兰德等14家合作伙伴的25位CTO和技术总监&#xff0c;为伙伴在实际落地中更好地应用大模型技术提供支…

【图像处理】Python判断一张图像是否亮度过低,图片模糊判定

文章目录 亮度判断模糊判断 亮度判断 比如&#xff1a; 直方图&#xff1a; 代码&#xff1a; 这段代码是一个用于判断图像亮度是否过暗的函数is_dark&#xff0c;并对输入的图像进行可视化直方图展示。 首先&#xff0c;通过import语句导入了cv2和matplotlib.pyplot模块…