C++11并发与多线程

C++11并发与多线程

1. 线程是进程中的实际运作单位

  • 并发:两个或者更多的任务(独立的活动)同时发生(进行):一个程序同时执行多个独立的任务

  • 进程:一个可执行程序运行起来了,就叫创建了一个进程。进程就是运行起来的可执行程序

  • 线程:进程中的实际运作单位

image-20230921152755890

image-20230921153333468

2. 线程

创建线程
#include <iostream>
#include <thread> // ①using namespace std; // ②void hello() { // ③cout << "Hello World from new thread." << endl;
}int main() {thread t(hello); // ④t.join(); // ⑤return 0;
}

对于这段代码说明如下:

  1. 为了使用多线程的接口,我们需要#include <thread>头文件。
  2. 为了简化声明,本文中的代码都将using namespace std;
  3. 新建线程的入口是一个普通的函数,它并没有什么特别的地方。
  4. 创建线程的方式就是构造一个thread对象,并指定入口函数。与普通对象不一样的是,此时编译器便会为我们创建一个新的操作系统线程,并在新的线程中执行我们的入口函数。
  5. 关于join函数在下文中讲解。

在linux下编译

g++ 01_hello_thread.cpp -o 01test -pthread

传递参数给入口函数

#include<iostream>
#include<thread>
#include<string>
using namespace std;void hello(string name){cout<<"Welcome to "<<name<<endl;
}int main(){thread t(hello,"Hang Zhou");t.join();return 0;
}
join和detach
API说明
join等待线程完成其执行
detach允许线程独立执行
  • join:调用此接口时,当前线程会一直阻塞,直到目标线程执行完成。

  • detachdetach是让目标线程成为守护线程(daemon threads)。一旦detach之后,目标线程将独立执行,即便其对应的thread对象销毁也不影响线程的执行。

如果在thread对象销毁的时候我们还没有做决定,则thread对象在析构函数出将调用std::terminate()从而导致我们的进程异常退出。

通过joinable()接口查询是否可以对它们进行join或者detach

管理当前线程
APIC++标准说明
yieldC++11让出处理器,重新调度各执行线程
get_idC++11返回当前线程的线程 id
sleep_forC++11使当前线程的执行停止指定的时间段
sleep_untilC++11使当前线程的执行停止直到指定的时间点
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <thread>
using  namespace std;void print_time(){auto now = chrono::system_clock::now();auto in_time_t = chrono::system_clock::to_time_t(now);std::stringstream ss;ss<< put_time(localtime(&in_time_t),"%Y-%m-%d %X");cout<< "now is: "<<ss.str()<<endl;
}
void sleep_thread(){this_thread::sleep_for(chrono::seconds(3));cout<< "[thread-"<<this_thread::get_id()<<"] is waking up"<<endl;
}
void loop_thread() {for (int i = 0; i < 10; i++) {cout << "[thread-" << this_thread::get_id() << "] print: " << i << endl;}
}int main() {print_time();thread t1(sleep_thread);thread t2(loop_thread);t1.join();t2.detach();print_time();return 0;
}

这段代码使用了C++的标准库chrono、ctime、iomanip、iostream、sstream和thread。

函数print_time用于获取当前的系统时间,并以特定的格式打印出来。它使用了C++的chrono库来获取当前时间,并使用localtime函数将时间转换为本地时间,然后使用put_time函数将时间以指定的格式进行格式化输出。

函数sleep_thread用于使当前线程休眠3秒,并在休眠结束后输出一条消息表明线程已经唤醒。它使用了C++的thread库中的sleep_for函数来使当前线程休眠。this_thread::get_id()函数可以获取当前线程的ID。

函数loop_thread用于打印从0到9的整数,并输出打印的线程ID。它在一个循环中打印数值,每次打印后会自动进行下一次迭代。

main函数中,首先调用print_time函数打印当前时间。然后创建了两个线程t1t2,分别执行sleep_threadloop_thread函数。线程t1会调用sleep_thread函数休眠3秒,而线程t2会在循环中执行loop_thread函数。使用t1.join()语句可以等待线程t1执行完毕,而t2.detach()语句则将线程t2与主线程分离,使得线程t2在后台执行而不会影响主线程的运行。最后再次调用print_time函数打印当前时间,并返回0表示程序正常结束。

