【系统编程】线程池以及API接口简介

  • (꒪ꇴ꒪ ),Hello我是祐言QAQ
  • 我的博客主页:C/C++语言,数据结构,Linux基础,ARM开发板,网络编程等领域UP🌍
  • 快上🚘,一起学习,让我们成为一个强大的攻城狮!
  • 送给自己和读者的一句鸡汤🤔:集中起来的意志可以击穿顽石!
  • 作者水平很有限,如果发现错误,请在评论区指正,感谢🙏


        线程池(Thread Pool)是一种并发编程的设计模式,它用于管理和复用线程,以便更有效地处理并发任务。线程池的主要目标是降低线程的创建和销毁成本,提高系统的性能和资源利用率

一、API接口

        API(Application Programming Interface)接口是一组定义了软件组件之间如何互相通信和交互的规范和协议。API允许不同的软件模块、应用程序或系统之间共享功能和数据,从而实现各种复杂的任务和功能。

        我们在学习线程池之前要明白,线程池本身不是一个API接口,而是一种用于管理和执行任务的并发编程模型。然而,线程池通常会被包装在API接口中,以便其他开发人员可以更容易地使用它来执行并发任务。

        API接口通常定义了一组可用于与软件组件、服务或库进行交互的方法和函数。在这种情况下,如果创建了一个包含线程池的库,并提供了一组方法或函数来操作和管理线程池,那么这些方法或函数可以被视为API接口的一部分。

        例如,可以设计一个具有以下功能的线程池API接口:

init_pool():初始化线程池
add_task():向线程池添加任务
add_thread():添加新的工作线程
remove_thread():从线程池中删除工作线程
destroy_pool():销毁线程池

      
        这些方法将构成线程池API接口的一部分,其他开发人员可以使用这些方法来实现并发任务执行,而无需了解线程池的内部工作原理。

        所以,线程池可以成为一个可用于构建API接口的组件,以简化并发编程任务的处理,这就是我们了解AIP接口的目的,当然知道这些还不足以写出一个线程池应用程序,我们还需先了解一下有关线程池的结构体。

二、线程池相关的结构体

        线程池相关的结构体在线程池的设计中扮演着重要角色,用于管理线程池的状态、任务队列等信息。

1.线程池结构体(Thread Pool)

        线程池结构体通常包含了线程池的各种属性和信息,用于管理线程池的整体状态。以下是一般线程池结构体的示例:

typedef struct ThreadPool {pthread_mutex_t lock;           // 互斥锁,保护线程池内部数据pthread_cond_t cond;            // 条件变量,用于线程之间的同步bool shutdown;                  // 线程池销毁标志struct Task *task_list;         // 任务链队列pthread_t *tids;                // 存储线程ID的数组unsigned int waiting_tasks;     // 等待执行的任务数量unsigned int active_threads;    // 活跃线程数量
} ThreadPool;
  • lockcond是用于线程同步的互斥锁和条件变量;
  • shutdown标志用于指示线程池是否正在销毁;
  • task_list是一个任务链队列,存储待执行的任务;
  • tids是存储线程ID的数组;
  • waiting_tasks记录等待执行的任务数量;
  • active_threads表示当前活跃线程数量。

2.任务结构体(Task)

        任务结构体表示线程池中的任务,包含了任务函数指针和参数。以下是一个示例:

//任务结构体
struct task
{void *(*task)(void *arg);	//返回值为void *的函数指针,参数列表void *arg,表示任务的地址void *arg;					//表示任务需要的参数struct task *next;			//表示下一个任务的地址
};
  • task 是指向任务函数的指针,任务函数接受一个void *参数;
  • arg 是传递给任务函数的参数;
  • next 是一个指向下一个任务的指针,用于构建任务队列。


        这些结构体协同工作,帮助线程池管理任务的执行和线程的管理。线程池结构体用于维护线程池的状态,任务结构体用于表示具体的任务。线程池中的线程会不断从任务队列中取出任务并执行,同时线程池负责管理线程的生命周期。

三、线程池

        线程池(Thread Pool)是一种并发编程的设计模式,它用于管理和复用线程,以便更有效地处理并发任务。线程池的主要目标是降低线程的创建和销毁成本,提高系统的性能和资源利用率

