【C++中线程学习】

1、多线程

C++11之前没有引入线程的概念,如果想要实现多线程,需要借助操作系统平台提供的API,比如Linux的<pthead.h>,或者windows下的<windows.h>。
C++11提供了语言层面上的多线程,包含在头文件<thread.h>中,解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。C++11新标准中引入了5个头文件来支持多线程编程。
在这里插入图片描述

1.1、多进程与多线程

- 多进程并发
使用多进程并发是将一个应用程序划分为多个独立的进程(其中每个进程只有一个线程),这些独立的进程间可以相互通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码,但这也就造成了多进程并发的两个缺点:

  1. 在进程间的通信,无论是用信号、套接字、还是文件、管道等方式,其使用要么是比较复杂,要么就是速度较慢,或者干脆就是又复杂又慢;
  2. 运行多个进程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。

由于多个进程并发完成同一任务时,不可避免的要操作同一数据和进程间的相互通信,上述的两个缺点也就决定了多进程不是一个好的选择。

- 多线程并发
多线程并发指的是在同一个进程中执行多个进程。

☆优点:
线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,统一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。

☆缺点:
由于缺少操作系统提供的保护机制,在多线程共享数据以及通信时,程序员就需要做出措施来保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避开死锁(deadlock)。

1.2、并发(concurrency)和并行(parallel)

- 并发
一个时间片运行一个线程的代码,宏观上是同时,但其实不是
在这里插入图片描述
- 并行
宏观与微观上都是同时运行
在这里插入图片描述

1.3、创建线程

创建线程:将函数添加进线程当中即可。

- 形式1:thread 线程名(函数名);

#include<iostream>
#include<thread>
using namespace std;
void thread_fun1(){cout<<"子线程Mythread1正在运行"<<endl;
}
int main(){//创建线程Mythread1thread Mythread1 (thread_fun1);//加入线程Mythread1.join();cout<<"主线程正在运行"<<endl;
}

- 形式2:thread 线程名(函数名(参数));

void thread_fun2(int x){cout<<x<<endl;
}//...thread Mythread2 (thread_fun2(100));Mythread2.join();
//...

- 形式3:thread (函数,参数).join();

void thrad_fun3(int x){cout<<x<<endl;
}
//...thread (thread_fun3,1).join();
//...

- 形式4:利用类的仿函数作为线程处理函数

class A{
public:void operator()(){cout<<"子线程"<<endl;}
}
int main(){//类的实例化对象充当线程函数A a;thread Mythread4(a);Mythread4.join();//或者这样写//thread Mythread((A()));//Mythrread.join();cout<<"主线程"<<endl;
}

- 形式5:通过Lambda表达式创建线程
简单来讲,就是把函数得定义和调用放在一处实现。

//...thread Mythread5([]{cout<<"子线程调用"<<endl;});Mythread5.join();

- 形式6:通过智能指针的方式创建线程
即以智能指针为参数的函数作为线程的处理函数

void thread_fun3(unique_ptr<int>ptr){cout<<"子线程:"<<ptr.get()<<endl;cout<<"子线程id:"<<this_thread::get_id()<<endl;
}
int main(){//智能指针作为参数的线程处理函数int *p = new int(12);cout<<*p<<endl;unique_ptr<int> ptr(new int(1000));cout<<"主线程"<<ptr.get()<<endl;//ptr.get()用于获取智能指针的地址thread Mythread6(thread_fun3,move(ptr));Mythread6.join();cout<<"主线程id"<<this_thread::get_id()<<endl;cout<<"主线程:"<<ptr.get()<<endl;return 0;
}

在这里插入图片描述

- 形式7:类的成员函数做线程处理函数

class A {
public:void func(int x) {cout << "子线程id:" <<this_thread::get_id()<< endl;}
};
int main(){A a;thread Mythread7(&A::func,a,1);//注意写法Mythread7.join();cout<<"主线程id:"<<this_thread::get_id()<<endl;return 0;
}

在这里插入图片描述

1.4、join()与detach()方式

当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。

  1. join():等待启动的线程完成,再会继续往下执行;
  2. detach():启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。

注意:thread对象只能被join或detach一次。
可以用joinable()来判断对象是否被join过,已经join过的线程用joinable会返回0。

1.5、this_thread