now is: 2023-09-21 19:22:24
[thread-140221292599040] print: 0
[thread-140221292599040] print: 1
[thread-140221292599040] print: 2
[thread-140221292599040] print: 3
[thread-140221292599040] print: 4
[thread-140221292599040] print: 5
[thread-140221292599040] print: 6
[thread-140221292599040] print: 7
[thread-140221292599040] print: 8
[thread-140221292599040] print: 9
[thread-140221300991744] is waking up
now is: 2023-09-21 19:22:27
一次调用
APIC++标准说明
call_onceC++11即便在多线程环境下,也能保证只调用某个函数一次
once_flagC++11call_once配合使用

有些任务需要执行一次,并且我们只希望它执行一次,例如资源的初始化任务。

#include <iostream>
#include <thread>
#include <mutex>using namespace std;void init(){cout<< "Initialing..."<<endl;
}void worker(once_flag* flag){call_once(*flag,init);
}int main(){once_flag  flag;thread t1(worker,&flag);thread t2(worker,&flag);thread t3(worker,&flag);t1.join();t2.join();t3.join();return 0;
}
Initialing...

无法确定具体是哪一个线程会执行init

3. 并发

#include<iostream>
#include<cmath>
#include<chrono>
#include<vector>
#include<thread>using namespace std;static const int MAX = 10e8;
static double sum = 0;void worker(int min,int max){for(int i = min; i<=max; i++){sum += sqrt(i);}
}void serial_task(int min,int max){auto start_time = chrono::steady_clock::now();sum = 0;worker(0,MAX);auto end_time = chrono::steady_clock::now();auto ms = chrono::duration_cast<chrono::microseconds>(end_time-start_time).count();cout << "Serail task finish, " << ms << " ms consumed, Result: " << sum << endl;
}void concurrent_task(int min, int max){auto start_time = chrono::steady_clock::now();unsigned concurrent_count = thread::hardware_concurrency();cout << "hardware_concurrency: " << concurrent_count << endl;vector<thread> threads;min = 0;sum = 0;for(int t = 0; t < concurrent_count; t++){int range = max / concurrent_count * (t + 1);threads.push_back(thread(worker,min,range));min = range + 1;}for(int i = 0; i < threads.size(); i++){threads[i].join();}auto end_time = chrono::steady_clock::now();auto ms = chrono::duration_cast<chrono::milliseconds>(end_time-start_time).count();cout << "Concurrent task finish, " << ms << " ms consumed, Result: " << sum << endl;}int main(){serial_task(0, MAX);concurrent_task(0, MAX);return 0;
}

定义了一个串行任务函数serial_task,它接受两个参数minmax。在这个函数中,首先记录任务开始的时间,然后将sum重置为0,接着调用worker函数来进行计算,计算的范围是从0到MAX。接着记录任务结束的时间,计算任务执行时间,并输出计算结果。

接着定义了一个并行任务函数concurrent_task,它也接受两个参数minmax。在这个函数中,首先记录任务开始的时间,然后通过thread::hardware_concurrency()函数获取系统的并发线程数,并输出到控制台。接着定义一个线程数组threads,并用循环创建多个线程来执行worker函数,每个线程计算一个范围内的数的平方根并累加到sum变量中。最后,等待所有线程执行完毕,记录任务结束的时间,计算任务执行时间,并输出计算结果。

Serail task finish, 6655 ms consumed, Result: 2.10819e+13
hardware_concurrency: 8
Concurrent task finish, 5232 ms consumed, Result: 2.88206e+12
#在这里并行的速度并没有比串行快多少,而且计算的结果还是错的

img

处理器在进行计算的时候,高速缓存会参与其中,例如数据的读和写。而高速缓存和系统主存(Memory)是有可能存在不一致的。即:某个结果计算后保存在处理器的高速缓存中了,但是没有同步到主存中,此时这个值对于其他处理器就是不可见的。

事情还远不止这么简单。我们对于全局变量值的修改:sum += sqrt(i);这条语句,它并非是原子的。它其实是很多条指令的组合才能完成。假设在某个设备上,这条语句通过下面这几个步骤来完成。它们的时序可能如下所示:

img

竞争条件与临界区