1.基本原理


        (1)线程复用: 线程池在启动时创建一组线程,这些线程一直保持活动状态,可用于处理任务。线程复用消除了频繁创建和销毁线程的开销,这也是为什么我们要学习线程池的原因。

        (2)任务队列: 线程池通常包括一个任务队列,用于存储等待执行的任务(条件变量)。当任务到达时,线程池将任务放入队列,并从池中的空闲线程中选择一个来执行任务。

        (3)线程调度: 线程池负责调度任务并分配给空闲线程。一旦任务完成,线程将返回池中等待下一个任务。

        (4)线程池大小控制: 线程池的大小通常是有限的,可以根据系统资源和性能需求进行配置。这有助于避免创建过多线程,从而导致资源耗尽和性能下降,也就是做到合理利用资源。

2.组成部分


        (1)线程池管理器(Thread Pool Manager): 负责创建、管理和监控线程池的核心组件。它维护线程池的状态,包括活动线程数、等待任务数等。

        (2)任务队列(Task Queue): 用于存储等待执行的任务。任务可以是函数、方法或对象,线程池从队列中取出任务并将其分配给可用线程。

        (3)工作线程(Worker Threads): 线程池中的线程,用于执行任务。这些线程在初始化时启动,并在完成任务后返回池中以供重用。

        (4)任务接口(Task Interface): 描述任务的接口或抽象类,通常包括一个run方法或函数,线程池根据此接口来执行任务。

四、C语言实现线程池

1. 初始化线程池

          用于初始化线程池,设置线程池的初始状态

原型:

        bool init_pool(thread_pool * pool, unsigned int threads_number);

参数:
        pool        线程池结构体指针,用于表示要初始化的线程池
        threads_number        指定线程池中的初始线程数量

// 初始化线程池结构体里面的成员,根据传入的线程个数,创建线程
bool init_pool(thread_pool *pool, unsigned int threads_number)
{//初始化互斥锁pthread_mutex_init(&pool->lock, NULL);//初始化条件变量pthread_cond_init(&pool->cond, NULL);// 关闭销毁线程池标识pool->shutdown = false;// 任务队列头结点pool->task_list = malloc(sizeof(struct task)); // 线程ID的指针申请空间pool->tids = malloc(sizeof(pthread_t) * MAX_ACTIVE_THREADS);// 判断任务队列头结点指针跟线程ID的指针是否申请成功if(pool->task_list == NULL || pool->tids == NULL){perror("allocate memory error");return false;}//将任务链式队列的下一个节点的地址初始化pool->task_list->next = NULL;//初始化任务个数0个pool->waiting_tasks = 0;//初始化活跃线程个数为传入的threads_number个pool->active_threads = threads_number;int i;// 循环创建指定数目线程for(i = 0; i<pool->active_threads; i++) {//调用pthread_create函数创建线程,线程ID存放在pool->tids的数组里面if(pthread_create(&((pool->tids)[i]), NULL, routine, (void *)pool) != 0){perror("create threads error");return false;}}return true;
}

2. 添加任务

        将一个任务添加到线程池的任务队列中,等待线程池的线程执行。

原型:

        bool add_task(thread_pool *pool, void *(*do_task)(void *arg), void * arg);

参数:
        pool        线程池结构体指针,表示要添加任务的线程池
        do_task        任务函数的指针,表示要执行的任务
        arg        任务函数需要的参数,可以是任何类型的数据

// 往链式任务队列添加任务,单个唤醒线程去执行任务
bool add_task(thread_pool *pool, void *(*task)(void *arg), void *arg)
{// 新任务节点struct task *new_task = malloc(sizeof(struct task));//新任务节点创建失败if(new_task == NULL){perror("allocate memory error");return false;}//新任务节点的函数指针做初始化new_task->task = task;//新任务节点的函数指针需要的参数做初始化new_task->arg = arg;//新任务节点的下一个节点的地址初始化为NULLnew_task->next = NULL;// 访问任务队列前获取互斥锁,此处无需注册取消处理例程pthread_mutex_lock(&pool->lock);//如果 任务链队列里面任务个数 大于等于 最大任务个数if(pool->waiting_tasks >= MAX_WAITING_TASKS){//解锁pthread_mutex_unlock(&pool->lock);fprintf(stderr, "too many tasks.\n");free(new_task);return false;}//拿链式队列的头节点指针struct task *tmp = pool->task_list;//循环遍历找到最后一个节点的地址while(tmp->next != NULL){tmp = tmp->next;}// 添加新的任务节点tmp->next = new_task;// 等待任务个数+1pool->waiting_tasks++;// 释放互斥锁pthread_mutex_unlock(&pool->lock);// 并唤醒其中一个阻塞在条件变量上的线程pthread_cond_signal(&pool->cond);return true;
}

