【Linux】线程池 | 自旋锁 | 读写锁
文章目录
- 一、线程池
- 1. 线程池模型和应用场景
- 2. 单例模式实现线程池(懒汉模式)
- 二、其他常见的锁
- 1. STL、智能指针和线程安全
- 2. 其他常见的锁
- 三、读者写者问题
- 1. 读者写者模型
- 2. 读写锁
一、线程池
1. 线程池模型和应用场景
线程池是一种线程使用模式
。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
💕 线程池模型
线程池模型本质上也是生产者消费者模型,线程池的实现原理是:在线程池中预先准备好并创建一批线程,然后上层将任务push到任务队列中,休眠的线程如果检测到任务队列中有任务,就直接被操作系统唤醒,然后去消费并处理任务,唤醒一个线程的代价比创建一个线程的代价小的很多。
任务线程指的是生产者,任务队列指的是交易场所,右边的一大批线程指的是消费者,因此。线程池的本质还是生产消费模型。
💕 线程池的应用场景
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。
2. 单例模式实现线程池(懒汉模式)
💕 ThreadPool.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <unistd.h>
#include "Thread.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"
using namespace std;const static int N = 5;// 将此代码设计成单例模式————懒汉模式template <class T>
class ThreadPool
{
private:ThreadPool(int num = N) : _num(num){pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T>& tp) = delete;void operator=(const ThreadPool<T>& tp) = delete;
public:// 设计一个静态成员函数来返回创建的对象static ThreadPool<T>* getinstance(){if(_instance == nullptr){LockGuard lockguard(&_instance_lock);{if(_instance == nullptr){_instance = new ThreadPool<T>();_instance->init();_instance->start();}}}return _instance;}pthread_mutex_t *getlock(){return &_lock;}void threadWait(){pthread_cond_wait(&_cond, &_lock);}void threadWake(){pthread_cond_signal(&_cond);}bool isEmpty(){return _tasks.empty();}void init(){for (int i = 0; i < _num; i++){_threads.push_back(Thread(i + 1, threadRoutine, this));}}void start(){for (auto &t : _threads){t.run();}}void check(){for (auto &t : _threads)cout << t.threadname() << " running..." << endl;}static void threadRoutine(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);while (true){T t;// 检测此时有没有任务, 如果有任务就处理任务, 否则就挂起等待{LockGuard lockguard(tp->getlock());while (tp->isEmpty()){tp->threadWait();}t = tp->popTask();}t();cout << "thread handler done, result: " << t.formatRes() << endl;}}T popTask(){T t = _tasks.front();_tasks.pop();return t;}void pushTask(const T &t){LockGuard lockguard(&_lock);_tasks.push(t);threadWake();}~ThreadPool(){for (auto &t : _threads){t.join();}pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}private:vector<Thread> _threads;int _num;queue<T> _tasks; // 使用stl的自动扩容机制pthread_mutex_t _lock;pthread_cond_t _cond;static ThreadPool<T>* _instance;static pthread_mutex_t _instance_lock;
};template<class T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;template<class T>
pthread_mutex_t ThreadPool<T>::_instance_lock = PTHREAD_MUTEX_INITIALIZER;
💕 Thread.hpp
#pragma once#include <iostream>
#include <cstdlib>
#include <string>
#include <pthread.h>
using namespace std;class Thread
{
public:typedef enum{NEW = 0,RUNNING,EXITED} ThreadStatus;typedef void (*func_t)(void*);public:Thread(int num, func_t func, void* args) :_tid(0), _status(NEW),_func(func),_args(args){char name[128];snprintf(name, 128, "thread-%d", num);_name = name;}int status(){ return _status; }string threadname(){ return _name; }pthread_t get_id(){if(_status == RUNNING)return _tid;elsereturn 0;}static void* thread_run(void* args){Thread* ti = static_cast<Thread*>(args);(*ti)();return nullptr;}void operator()(){if(_func != nullptr)_func(_args);}void run() // 封装线程运行{int n = pthread_create(&_tid, nullptr, thread_run, this);if(n != 0)exit(-1);_status = RUNNING; // 线程状态变为运行}void join() // 疯转线程等待{int n = pthread_join(_tid, nullptr);if(n != 0){cout << "main thread join thread: " << _name << "error" << endl;return;}_status = EXITED;}~Thread(){}
private:pthread_t _tid;string _name;func_t _func; // 线程未来要执行的回调void* _args;ThreadStatus _status;
};
💕 Task.hpp
#pragma once
#include <iostream>
#include <string>
using namespace std;class Task
{
public:Task(){}Task(int x, int y, char op):_x(x), _y(y), _op(op), _result(0), _exitcode(0){}void operator()(){switch (_op){case '+':_result = _x + _y; break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':{if(_y == 0)_exitcode = -1;else _result = _x / _y;}break;case '%':{if(_y == 0)_exitcode = -1;else _result = _x % _y;}break;default:break;}}string formatArge(){return to_string(_x) + _op + to_string(_y) + "=";}string formatRes(){return to_string(_result) + "(" + to_string(_exitcode) + ")";}~Task(){}private:int _x;int _y;char _op;int _result;int _exitcode;
};
💕 lockGuard.hpp
#pragma once#include <iostream>
#include <pthread.h>using namespace std;class Mutex // 自己不维护锁,有外部传入
{
public:Mutex(pthread_mutex_t *mutex):_pmutex(mutex){}void lock(){pthread_mutex_lock(_pmutex);}void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}
private:pthread_mutex_t *_pmutex;
};class LockGuard // 自己不维护锁,有外部传入
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}
private:Mutex _mutex;
};
💕 main.cc
#include "ThreadPool_V4.hpp"
#include "Task.hpp"
#include <memory>const string ops = "+-*/%";int main()
{srand(time(nullptr) ^ getpid());while(true){sleep(1);int x = rand() % 100;int y = rand() % 100;char op = ops[(x + y) % ops.size()];Task t(x, y, op);ThreadPool<Task>::getinstance()->pushTask(t);// tp->pushTask(t);cout << "the question is what: " << t.formatArge() << " ? " << endl;}return 0;
}
二、其他常见的锁
1. STL、智能指针和线程安全
💕 STL中的容器是否是线程安全的?
不是
;原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响,而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。
💕 智能指针是线程安全的吗?
智能指针是线程安全的吗?unique_ptr
是和资源强关联,只是在当前代码块范围内生效,因此不涉及线程安全问题。对于 shared_ptr
,多个对象需要共有一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候也考虑到了这个问题,就基于原子操作(Compare And Swap(CAS)) 的方式保证 shared_ptr 能够高效原子地操作引用计数。shared_ptr 是线程安全的,但不意味着对其管理的资源进行操作是线程安全的,所以对 shared_ptr 管理的资源进行操作时也可能需要进行加锁保护。
2. 其他常见的锁
悲观锁
:悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问贡献资源前,先要进行加锁保护。常见的悲观锁有:互斥锁、自旋锁和读写锁等。乐观锁
:乐观锁做事比较乐观,它乐观地认为共享数据不会被其他线程修改,因此不上锁。它的工作方式是:先修改完共享数据,再判断这段时间内有没有发生冲突。如果其他线程没有修改共享数据,那么则操作成功。如果发现其他线程已经修改该共享数据,就放弃本次操作。乐观锁全程并没有加锁,所以它也叫无锁编程。乐观锁主要采取两种方式:版本号机制(Gitee等)和 CAS 操作。乐观锁虽然去除了加锁和解锁的操作,但是一旦发生冲突,重试的成本是很高的,所以只有在冲突概率非常低,且加锁成本非常高的场景下,才考虑使用乐观锁。CAS 操作
:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。自旋锁
:使用自旋锁的时候,当多线程发生竞争锁的情况时,加锁失败的线程会忙等待(这里的忙等待可以用 while 循环等待实现),直到它拿到锁。而互斥锁加锁失败后,线程会让出 CPU 资源给其他线程使用,然后该线程会被阻塞挂起。如果临界区代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和临界区代码执行的时间是成正比的关系。如果临界区代码执行的时间很短,就不应该使用互斥锁,而应该选用自旋锁。因为互斥锁加锁失败,是需要发生上下文切换的,如果临界区执行的时间比较短,那可能上下文切换的时间会比临界区代码执行的时间还要长。
三、读者写者问题
1. 读者写者模型
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢?
这就需要我们的读者写者模型出场了,读者写者模型其实也是维护321原则;三种关系:读者与读者、读者与写者、写者与写者。两种对象:读者和写者。一个交易场所:需要写入和从中读取的缓冲区。
下面我们来看一下读者写者模型的三种关系:
读者与读者
:没有关系读者与写者
:互斥与同步写者与写者
:互斥
那么,为什么在生产者消费者模型中,消费者和消费者是互斥关系,而在读者写者问题中,读者和读者之间没有关系呢?
读者写者模型和生产者消费者模型的最大区别就是:消费者会将数据拿走,而读者不会拿走数据,读者仅仅是对数据做读取,并不会进行任何修改的操作,因此共享资源也不会因为有多个读者来读取而导致数据不一致的问题。
2. 读写锁
在读者写者模型中,pthread库为我们提供了 读写锁
来维护其中的同步与互斥关系。读写锁由读锁
和写锁
两部分构成,如果只读取共享资源用读锁加锁,如果要修改共享资源则用写锁加锁。所以,读写锁适用于能明确区分读操作和写操作的场景。
读写锁的工作原理:
当写锁没有被写线程持有时,多个读线程能够并发地持有读锁,这大大提高了共享资源的访问效率。因为读锁是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。但是,一旦写锁被写进程持有后,读线程获取读锁的操作会被阻塞,而其它写线程的获取写锁的操作也会被阻塞。
伪代码:
// 写者进程/线程执行的函数
void Writer()
{while(true){P(wCountMutex); // 进入临界区if(wCount == 0)P(rMutex); // 当第一个写者进入,如果有读者则阻塞读者wCount++;// 写者计数 + 1V(wCountMutex); // 离开临界区P(wDataMutex); // 写者写操作之间互斥,进入临界区write(); // 写数据V(wDataMutex); // 离开临界区P(wCountMutex); // 进入临界区wCount--; // 写完数据,准备离开if(wCount == 0){V(rMutex); // 最后一个写者离开了,则唤醒读者}V(wCountMutex); //离开临界区}
}// 读者进程/线程执行的次数
void reader()
{while(TRUE){P(rMutex);P(rCountMutex); // 进入临界区if ( rCount == 0 )P(wDataMutex); // 当第一个读者进入,如果有写者则阻塞写者写操作rCount++;V(rCountMutex); // 离开临界区V(rMutex);read( ); // 读数据P(rCountMutex); // 进入临界区rCount--;if ( rCount == 0 )V(wDataMutex); // 当没有读者了,则唤醒阻塞中写者的写操作V(rCountMutex); // 离开临界区}
}
初始化
:
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);
读者写者问题很明显会存在读者优先还是写者优先的问题,如果是读者优先的话,可能就会带来写者饥饿的问题。而写者优先可以保证写线程不会饿死,但如果一直有写线程获取写锁,那么读者也会被饿死。所以使用读写锁时,需要考虑应用场景。读写锁通常用于数据被读取的频率非常高,而被修改的频率非常低。注:Linux 下的读写锁默认是读者优先的。
相关文章:

【Linux】线程池 | 自旋锁 | 读写锁
文章目录 一、线程池1. 线程池模型和应用场景2. 单例模式实现线程池(懒汉模式) 二、其他常见的锁1. STL、智能指针和线程安全2. 其他常见的锁 三、读者写者问题1. 读者写者模型2. 读写锁 一、线程池 1. 线程池模型和应用场景 线程池是一种线程使用模式。线程过多会带来调度开…...

[网鼎杯 2020 青龙组]bang 题解
写一道安卓题的WP 首先你需要一个root机,使用真机或者虚拟机,根据网上的教程刷机并获取root 我使用真机调试,pixel2 讲安卓包下载到真机 在PC端配置frida 对应版本的server传送到/data/local/tmp 然后进行以上操作,启动server …...
创建环境时提示:ERROR conda.core.link:_execute(502)
创建环境时提示:ERROR conda.core.link:_execute(502) 创建环境最后Executing transaction,失败,提示如下: Preparing transaction: done Verifying transaction: done Executing transaction: failed ERROR conda.core.link:_e…...
Python150题day07
1.5集合练习题 集合间的运算 lst1 [1, 2, 3, 5, 6, 3, 2] lst2 [2, 5, 7, 9] 哪些整数既在Ist1中,也在Ist2中哪些整数在Ist1中,不在Ist2中两个列表一共有哪些整数 虽然题目问的是两个列表之间的问题,但是用列表解答的效率很低,…...

LeetCode 2596. 检查骑士巡视方案
【LetMeFly】2596.检查骑士巡视方案 力扣题目链接:https://leetcode.cn/problems/check-knight-tour-configuration/ 骑士在一张 n x n 的棋盘上巡视。在有效的巡视方案中,骑士会从棋盘的 左上角 出发,并且访问棋盘上的每个格子 恰好一次 。…...
大数据学习1.0-目录
学习内容持续更新ing 1.大数据学习1.1-Centos8虚拟机安装 大数据学习1.0-Centos8虚拟机安装_汉卿HanQ的博客-CSDN博客 2.大数据学习1.2-yum配置 大数据学习1.2-yum配置_汉卿HanQ的博客-CSDN博客 3.大数据学习1.3-xShell配置jdk 大数据学习1.3-xShell配置jdk_汉卿HanQ的博客…...

无涯教程-JavaScript - POWER函数
描述 POWER函数返回加到幂的数字的输出。 语法 POWER (number, power)争论 Argument描述Required/OptionalNumber 基数。 它可以是任何实数。 RequiredPowerThe exponent to which the base number is raised.Required Notes 可以使用" ^"运算符代替POWER来指示…...
ChatGPT:解释Java中 ‘HttpResponse‘ 使用 ‘try-with-resources‘ 的警告和处理 ‘Throwable‘ 打印警告
ChatGPT:解释Java中 ‘HttpResponse’ 使用 ‘try-with-resources’ 的警告和处理 ‘Throwable’ 打印警告 我在IDEA中对一个函数的警告点击了ignore,怎么撤回这个呢 ChatGPT: 要撤回在IDEA中对一个函数的警告的忽略,您可以按照以…...

Linux编辑器-gcc的使用
一:背景知识 1.预处理(头文件展开、去注释、宏替换、条件编译) 2.编译(由C生成汇编) 3.汇编(生成及其可识别代码) 4.连接(生成可执行文件或库文件) 二:gcc…...

第16篇ESP32 platformio_arduino框架 wifi联网_连接WiFi热点并连接tcp server收发数据进行通讯
第1篇:Arduino与ESP32开发板的安装方法 第2篇:ESP32 helloword第一个程序示范点亮板载LED 第3篇:vscode搭建esp32 arduino开发环境 第4篇:vscodeplatformio搭建esp32 arduino开发环境 第5篇:doit_esp32_devkit_v1使用pmw呼吸灯实验 第6篇:ESP32连接无源喇叭播…...

day1| 704. 二分查找、27. 移除元素
704. 二分查找 题目链接:https://leetcode.cn/problems/binary-search/ 文档讲解:https://programmercarl.com/0704.%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE.html 视频讲解:https://www.bilibili.com/video/BV1fA4y1o715 1、二分法的前提 这道…...

R绘制箱线图
代码大部分来自boxplot()函数的帮助文件,可以通过阅读帮助文件,调整代码中相应参数看下效果,进而可以理解相应的作用,帮助快速掌握barplot()函数的用法。 语法 Usage(来自帮助文件) barplot(height, ...)## Default S3 method: …...

利用Audit审计系统行为
标题利用Audit审计系统行为 Linux Audit守护进程是一个可以审计Linux系统事件的框架 这个框架本身有数个组件,包括内核、二进制文件及其他文件。 1.内核audit:钩在内核中来捕获事件并将它们发送到auditd。 2.二进制文件 auditd:捕捉事件并…...

uniapp:不同权限设置不同的tabBar
1、在pages.json里,将所有tabBar涉及的页面都加进来。 我这里使用username来动态显示tabBar。 jeecg用户显示:首页,订单,消息,发现,我的,一共5个tabBar。 admin用户显示:首页&…...

如何将本地的项目上传到Git
一、GitHub or GitLab or Gitee创建一个新的仓库 二、仓库路径创建成功后,将本地项目上传到git 1. 进入本地项目所在文件夹位置,右击 2.出现git命令框 输入git init 在当前项目的目录中生成本地的git管理(会发现在当前目录下多了一个.git文件…...

[php] 文件上传的一个项目emmm
项目完整地址 <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><title>上传文件</title><link href"./css/bootstrap.min.css" rel"stylesheet"><style>font-face {fo…...
uniapp-时间格式和距离格式的转换
时间格式的转换 第一种是把 YYYY-MM-DD hh:mm:ss 转换成 MM月DD日 第二种是把 hh:mm:ss 转换成 hh:mm /*** 格式化时间 1* 把传入的完整时间分为 MM月DD日 的格式* returns*/ export function formatDate(timeStr) {const date new Date(timeStr);const month (date.ge…...

【卖出备兑看涨期权策略(Covered_call)】
卖出备兑看涨期权策略(Covered_call) 卖出备兑看涨期权策略是一种最基本的收入策略,该策略主要操作就是在持有标的资产的同时卖出对应的看涨期权合约,以此来作为从持有的标的资产中获取租金的一种方法。如果标的资产的价格上涨到…...

【校招VIP】测试算法考点之智力分析
考点介绍: 智力题(逻辑分析题)准备校招的同学们好好准备下,测试笔试中经常遇到。 测试算法考点之智力分析-相关题目及解析内容可点击文章末尾链接查看! 一、考点试题 1.5个囚犯在装有100颗豆子的袋子里摸,他们谁的存活几率大? 5个囚犯,分…...

【Linux 服务器运维】定时任务 crontab 详解 | 文末送书
文章目录 前言一、crontab 介绍1.1 什么是 crontab1.2 crontab 命令工作流程1.3 Linux 定时任务分类 二、crontab 用法详解2.1 crond 服务安装2.2 crontab 文件内容分析2.3 crontab 命令用法2.3.1 查看定时任务列表2.3.2 编辑/创建定时任务2.3.3 删除定时任务2.3.4 其他 cronta…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

LeetCode - 394. 字符串解码
题目 394. 字符串解码 - 力扣(LeetCode) 思路 使用两个栈:一个存储重复次数,一个存储字符串 遍历输入字符串: 数字处理:遇到数字时,累积计算重复次数左括号处理:保存当前状态&a…...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...

MyBatis中关于缓存的理解
MyBatis缓存 MyBatis系统当中默认定义两级缓存:一级缓存、二级缓存 默认情况下,只有一级缓存开启(sqlSession级别的缓存)二级缓存需要手动开启配置,需要局域namespace级别的缓存 一级缓存(本地缓存&#…...
如何配置一个sql server使得其它用户可以通过excel odbc获取数据
要让其他用户通过 Excel 使用 ODBC 连接到 SQL Server 获取数据,你需要完成以下配置步骤: ✅ 一、在 SQL Server 端配置(服务器设置) 1. 启用 TCP/IP 协议 打开 “SQL Server 配置管理器”。导航到:SQL Server 网络配…...

Qt的学习(一)
1.什么是Qt Qt特指用来进行桌面应用开发(电脑上写的程序)涉及到的一套技术Qt无法开发网页前端,也不能开发移动应用。 客户端开发的重要任务:编写和用户交互的界面。一般来说和用户交互的界面,有两种典型风格&…...