当多个进程或者线程同时访问共享数据时,只要有一个任务会修改数据,那么就可能会发生问题。此时结果依赖于这些任务执行的相对时间,这种场景称为竞争条件(race condition)。

访问共享数据的代码片段称之为临界区(critical section)。具体到上面这个示例,临界区就是读写sum变量的地方。

4. 互斥和锁

mutex

开发并发系统的目的主要是为了提升性能:将任务分散到多个线程,然后在不同的处理器上同时执行。这些分散开来的线程通常会包含两类任务:

  1. 独立的对于划分给自己的数据的处理
  2. 对于处理结果的汇总

其中第1项任务因为每个线程是独立的,不存在竞争条件的问题。而第2项任务,由于所有线程都可能往总结果(例如上面的sum变量)汇总,这就需要做保护了。在某一个具体的时刻,只应当有一个线程更新总结果,即:保证每个线程对于共享数据的访问是“互斥”的。mutex 就提供了这样的功能。

APIC++标准说明
mutexC++11提供基本互斥设施
timed_mutexC++11提供互斥设施,带有超时功能
recursive_mutexC++11提供能被同一线程递归锁定的互斥设施
recursive_timed_mutexC++11提供能被同一线程递归锁定的互斥设施,带有超时功能
shared_timed_mutexC++14提供共享互斥设施并带有超时功能
shared_mutexC++17提供共享互斥设施
方法说明
lock锁定互斥体,如果不可用,则阻塞
try_lock尝试锁定互斥体,如果不可用,直接返回
unlock解锁互斥体

这三个方法提供了基础的锁定和解除锁定的功能。使用lock意味着有很强的意愿一定要获取到互斥体,而使用try_lock则是进行一次尝试。这意味着如果失败了,你通常还有其他的路径可以走。

在这些基础功能之上,其他的类分别在下面三个方面进行了扩展:

  • 超时timed_mutexrecursive_timed_mutexshared_timed_mutex的名称都带有timed,这意味着它们都支持超时功能。它们都提供了try_lock_fortry_lock_until方法,这两个方法分别可以指定超时的时间长度和时间点。如果在超时的时间范围内没有能获取到锁,则直接返回,不再继续等待。
  • 可重入recursive_mutexrecursive_timed_mutex的名称都带有recursive。可重入或者叫做可递归,是指在同一个线程中,同一把锁可以锁定多次。这就避免了一些不必要的死锁。
  • 共享shared_timed_mutexshared_mutex提供了共享功能。对于这类互斥体,实际上是提供了两把锁:一把是共享锁,一把是互斥锁。一旦某个线程获取了互斥锁,任何其他线程都无法再获取互斥锁和共享锁;但是如果有某个线程获取到了共享锁,其他线程无法再获取到互斥锁,但是还有获取到共享锁。这里互斥锁的使用和其他的互斥体接口和功能一样。而共享锁可以同时被多个线程同时获取到(使用共享锁的接口见下面的表格)。共享锁通常用在读者写者模型上。

使用共享锁的接口如下:

方法说明
lock_shared获取互斥体的共享锁,如果无法获取则阻塞
try_lock_shared尝试获取共享锁,如果不可用,直接返回
unlock_shared解锁共享锁
  1. 在访问共享数据之前加锁
  2. 访问完成之后解锁
  3. 在多线程中使用带锁的版本