3. 添加活跃线程

        向线程池中添加额外的活跃线程,增加线程池的处理能力。

原型:

        int add_thread(thread_pool *pool, unsigned int additional_threads);

参数:
        pool        线程池结构体指针,表示要添加线程的线程池        
        additional_threads        指定要添加的额外线程数量        

// 根据传入的数量创建线程
int add_thread(thread_pool *pool, unsigned additional_threads)
{//添加活跃线程个数为0,不需要往后执行了if(additional_threads == 0){return 0;}// 定义一个变量total_threads = 当前活跃线程个数 + 添加活跃线程个数unsigned total_threads = pool->active_threads + additional_threads;int i, actual_increment = 0;// 循环地创建若干指定数目的线程for(i = pool->active_threads; i < total_threads && i < MAX_ACTIVE_THREADS; i++){if(pthread_create(&((pool->tids)[i]), NULL, routine, (void *)pool) != 0){perror("add threads error");//添加活跃线程个数如果一开始就为0,表示一个都没有创建成功if(actual_increment == 0){return -1;}break;}actual_increment++;}//更新活跃线程个数,用创建成功的个数加上之前的个数pool->active_threads += actual_increment;return actual_increment;
}

4. 删除活跃线程

        从线程池中删除指定数量的活跃线程,减少线程池的处理能力。

原型:

        int remove_thread(thread_pool *pool, unsigned int removing_threads);

参数:
        pool        线程池结构体指针,表示要删除线程的线程池        
        removing_threads        指定要删除的线程数量        

// 根据传入的个数去删除,返回删除之后的剩下的活跃线程个数
int remove_thread(thread_pool *pool, unsigned int removing_threads)
{//如果删除的线程个数为0,直接返回当前的活跃线程个数if(removing_threads == 0){return pool->active_threads;}//定义一个变量删除之后剩下的线程个数 = 当前活跃线程个数-删除活跃线程个数int remain_threads = pool->active_threads - removing_threads;//如果删除之后的线程个数小于0,保留一个,如果不小于0,就有多少保留多少remain_threads = remain_threads > 0 ? remain_threads : 1;int i; // 循环地取消掉指定数目的线程for(i = pool->active_threads-1; i>remain_threads-1; i--){errno = pthread_cancel(pool->tids[i]);if(errno != 0){break;}}//如果一个都没有删,返回-1if(i == pool->active_threads-1){return -1;}else{//更新最大活跃线程个数pool->active_threads = i+1;return i+1;}
}

5. 销毁线程池

        销毁线程池,释放线程池占用的资源,并停止线程池的运行。

原型:

        bool destroy_pool(thread_pool *pool);

参数:

  pool        线程池结构体指针,表示要销毁的线程池

// 释放资源
bool destroy_pool(thread_pool *pool)
{//线程池销毁标志为真pool->shutdown = true;//广播唤醒条件变量等待队列里面的线程pthread_cond_broadcast(&pool->cond);int i;for(i=0; i<pool->active_threads; i++){//pthread_join( )指定的线程如果尚在运行,那么他将会阻塞等待errno = pthread_join(pool->tids[i], NULL);if(errno != 0){printf("join tids[%d] error: %s\n", i, strerror(errno));}else{printf("[%u] is joined\n", (unsigned)pool->tids[i]);}	}free(pool->task_list);free(pool->tids);free(pool);return true;
}

6.线程任务函数

        routine( )函数是线程池中线程的主要工作函数,它执行以下任务:

        ①不断地从任务队列中取出任务;
        ②执行取出的任务;
        ③如果任务队列为空且线程池没有被销毁,则进入等待状态,等待新任务的到来。