this_thread是一个类,它有4个功能函数,具体如下:

函数使用说明
get_idthis_thread::get_id()获得线程id
yieldthis_thread::yield()放弃线程执行,回到就绪状态
sleep_forthis_thread::sleep_for(x)暂停x秒
sleep_until具体用法如下直到…时间才开始运行
#include<iostream>
#include<thread>
//包含标准时间库
#include<chrono>
//包含时间和日期函数
#include<ctime>
#include<iomanip>
//禁用编译器对localtime的警告4996
#pragma warning(disable:4996)
using namespace std;int main(){using chrono::system_clock;time_t tt = system_clock::to_time_t(system_clock::now());//输出当前时间并转换为time_t类型struct tm *ptm = localtime(&tt);//将time_t类型的时间转换为struct tm类型cout<<"Current time:"<<put_time(ptm,"%X")<<endl;//必须大写X,若小写,输出的为日期cout<<"Waiting for the next minute to begin..."<<endl;++ptm->tm_min;//增加当前分钟数ptm->tm_sec = 0;//将秒数设为0this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));//使当前线程休眠直到指定时间cout<<put_time(ptm,"%X")<<"reached"<<endl;getchar();return 0;
}

在这里插入图片描述

2、mutex

2.1、mutex

mutex头文件主要声明了与互斥量(mutex)相关的类。
互斥量mutex:是线程间通信的一种方式,只有用户互斥对象的线程才能访问公共资源,因为互斥对象只有一个,从而避免了多个线程同时访问公共资源
mutex提供了4种互斥类型,如下所示:

类型说明
mutex最基本的Mutex类
recursive_mutex递归Mutex类
time_mutex定时Mutex类
recursive_timed_mutex定时递归Mutex类