#include <chrono>
#include <cmath>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>using namespace std;static const int MAX = 10e8;
static double sum = 0;static mutex exclusive;void concurrent_worker(int min, int max) {for (int i = min; i <= max; i++) {exclusive.lock();sum += sqrt(i);exclusive.unlock();}
}void worker(int min, int max) {for (int i = min; i <= max; i++) {double sqrt_i = sqrt(i);// 使用互斥锁保护对 sum 的访问lock_guard<mutex> lock(sum_mutex);sum += sqrt_i;}
}void concurrent_task(int min, int max) {auto start_time = chrono::steady_clock::now();unsigned concurrent_count = thread::hardware_concurrency();cout << "hardware_concurrency: " << concurrent_count << endl;vector<thread> threads;min = 0;sum = 0;for (int t = 0; t < concurrent_count; t++) {int range = max / concurrent_count * (t + 1);threads.push_back(thread(concurrent_worker, min, range));min = range + 1;}for (int i = 0; i < threads.size(); i++) {threads[i].join();}auto end_time = chrono::steady_clock::now();auto ms = chrono::duration_cast<chrono::milliseconds>(end_time - start_time).count();cout << "Concurrent task finish, " << ms << " ms consumed, Result: " << sum << endl;
}int main() {concurrent_task(0, MAX);return 0;
}
Serail task finish, 26665 ms consumed, Result: 2.10819e+13
(1)
hardware_concurrency: 8
Concurrent task finish, 156722 ms consumed, Result: 2.10819e+13
(2)
hardware_concurrency: 8
Concurrent task finish, 167880 ms consumed, Result: 2.10819e+13

结果是对了,但是我们却发现这个版本比原先单线程的版本性能还要差很多。这是因为加锁和解锁是有代价的,这里计算最耗时的地方在锁里面,每次只能有一个线程串行执行,相比于单线程模型,它不但是串行的,还增加了锁的负担,因此就更慢了。

于是我们改造concurrent_worker,像下面这样:

// 08_improved_mutex_lock.cppvoid concurrent_worker(int min, int max) {double tmp_sum = 0;for (int i = min; i <= max; i++) {tmp_sum += sqrt(i); // ①}exclusive.lock(); // ②sum += tmp_sum;exclusive.unlock();
}

这段代码的改变在于两处:

  1. 通过一个局部变量保存当前线程的处理结果
  2. 在汇总总结过的时候进行锁保护
hardware_concurrency: 8
Concurrent task finish, 1304 ms consumed, Result: 2.10819e+13

用锁的粒度(granularity)来描述锁的范围。细粒度(fine-grained)是指锁保护较小的范围,粗粒度(coarse-grained)是指锁保护较大的范围。出于性能的考虑,我们应该保证锁的粒度尽可能的细。并且,不应该在获取锁的范围内执行耗时的操作,例如执行IO。如果是耗时的运算,也应该尽可能的移到锁的外面。

死锁
#include <iostream>
#include <mutex>
#include <set>
#include <thread>using namespace std;class Account {
public:Account(string name, double money): mName(name), mMoney(money) {};public:void changeMoney(double amount) {mMoney += amount;}string getName() {return mName;}double getMoney() {return mMoney;}mutex* getLock() {return &mMoneyLock;}private:string mName;double mMoney;mutex mMoneyLock;
};class Bank {
public:void addAccount(Account* account) {mAccounts.insert(account);}bool transferMoney(Account* accountA, Account* accountB, double amount) {lock_guard<mutex> guardA(*accountA->getLock());lock_guard<mutex> guardB(*accountB->getLock());if (amount > accountA->getMoney()) {return false;}accountA->changeMoney(-amount);accountB->changeMoney(amount);return true;}double totalMoney() const {double sum = 0;for (auto a : mAccounts) {sum += a->getMoney();}return sum;}private:set<Account*> mAccounts;
};void randomTransfer(Bank* bank, Account* accountA, Account* accountB) {while(true) {double randomMoney = ((double)rand() / RAND_MAX) * 100;if (bank->transferMoney(accountA, accountB, randomMoney)) {cout << "Transfer " << randomMoney << " from " << accountA->getName()<< " to " << accountB->getName()<< ", Bank totalMoney: " << bank->totalMoney() << endl;} else {cout << "Transfer failed, "<< accountA->getName() << " has only $" << accountA->getMoney() << ", but "<< randomMoney << " required" << endl;}}
}int main() {Account a("Paul", 100);Account b("Moira", 100);Bank aBank;aBank.addAccount(&a);aBank.addAccount(&b);thread t1(randomTransfer, &aBank, &a, &b);thread t2(randomTransfer, &aBank, &b, &a);t1.join();t2.join();return 0;
}

这两个线程可能会同时获取其中一个账号的锁,然后又想获取另外一个账号的锁,此时就发生了死锁。如下图所示:

img

当然,发生死锁的原因远不止上面这一种情况。如果两个线程互相join就可能发生死锁。还有在一个线程中对一个不可重入的互斥体(例如mutex而非recursive_mutex)多次加锁也会死锁。

修改