//创建线程之后,开始去执行任务,有任务就执行,没有任务,线程进入条件变量等待队列等待唤醒执行任务
void *routine(void *arg)
{//接收传入进来的参数poolthread_pool *pool = (thread_pool *)arg;//定义任务结构体指针变量struct task *p;while(1){// 访问任务队列前加锁,为防止取消后死锁,注册处理例程 handlerpthread_cleanup_push(handler, (void *)&pool->lock);//加锁访问任务队列pthread_mutex_lock(&pool->lock);// 若当前没有任务,且线程池关闭标志未关闭,则进入条件变量等待队列睡眠,等待新任务的到来或线程池关闭while(pool->waiting_tasks == 0 && !pool->shutdown){//解锁 进入条件变量等待队列睡眠   收到通知的时候,加锁pthread_cond_wait(&pool->cond, &pool->lock);}// 若当前没有任务,且线程池关闭标识为真,则立即释放互斥锁并退出if(pool->waiting_tasks == 0 && pool->shutdown == true){//解锁pthread_mutex_unlock(&pool->lock);//线程退出pthread_exit(NULL);}// 若当前有任务,则消费任务队列中的任务// 拿链式队列的下一个节点的地址p = pool->task_list->next;//将链式队列当前指向,指向下一个pool->task_list->next = p->next;//任务个数-1pool->waiting_tasks--;// 释放互斥锁,并弹栈 handler(但不执行他)pthread_mutex_unlock(&pool->lock);pthread_cleanup_pop(0);// 执行任务,并且在此期间禁止响应取消请求,执行期间用pthread_cancel发送取消请求pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);//调用任务结构体里面的函数,传参数(p->task)(p->arg);	// task(arg)//执行任务完成,并且在此期间使能响应取消请求pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);//释放p指向的地址空间free(p);}//线程退出pthread_exit(NULL);
}

7.任务函数

void *mytask(void *arg);

        该函数接受一个 void 指针类型的参数 arg,并返回一个 void 指针。函数主要用于你想要完成的任务操作,这里举例了一个文件复制函数:

//任务函数(想要完成的操作)
void *mytask(void *arg)
{//文件复制int fd1 = open("./1.txt", O_RDWR);//int fd1 = open(arg->first, O_RDWR);if (fd1 == -1){perror("open fd1 error");return NULL;}int fd2 = open("./2.txt", O_RDWR|O_CREAT, 0777);//int fd2 = open(arg->second, O_RDWR|O_CREAT, 0777);if (fd2 == -1){perror("open fd2 error");return NULL;}char buf[1024];int size;while(1){bzero(buf, 1024);size = read(fd1, buf, 1024);if (size==0){break;}write(fd2, buf, size);}close(fd1);close(fd2);return NULL;
}

        这里我们创建了五条线程的一个线程池,来复制一个拥有50个文件大小为45MB的大文件,可以看到用时4.433s,然而如果采用单线程处理的话,至少时间在8s左右。当然具体情况因个人电脑配置不同而异。

五、总结

        让我们用一个类比来解释一下线程池,每辆出租车就是一个线程它的工作是执行各种任务,就像司机可以运送乘客到不同的目的地一样。而线程池就像是一家出租公司这家公司拥有多组出租车(线程),并根据需要分配任务给这些出租车。公司负责管理、维护和监控这些出租车,以确保它们随时可以为乘客提供服务。任务(工作)就像是乘客需要到达的目的地。这可以是任何需要执行的工作,例如计算、数据处理、文件上传等等。线程池接受这些任务,并将它们分配给可用的出租车(线程),从而高效的完成他们。

1.优点

  • 资源控制: 线程池可以限制同时运行的线程数量,有效控制系统资源的使用。
  • 性能提升: 通过减少线程的创建和销毁,线程池可以提高应用程序的性能,减少了线程切换的开销。
  • 任务管理: 线程池可以管理任务的排队和执行,确保任务按照顺序或优先级执行。
  • 可伸缩性: 线程池可以根据系统负载动态调整线程数量,以适应不同的工作负载。

2.适用场景

        线程池适用于需要并发执行多个任务的情况,特别是在以下情况下使用效果更佳:

  • Web服务器处理多个并发请求。
  • 数据库连接池管理多个数据库连接。
  • 后台任务处理,如日志处理、邮件发送、文件处理等。
  • 任何需要并发执行的计算密集型或I/O密集型任务。