示例,不加锁的情况:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>int counter = 0;
void increase(int time) {for (int i = 0; i < time; i++) {// 当前线程休眠1毫秒std::this_thread::sleep_for(std::chrono::milliseconds(1));counter++;}
}int main(int argc, char** argv) {std::thread t1(increase, 100);std::thread t2(increase, 100);t1.join();t2.join();std::cout << "counter:" << counter << std::endl;return 0;
}

第一次运行的结果为:
在这里插入图片描述
第二次运行的结果为:
在这里插入图片描述
加上锁:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>int counter = 0;
std::mutex mtx;void increase(int time) {for (int i = 0; i < time; i++) {//上锁mtx.lock();// 当前线程休眠1毫秒std::this_thread::sleep_for(std::chrono::milliseconds(1));counter++;//解锁mtx.unlock();}
}int main(int argc, char** argv) {std::thread t1(increase, 100);std::thread t2(increase, 100);t1.join();t2.join();std::cout << "counter:" << counter << std::endl;return 0;
}

第一次运行结果为:
在这里插入图片描述
第二次运行结果为:
在这里插入图片描述
注意:

  1. 任意时刻只允许一个线程对其上锁;
  2. mtx.lock():调用该函数得线程尝试加锁,如果上锁不成功,即其他线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生;
  3. mutex还有一个操作为:mtx.try_lock(),字面意思就是“尝试上锁”,与mtx.lock()不同的是:如果上锁不成功,当前线程不会阻塞。

2.2、lock_guard

创建lock_guard时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。

特点:

  1. 创建即加锁,作用域结束后自动析构并解锁,不需要手动解锁;
  2. 不能中途解锁,必须等作用域结束才能解锁;
  3. 不能复制。

示例:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>
using namespace std;int counter = 0;
std::mutex mtx;void increase(int time) {for (int i = 0; i < time; i++) {//上锁const lock_guard<std::mutex>lock(mtx);++counter;}
}int main(int argc, char** argv) {std::thread t1(increase, 100);std::thread t2(increase, 100);t1.join();t2.join();std::cout << "counter:" << counter << std::endl;return 0;
}

每次运行后结果都为200。

2.3、unique_lock

unique_lock是lock_guard的优化版,具有lock_guard的所有功能,还具有很多其他方法,使用起来更加灵活方便,能够应对更复杂的锁需要。

特点:

  1. 创建时可以不锁定(通过指定第二个参数defer_lock),而在需要时再锁;
  2. 可以随时加锁解锁;
  3. 作用域结束后自动析构并解锁;
  4. 不可复制,可移动;
  5. 条件变量需要改类型的锁作为参数(此时必须使用unique_lock)

ref():用于包装引用传递的值;
cref():用于包装按const引用传递的值。

3、condition_variable

condition_variable的头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any。condition_variable必须结合unique_lock使用,而condition_variable_any可以使用任意的锁。

condition_variable条件变量可以阻塞(wait 、wait_for、wait_until)调用的线程直到使用(notify_one、notify_all)通知恢复为止。condition_variable是一个类,既有构造函数,也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。

类型说明
condition_variable构建对象
析构删除,释放资源
waitwait until notified
wait_forwait for timeout or until notified
wait_untilwait until notified or time point
notify_one解锁一个线程,若有多个,则未知哪个线程执行
botify_all解锁所有线程
cv_status这是一个类,表示variable的状态
enum class cv_status{no time_out, timeout};

3.1、wait

condition_variable提供了两种wait()函数分别是:

//只有一个参数为unique_lock对象,当前线程的执行会被阻塞,直到收到notify为止
void wait(unique_lock<mutex>&lck);
//有两个参数分别为unique_lock对象和一个可调用对象(函数或者Lambda表达式等),当前线程仅在pred=false时阻塞
template <class Predicate>
void wait(unique_lock<mutex>&lck, Predicate pred);

调用wait时,该函数会自动调用lck.unlock()释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行,然后阻塞当前线程,另外,一旦当前线程获得通知(notified,通常是另外某个线程调用notify_*唤醒了当前线程),wait()函数再次调用lck.lock()重新上锁然后wait返回退出,可以理解为lck的状态变换和wait函数被调用(退出)是同时进行的。

示例:

#include <iostream>           // std::cout
#include <thread>             // std::thread, std::this_thread::yield
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variablestd::mutex mtx;
std::condition_variable cv;int cargo = 0;
//当cargo为0时,返回bool值0,否则返回1
bool shipment_available() {return cargo!=0;}void consume (int n) {for (int i=0; i<n; ++i) {std::unique_lock<std::mutex> lck(mtx);//自动上锁//第二个参数为false才阻塞(wait),阻塞完即unlock,给其它线程资源cv.wait(lck,shipment_available);// consume:std::cout << cargo << '\n';cargo=0;}
}int main ()
{std::thread consumer_thread (consume,10);for (int i=0; i<10; ++i) {//每次cargo每次为0时,shipment_avariable会返回false,就不会进入while下的语句(即不会放弃当前线程)while (shipment_available())    std::this_thread::yield();std::unique_lock<std::mutex> lck(mtx);cargo = i+1;cv.notify_one();}consumer_thread.join();,return 0;
}

说明:

  1. 主线程中的while,要在cargo为0时才会执行;
  2. 每次cargo置0后,子线程consumer_thread会解锁,主线程得以执行;
  3. 且每次cargo被置0后,wait就会启动等待。

3.2、wait_for

与wait()类似,不过wait_for()可以指定等待一个时间段,在当前线程收到notify或者rel_time超时之前,该线程都会处于阻塞状态,而一旦超时或收到通知,wait_for返回,剩下的处理步骤与wait类似。

template <class Rep, class Period>
cv_status wait_for(unique_lock<mutex>&lck, const chrono::duration<Rep,Period>&rel_time);

另外,wait_for()的重载版本的最后一个参数pred表示wait_for的预测条件,只有当pred为false时,调用wait_for()才会阻塞当前线程,并且在收到其他线程的通知后,只有当pred为true时才会解除阻塞。

template<class Rep, class Period>
cv_status wait_for(unique_lock<mutex>&lck, const chrono::duration<Rep, Period>&rel_time, Predicate pred);

示例:

#include<iostream>
#include<thread>
#include<chrono>
#include<mutex>
#include<condition_variable>using namesapce std;condition_variable cv;int value;
void readvalue(){cin>>value;cv.notify_one();
}int main(){cout<<"请输入一个整数:"<<endl;thread th(readvalue);mutex mtx;unique_lock<mutex>lck(mtx);while(cv.wait_for(lck,chrono::seconds(1)) == cv_status::timeout){cout<<"."<<endl;}cout<<"输入的整数为:"<<value<<endl;th.join();return 0;
}

说明:

通知或超时都会解锁,所以主线程会一直输出。

4、线程池

4.1、线程池的概念

在一个程序中,如果我们需要多次使用线程,这就意味着,需要多次的创建和销毁,而创建线程的过程必定会消耗内存,线程过多会带来调用的开销,进而影响缓存局部性能和整体性能。
所存在的问题如下:

  1. 创建太多线程,会浪费一定的资源,有些线程没有得到充分利用;
  2. 销毁太多线程,会导致之后再浪费时间重新进行创建;
  3. 创建线程太慢,会导致长时间的等待,弱化性能;
  4. 销毁线程太慢,会导致其他线程饥饿。
    而线程池的作用就体现出来了,它维护着多个线程,避免了在处理短时间任务时,创建与销毁线程的代价。

4.2、线程池的实现

在程序开始运行前就创建多个线程,这样,在程序运行时,只需要从线程池中拿来用就可以了,大大提高了程序运行效率。

一般线程池都由以下几个部分构成:

  1. 线程池管理(ThreadPoolManager):用于创建并管理线程池,也就是线程池类;
  2. 工作线程(WorkThread):线程池中线程;
  3. 任务队列task:用于存放没有处理的任务,提供一种缓冲机制;
  4. append:用于添加任务的接口。
    线程池实现代码:
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include <vector>
#include <queue>
#include <thread>
#include <iostream>
#include <stdexcept>
#include <condition_variable>
#include <memory> //unique_ptr
#include<assert.h>const int MAX_THREADS = 1000; //最大线程数目template <typename T>
class threadPool
{
public:threadPool(int number = 1);//默认开一个线程~threadPool();std::queue<T *> tasks_queue;		   //任务队列bool append(T *request);//往请求队列<task_queue>中添加任务<T *>private://工作线程需要运行的函数,不断的从任务队列中取出并执行static void *worker(void *arg);void run();private:std::vector<std::thread> work_threads; //工作线程std::mutex queue_mutex;std::condition_variable condition;  //必须与unique_lock配合使用bool stop;
};//end class//构造函数,创建线程
template <typename T>
threadPool<T>::threadPool(int number) : stop(false)
{if (number <= 0 || number > MAX_THREADS)throw std::exception();for (int i = 0; i < number; i++){std::cout << "created Thread num is : " << i <<std::endl;work_threads.emplace_back(worker, this);//添加线程//直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。}
}
template <typename T>
inline threadPool<T>::~threadPool()
{std::unique_lock<std::mutex> lock(queue_mutex);stop = true;condition.notify_all();for (auto &ww : work_threads)ww.join();//可以在析构函数中join
}
//添加任务
template <typename T>
bool threadPool<T>::append(T *request)
{/*操作工作队列时一定要加锁,因为他被所有线程共享*/queue_mutex.lock();//同一个类的锁tasks_queue.push(request);queue_mutex.unlock();condition.notify_one(); //线程池添加进去了任务,自然要通知等待的线程return true;
}
//单个线程
template <typename T>
void *threadPool<T>::worker(void *arg)
{threadPool *pool = (threadPool *)arg;pool->run();//线程运行return pool;
}
template <typename T>
void threadPool<T>::run()
{while (!stop){std::unique_lock<std::mutex> lk(this->queue_mutex);/* unique_lock() 出作用域会自动解锁 */this->condition.wait(lk, [this] { return !this->tasks_queue.empty(); });//如果任务为空,则wait,就停下来等待唤醒//需要有任务,才启动该线程,不然就休眠if (this->tasks_queue.empty())//任务为空,双重保障{assert(0&&"断了");//实际上不会运行到这一步,因为任务为空,wait就休眠了。continue;}else{T *request = tasks_queue.front();tasks_queue.pop();if (request)//来任务了,开始执行request->process();}}
}
#endif

说明:

  1. 构造函数创建所需要的线程数;
  2. 一个线程对应一个任务,任务可能随时完成,线程则可能休眠,所以用任务队列queue实现(线程数量有限),线程采用wait机制;
  3. 任务在不断地添加,有可能大于线程数,处于队首的任务先执行;
  4. 只有添加任务(append)后,才开启condition.notify_one();
  5. wait表示任务为空时,线程休眠,等待新任务的加入;
  6. 添加新任务时需要添加锁,因为共享资源。
    测试代码:
#include "mythread.h"
#include<string>
#include<math.h>
using namespace std;
class Task
{public:void process(){//cout << "run........." << endl;//测试任务数量long i=1000000;while(i!=0){int j = sqrt(i);i--;}}
};
int main(void)
{threadPool<Task> pool(6);//6个线程,vectorstd::string str;while (1){Task *tt = new Task();//使用智能指针pool.append(tt);//不停的添加任务,任务是队列queue,因为只有固定的线程数cout<<"添加的任务数量: "<<pool.tasks_queue.size()<<endl;;delete tt;}
}

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

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

相关文章

雪花算法 集群uid重复问题 uid-generator-spring-boot-starter

1、在生成环境 在某个业务使用该插件生成uid,由于业务整合了 mybatis-plus模块 2、该业务是分部署集群部署以及使用的多线程获取uid&#xff0c;使用中发现唯一建冲突&#xff0c;生成的uid有重复。 然后查看日志发现 workerId 始终为0 怀疑是生成workerId出了问题。 查看跟…

Cadence23学习笔记(十四)

ARC就是圆弧走线的意思&#xff1a; 仅打开网络的话可以只针对net进行修改走线的属性&#xff1a; 然后现在鼠标左键点那个走线&#xff0c;那个走线就会变为弧形&#xff1a; 添加差分对&#xff1a; 之后&#xff0c;分别点击两条线即可分配差分对&#xff1a; 选完差分对之后…

使用Spring Boot与Spire.Doc实现Word文档的多样化操作

​ 博客主页: 南来_北往 系列专栏&#xff1a;Spring Boot实战 前言 使用Spring Boot与Spire.Doc实现Word文档的多样化操作具有以下优势&#xff1a; 强大的功能组合&#xff1a;Spring Boot提供了快速构建独立和生产级的Spring应用程序的能力&#xff0c;而Spire.Doc则…

docker产生日志过大优化

1、Docker容器启动后日志存放位置 #cat /var/lib/docker/containers/容器ID/容器ID-json.log #echo >/var/lib/docker/containers/容器ID/容器ID-json.log临时清除日志 注&#xff1a;echo一个空进去&#xff0c;不需要重启容器&#xff0c;但如果你直接删除这个日志&…

如何在vscode中对在服务器上多卡运行的bash脚本进行debug?

问题描述 使用vscode可以很方便地添加断点&#xff0c;进行代码调试。 在使用服务器时&#xff0c;我们的python代码通常是通过bash脚本来执行的&#xff0c;那么如何进行debug呢&#xff1f; 待运行的bash 脚本示例 前半段定义了一些参数&#xff0c;后半段是执行python代码…

MiniExcel:.NET中处理Excel的高效方案

在.NET开发环境中&#xff0c;处理Excel文件是一项常见的任务&#xff0c;无论是数据导入、导出还是报表生成。传统的解决方案可能存在性能瓶颈或功能限制。MiniExcel作为一个现代、高效的库&#xff0c;为.NET开发者提供了一个强大的工具来简化Excel操作。本文将介绍MiniExcel…

萝卜快跑:自动驾驶的先锋与挑战

萝卜快跑&#xff1a;自动驾驶的先锋与挑战 近段时间&#xff0c;由萝卜快跑引发的自动驾驶事件如火如荼&#xff0c;成为科技领域的热门话题。萝卜快跑作为自动驾驶领域的重要参与者&#xff0c;其最新事件引发了广泛的关注和讨论。 萝卜快跑是百度推出的自动驾驶出行服务平台…

【Socket 编程】应用层自定义协议与序列化

文章目录 再谈协议序列化和反序列化理解 read、write、recv、send 和 tcp 为什么支持全双工自定义协议网络计算器序列化和反序列化 再谈协议 协议就是约定&#xff0c;协议的内容就是约定好的某种结构化数据。比如&#xff0c;我们要实现一个网络版的计算器&#xff0c;客户端…

LeetCode算法——滑动窗口矩阵篇

1、长度最小的子数组 题目描述&#xff1a; 解法&#xff1a; 设一个 for 循环来改变指向窗口末尾的指针&#xff0c;再不断抛弃当前窗口内的首元素 最终确定满足条件的最小长度 class Solution { public:int minSubArrayLen(int target, vector<int>& nums) {int …

Android --- ContentProvider 内容提供者

理论知识 ContentProvider 是 Android中用于数据共享的机制&#xff0c;主要是用于进程间(App之间)。 如何进行数据共享&#xff1f; 内容提供者 ContentProvider 提供数据&#xff0c;需要继承这个类,&#xff0c;并重写其中的增删改查方法。 继承 ContentProvider 类并重写增…

数组Arrays,排序算法,String类,Stringbulider,正则表达式

## 数组 排序 经典的三大排序&#xff1a;冒泡&#xff0c;选择&#xff0c;插入 &#xff08;一&#xff09;冒泡排序核心&#xff1a;数组中的 相邻 两项比较&#xff0c;交换&#xff08;正序or倒序&#xff09; 正序原理图&#xff1a; 代码实现&#xff1a; public s…

智慧大棚数据库版

创建一个SMartBigHouse数据库 在数据库创建一个表用来存储数据 这边将id设为主键并将标识增量设为1 搭建Winfrom 搭建历史查询界面 串口数据&#xff0c;(这边是用的一个虚拟的串口工具&#xff0c;需要的话私) ModbusSerialMaster master;DataPointCollection wenduValues; //…

opencascade AIS_Line源码学习

前言 AIS_Line 是 OpenCASCADE 库中的一个类&#xff0c;用于表示和操作三维直线。它可以通过几何线&#xff08;Geom_Line&#xff09;或者两个几何点&#xff08;Geom_Point&#xff09;来初始化。 方法 1 //! 初始化直线 aLine。 Standard_EXPORT AIS_Line(const Handl…

单片机学习历程

学习单片机的过程可以分为几个主要阶段&#xff0c;每个阶段都涉及不同的学习内容和技能提升。下面我将以一个典型的学习历程为例进行介绍&#xff1a; 初学阶段 1.入门理论学习&#xff1a; 开始接触单片机的基础知识&#xff0c;学习其工作原理、体系结构和常见的芯片类型…

Linux_make/Makefile的理解

1.make是一个命令&#xff0c;makefile是一个文件, 依赖关系和依赖方法. a.快速使用一下 i.创建一个Makefile文件(首字母也可以小写) b.依赖关系和依赖方法 i.依赖关系: 我为什么要帮你? mybin:mytest.c ii.依赖方法: 怎么帮? gcc -o mybin mytest.c make之前要注意先创建…

IDEA搭建Vue开发环境(安装Node.js、安装vue-cli、创建项目、编译项目、启动项目、yarn启动项目、npm和yarn命令行命令简单使用)

目录 1. 安装Node.js2. 安装vue-cli构建工具3. 使用vue-cli创建项目4. 启动项目5. IDEA启动vue6. 在IDEA编译vue项目7. 用yarn启动vue项目8. npm和yarn命令行命令简单使用8.1 npm8.2 yarn 1. 安装Node.js Node.js基于Google的V8引擎&#xff0c;形成了一个Javascript的运行环境…

AI绘画SD万能模型 ControlNet Union (也称ControlNet++ 或 ControlNetPlus)!10余种控制效果一键生成!

大家好&#xff0c;我是画画的小强 Controlnet 可以说是目前最重要的一款 AI 绘画控制插件&#xff0c;可以帮我们实现轮廓、深度、动作姿势、颜色等多种控制效果。由于每种控制条件都需要调用不同的控制模型&#xff0c;加上 SD1.5 和 SDXL 的生态并不互通&#xff0c;大家肯…

photoshop学习笔记——选区

选区工具快捷键&#xff1a;M shift M 切换 矩形/椭圆选区工具 基本用法 选区框选出的地方被激活&#xff08;其后进行的操作&#xff0c;仅在选区中生效&#xff09; 选区工具选择后&#xff08;以矩形选区为例&#xff09; 按下鼠标左键拖动&#xff0c;画出一块矩形区…

腾讯云COS异步操作上传(Python)

文章目录 相关概念介绍相关术语SDK使用异步上传文件 相关概念介绍 COS全称“云对象存储”&#xff08;Cloud Object Storage&#xff09;&#xff0c;是一种分布式存储服务&#xff0c;通过将数据作为对象存储&#xff0c;可以实现数据的高可靠性和可扩展性。它通常用于存储非…

《后端程序猿 · @Value 注释说明》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…