#include <iostream>
#include <mutex>
#include <set>
#include <thread>using namespace std;class Account {
public:Account(string name, double money): mName(name), mMoney(money) {};public:void changeMoney(double amount) {mMoney += amount;}string getName() {return mName;}double getMoney() {return mMoney;}mutex* getLock() {return &mMoneyLock;}private:string mName;double mMoney;mutex mMoneyLock;
};class Bank {
public:void addAccount(Account* account) {mAccounts.insert(account);}bool transferMoney(Account* accountA, Account* accountB, double amount) {// lock(*accountA->getLock(), *accountB->getLock());// lock_guard lockA(*accountA->getLock(), adopt_lock);// lock_guard lockB(*accountB->getLock(), adopt_lock);scoped_lock lockAll(*accountA->getLock(), *accountB->getLock());if (amount > accountA->getMoney()) {return false;}accountA->changeMoney(-amount);accountB->changeMoney(amount);return true;}double totalMoney() const {double sum = 0;for (auto a : mAccounts) {sum += a->getMoney();}return sum;}private:set<Account*> mAccounts;
};mutex sCoutLock;
void randomTransfer(Bank* bank, Account* accountA, Account* accountB) {while(true) {double randomMoney = ((double)rand() / RAND_MAX) * 100;if (bank->transferMoney(accountA, accountB, randomMoney)) {sCoutLock.lock();cout << "Transfer " << randomMoney << " from " << accountA->getName()<< " to " << accountB->getName()<< ", Bank totalMoney: " << bank->totalMoney() << endl;sCoutLock.unlock();} else {sCoutLock.lock();cout << "Transfer failed, "<< accountA->getName() << " has only " << accountA->getMoney() << ", but "<< randomMoney << " required" << endl;sCoutLock.unlock();}}
}int main() {Account a("Paul", 100);Account b("Moira", 100);Bank aBank;aBank.addAccount(&a);aBank.addAccount(&b);thread t1(randomTransfer, &aBank, &a, &b);thread t2(randomTransfer, &aBank, &b, &a);t1.join();t2.join();return 0;
}

C++17 或更早版本的标准,并且编译器不支持 C++17 的 <mutex> 头文件中的 scoped_lock,所以要指定g++以c++17标准编译

g++ -std=c++17 10_improved_bank_transfer.cpp -o 10test -pthread
通用锁定算法
APIC++标准说明
lockC++11锁定指定的互斥体,若任何一个不可用则阻塞
try_lockC++11试图通过重复调用 try_lock 获得互斥体的所有权
RAII

std::mutex 是 C++11 中最基本的 mutex 类,通过实例化 std::mutex 可以创建互斥量, 而通过其成员函数 lock() 可以进行上锁,unlock() 可以进行解锁。 但是在实际编写代码的过程中,最好不去直接调用成员函数, 因为调用成员函数就需要在每个临界区的出口处调用 unlock(),当然,还包括异常。 这时候 C++11 还为互斥量提供了一个 RAII 语法的模板类 std::lock_guard。 RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。

在 RAII 用法下,对于临界区的互斥量的创建只需要在作用域的开始部分,例如:

#include <iostream>
#include <mutex>
#include <thread>int v = 1;void critical_section(int change_v) {static std::mutex mtx;std::lock_guard<std::mutex> lock(mtx);// 执行竞争操作v = change_v;// 离开此作用域后 mtx 会被释放
}int main() {std::thread t1(critical_section, 2), t2(critical_section, 3);t1.join();t2.join();std::cout << v << std::endl;return 0;
}
情况1(不加锁):
233
情况2:
2
3
3
情况3:
3
2
3

由于 C++ 保证了所有栈对象在生命周期结束时会被销毁,所以这样的代码也是异常安全的。 无论 critical_section() 正常返回、还是在中途抛出异常,都会引发堆栈回退,也就自动调用了 unlock()

std::unique_lock 则是相对于 std::lock_guard 出现的,std::unique_lock 更加灵活, std::unique_lock 的对象会以独占所有权(没有其他的 unique_lock 对象同时拥有某个 mutex 对象的所有权) 的方式管理 mutex 对象上的上锁和解锁的操作。所以在并发编程中,推荐使用 std::unique_lock

std::lock_guard 不能显式的调用 lockunlock, 而 std::unique_lock 可以在声明后的任意位置调用, 可以缩小锁的作用范围,提供更高的并发度。