3.注意事项

  • 线程池的大小应该根据系统资源和负载需求进行调整,过大的线程池可能会消耗大量内存,而过小的线程池可能会导致任务排队和性能下降。

  • 线程池中的任务应该是独立的,不应该有共享状态或依赖关系,以避免竞态条件和死锁。

  • 异常处理对于线程池非常重要,确保捕获和处理任务中的异常,以避免线程终止和资源泄漏。

        更多C/C++语言Linux系统数据结构ARM板实战相关文章,关注专栏:

   手撕C语言

            玩转linux

                    脚踢数据结构

                            系统、网络编程

                                     探索C++

                                             6818(ARM)开发板实战

📢写在最后

  • 今天的分享就到这啦~
  • 觉得博主写的还不错的烦劳 一键三连喔~
  • 🎉🎉🎉感谢关注🎉🎉🎉

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

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

相关文章

数据结构与算法-插入希尔归并

一&#xff1a;排序引入 我们通常从哪几个方面来分析一个排序算法&#xff1f; 1.时间效率&#xff1a;决定了算法运行多久&#xff0c;O&#xff08;1&#xff09; 2.空间复杂度&#xff1a; 3.比较次数&交换次数:排序肯定会牵涉到两个操作&#xff0c;一个比较是肯定的。…

《存储IO路径》专题:块设备层多队列blk-mq架构

我们想象一下&#xff0c;你是一个餐厅的厨师&#xff0c;你要准备很多不同的菜肴&#xff0c;而每种菜肴需要不同的食材和烹饪时间。如果每道菜都按照需要的顺序来准备&#xff0c;那么你的工作效率一定会非常低。为了提高效率&#xff0c;你会怎么做呢&#xff1f; 在linux架…

uni-app 之 v-on:click点击事件

uni-app 之 v-on:click点击事件 image.png <template><!-- vue2的<template>里必须要有一个盒子&#xff0c;不能有两个&#xff0c;这里的盒子就是 view--><view>--- v-on:click点击事件 ---<view v-on:click"onclick">{{title}}<…

Git 回顾小结

Git是一个免费开源&#xff0c;分布式的代码版本控制系统&#xff0c;版主开发团队维护代码 作用&#xff1a;记录代码内容&#xff0c;切换代码版本&#xff0c;多人开发时高校合并代码内容 Git常用命令 命令作用注意git -v查看Git版本git init初始化本地Git仓库git add 文件…

QTday5

一、客户端 二、服务器 三、配置图像处理环境 四、XMind思维导图

vscode新建vue3文件模板