如果你用到了条件变量 std::condition_variable::wait 则必须使用 std::unique_lock 作为参数。

例如:

#include <iostream>
#include <mutex>
#include <thread>using namespace std;
int v = 1;
void critical_section(int change_v){static mutex mtx;unique_lock<mutex> lock(mtx);v = change_v;cout<<"1:"<< v << endl;//释放锁lock.unlock();// 在此期间,任何人都可以抢夺 v 的持有权// 开始另一组竞争操作,再次加锁lock.lock();v += 1;cout<<"2:"<< v << endl;
}int main(){thread t1(critical_section,2),t2(critical_section,3);t1.join();t2.join();return 0;
}
情况1
1:3
2:4
1:2
2:3
情况2
1:2
2:3
1:3
2:4

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

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

相关文章

1、TCP 和 UDP 区别? 2、TCP/IP 协议涉及哪几层架构? 3、描述下 TCP 连接 4 次挥手的过程?为什么要 4 次挥手?

文章目录 1、TCP 和 UDP 区别&#xff1f;2、TCP/IP 协议涉及哪几层架构&#xff1f;3、描述下 TCP 连接 4 次挥手的过程&#xff1f;为什么要 4 次挥手&#xff1f; 1、TCP 和 UDP 区别&#xff1f; &#xff08;1&#xff09;TCP 基于连接&#xff0c;UDP 基于无连接。 &…

蓝桥杯2019年11月青少组Python程序设计省赛真题

1、试编写一个程序,输入一个整数,输出它的各个数位之和。 2、试编写一个程序,输入一个带有小数的数字,输出它的各个数位之和。 3、小兰要为1-2020住户制作门牌号,例如制作1107号门牌,需要制作2块1字符,一块0"字符一块7"字符,求制作1-2020需要多少块2. 4、编程画…

【量化金融】证券投资学

韭菜的自我修养 第一章&#xff1a; 基本框架和概念1.1 大盘底部形成的技术条件1.2 牛市与熊市1.3 交易系统1.3.1 树懒型交易系统1.3.2 止损止损的4个技术 第二章&#xff1a;证券家族4兄弟2.1 债券&#xff08;1&#xff09;债券&#xff0c;是伟大的创新&#xff08;2&#x…

【零基础入门Python】Python参数

✍面向读者&#xff1a;所有人 ✍所属专栏&#xff1a;零基础入门Pythonhttps://blog.csdn.net/arthas777/category_12455877.html 目录 print&#xff08;&#xff09;中的Python结束参数 print&#xff08;&#xff09;中的Python|sep参数 Python的格式转换规则 使用格式…

种系进化树分析和构建工具R工具包S.phyloMaker的介绍和详细使用方法

S.PhyloMaker介绍 先看文章&#xff1a;updated megaphylogeny of plants, a tool for generating plant phylogenies and an analysis of phylogenetic community structure | Journal of Plant Ecology | Oxford Academic 再看仓库&#xff1a;GitHub - jinyizju/S.PhyloMa…

【数据结构入门精讲 | 第十五篇】散列表知识点及考研408、企业面试练习(2)

在上一篇文章中我们学习了散列表的相关知识点及进行了选择题、编程题的练习&#xff0c;这篇文章中我们将进行编程题的练习&#xff0c;带领读者以练代学、更好地掌握知识点。 目录 R7-1 QQ帐户的申请与登陆R7-2 词频统计R7-3 新浪微博热门话题R7-4 航空公司VIP客户查询R7-1 字…

九大GRASP类和设计模式

面向对象设计原则是一组指导软件设计的原则&#xff0c;其中GRASP&#xff08;General Responsibility Assignment Software Patterns&#xff09;是其中的一部分。这些原则帮助设计者确定类应该负责执行哪些职责&#xff0c;以及如何分配这些职责。在下面的文档中&#xff0c;…

leetcode 30. 串联所有单词的子串

题目链接&#xff1a;leetcode 30 1.题目 给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。 s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。 例如&#xff0c;如果 words [“ab”,“cd”,“ef”]&#xff0c; …

PyTorch之线性回归