输入快捷新建的名字 enter 确认后在文件中输入以下内容 {// Place your snippets for vue here. Each snippet is defined under a snippet name and has a prefix, body and// description. The prefix is what is used to trigger the snippet and the body will be expand…

无需租用云服务器:使用Linux本地搭建web服务并实现内网穿透发布公网访问的详细教程

文章目录 前言1. 本地搭建web站点2. 测试局域网访问3. 公开本地web网站3.1 安装cpolar内网穿透3.2 创建http隧道&#xff0c;指向本地80端口3.3 配置后台服务 4. 配置固定二级子域名5. 测试使用固定二级子域名访问本地web站点 前言 在web项目中,部署的web站点需要被外部访问,则…

前端自动化部署,Devops,CI/CD

DevOps 提到 Jenkins&#xff0c;想到的第一个概念就是 CI/CD 在这之前应该再了解一个概念。 DevOps Development 和 Operations 的组合&#xff0c;是一种方法论&#xff0c;并不特指某种技术或者工具。DevOps 是一种重视 Dev 开发人员和 Ops 运维人员之间沟通、协作的流程。…

Java on VS Code 8月更新|反编译器用户体验优化、新 Maven 项目工作流、代码高亮稳定性提升

作者&#xff1a;Nick Zhu 排版&#xff1a;Alan Wang 大家好&#xff0c;欢迎来到 Visual Studio Code for Java 的 8 月更新&#xff01;在这篇博客中&#xff0c;我们将为您提供有关反编译器支持的更多改进。此外&#xff0c;我们将展示如何创建没有原型的 Maven 项目以及一…

【C语言】字符函数,字符串函数,内存函数

大家好&#xff01;今天我们来学习C语言中的字符函数&#xff0c;字符串函数和内存函数。 目录 1. 字符函数 1.1 字符分类函数 1.2 字符转换函数 1.2.1 tolower&#xff08;将大写字母转化为小写字母&#xff09; 1.2.2 toupper&#xff08;将小写字母转化为大写字母&…

SpringMVC之入门

目录 1.SpringMVC工作流程 2.SpringMVC核心组件 2.1 DispatcherServlet 2.2 HandlerMapping 2.3 Handler 2.4 HandlerAdapter 2.5 ViewResolver 2.6 View 3.SpringMVC的入门 3.1 添加相关依赖 3.2 创建Spring-mvc.xml 3.3 配置web.xml 3.4 效果演示 4.静态资源处…

ios 运行ipa包 日志查看方式

方法一&#xff1a; 使用ideviceinstaller工具 # 安装ipa命令 brew install ideviceinstaller ideviceinstaller -i xxx.ipa# 查看运行日志 idevicesyslog# idevicesyslog 查找命令 idevicesyslog | grep test -A 3 -B 2 # 输出关键字所在行后3行&#xff0c;前2行) idevic…

GeoServe Web管理界面远程访问GeoServe Web管理界面的最佳工具

文章目录 前言1.安装GeoServer2. windows 安装 cpolar3. 创建公网访问地址4. 公网访问Geo Servcer服务5. 固定公网HTTP地址 前言 GeoServer是OGC Web服务器规范的J2EE实现&#xff0c;利用GeoServer可以方便地发布地图数据&#xff0c;允许用户对要素数据进行更新、删除、插入…

数据库-多表查询

概述&#xff1a; 介绍&#xff1a;多表查询&#xff1a;指从多张表中查询数据 笛卡儿积&#xff1a;笛卡儿积是指在数学中&#xff0c;两个集合&#xff08;A集合和B集合&#xff09;的所有组合情况&#xff08;在多表查询时&#xff0c;需要消除无效的笛卡儿积&#xff09; 分…

excel中的引用与查找函数篇1

1、COLUMN(reference)&#xff1a;返回与列号对应的数字 2、ROW(reference)&#xff1a;返回与行号对应的数字 参数reference表示引用/参考单元格&#xff0c;输入后引用单元格后colimn()和row()会返回这个单元格对应的列号和行号。若参数reference没有引用单元格&#xff0c;…

传输层—TCP原理详解

目录 前言 1.TCP协议 2.TCP协议段格式 3.如何解包如何分用 4.网络协议栈和文件的关系 5.如何理解TCP报头 6.TCP的特点 7.TCP字段 7.1 16位窗口大小 7.2标志位 8.超时重传 9.连接管理机制 10.滑动窗口 11.拥塞控制 12.延迟应答 13.捎带应答 14.理解TCP的面向字…

【C++】Visual Studio EditorConfig 格式设置

【C】Visual Studio EditorConfig 格式设置 文章目录 【C】Visual Studio EditorConfig 格式设置I - EditorConfig1.1 - 通用设置indent_styleindent_sizetab_widthend_of_linecharsettrim_trailing_whitespaceinsert_final_newline II - Visual Studio 特定键值缩进设置cpp_in…

蚂蚁集团SQLess 开源,与内部版有何区别?

当我们使用关系型数据库时&#xff0c;SQL 是联系起用户和数据库的一座桥梁。 SQL 是一种高度非过程化的语言&#xff0c;当我们在编写SQL 时&#xff0c;表达的是想要什么数据&#xff0c;而不是怎么获取数据。因此&#xff0c;我们往往更关心SQL 有没有满足业务逻辑&#xff…

朴素,word,任何参考文献导入endnote

朴素&#xff0c;word&#xff0c;任何参考文献导入endnote 注意&#xff1a;对于以下这几种不做阐述&#xff0c;看其他帖子都有讲述&#xff1a; 这里的参考文献指的是类似于&#xff1a; [1]. Li Y, Lu Y, Huo X, et al. Bandgap tuning strategy by cations and halide io…