1.定义&#xff1a; 回归分析是确定两种或两种以上变量间相互依赖的定量关系的一种统计分析方法。线性回归是利用称为线性回归方程的最小二乘函数&#xff0c;对一个或多个自变量和因变量之间关系&#xff0c;进行建模的一种回归分析。这种函数是一个或多个称为回归系数的模型参…

LeetCode——1962. 移除石子使总数最小

通过万岁&#xff01;&#xff01;&#xff01; 题目&#xff1a;给你一个数组。数组中的元素表示石子的个数&#xff0c;我们可以从里面移除一些元素&#xff0c;溢出的规则是第i位置的元素除2后向下取整。并且可以移除k次&#xff0c;要求最后的石子总个数最小。思路一&…

生成allure报告出现:ALLURE REPORT UNKNOWN

问题&#xff1a;点击浏览器查看时无法查看到报告 错误代码&#xff1a; if __name__ "__main__":pytest.main([./test_study/test_fixture.py])os.system("allure generate ./temps -o ./temps --clean") 结果导向&#xff1a; 解决&#xff1a;因为…

Hadoop入门学习笔记——四、MapReduce的框架配置和YARN的部署

视频课程地址&#xff1a;https://www.bilibili.com/video/BV1WY4y197g7 课程资料链接&#xff1a;https://pan.baidu.com/s/15KpnWeKpvExpKmOC8xjmtQ?pwd5ay8 Hadoop入门学习笔记&#xff08;汇总&#xff09; 目录 四、MapReduce的框架配置和YARN的部署4.1. 配置MapReduce…

动态规划03-01背包问题

问题描述 作为动态规划中最重要的经典例题&#xff0c;01背包问题开启了我们学习二维dp数组的道路。 题目如下&#xff1a; 有一个容量为V的背包&#xff0c;还有n个物体。现在忽略物体实际几何形状&#xff0c;我们认为只要背包的剩余容量大于等于物体体积&#xff0c;那就可…

缓存高可用:缓存如何保证高可用?

前面我们提到了缓存集群的负载均衡策略&#xff0c;保证缓存服务的高可用&#xff0c;集群策略是最常用的&#xff0c;本文我们以 Redis 为例&#xff0c;分析一下单点缓存如何扩展到集群&#xff0c;以及集群部署的几种常见模式。 Redis 的主从复制 集群实现依靠副本&#x…

爬虫字典生成工具,CeWL使用教程

爬虫字典生成工具,CeWL使用教程 1.工具概述2.参数解析3.使用实例1.工具概述 CeWL 是一个 ruby 应用程序,它将给定的 URL 爬到指定的深度,可以选择跟随外部链接,并返回一个单词列表,然后可用于密码破解者 Cewl 是黑客武器库中的强大工具,因为它允许创建有针对性的单词列…

如何在Spring Boot中优雅地进行参数校验

1. 前言 在平时的开发工作中&#xff0c;我们通常需要对接口进行参数格式验证。当参数个数较少&#xff08;个数小于3&#xff09;时&#xff0c;可以使用if ... else ...手动进行参数验证。当参数个数大于3个时&#xff0c;使用if ... else ...进行参数验证就会让代码显得臃肿…

使用 Spring Boot + MyBatis开发需要注意的事项以及开发模版

前言&#xff1a; 注意&#xff0c;本篇不适用于有相关开发经验的开发者&#xff0c;作为一个在职开发者&#xff0c;我经常在完成从0-1的模块&#xff0c;也就是从数据库表开始到创建实体类&#xff0c;以及dao层&#xff0c;Service层等业务需要添加相关注解&#xff0c;这样…

pytorch常用的几个函数详解

view view() 是 PyTorch 中的一个常用函数&#xff0c;用于改变张量&#xff08;tensor&#xff09;的形状。在深度学习中&#xff0c;我们经常需要调整数据的形状以适应不同的网络结构或计算需求&#xff0c;view() 函数就是用来完成这个任务的。 基本用法 view() 函数接受…

nn.LSTM个人记录

简介 nn.LSTM参数 torch.nn.lstm(input_size, "输入的嵌入向量维度&#xff0c;例如每个单词用50维向量表示&#xff0c;input_size就是50"hidden_size, "隐藏层节点数量,也是输出的嵌入向量维度"num_layers, "lstm 隐层的层数&#xff0c;默认…