30. 并发编程
一、什么是多任务
如果一个操作系统上同时运行了多个程序,那么称这个操作系统就是 多任务的操作系统,例如:Windows、Mac、Android、IOS、Harmony 等。如果是一个程序,它可以同时执行多个事情,那么就称为 多任务的程序。
一个 CPU 默认可以执行一个程序,如果想要多个程序一起执行,理论上就需要多个 CPU 来执行。
如果一个 CPU 是一个核心,理论上只能同时运行一个任务,但是事实上却可以运行很多个任务。这是因为操作系统控制着 CPU,让 CPU 做了一个特殊的事情,一会运行一个任务,然后快速的运行另一个任务,依次类推,实现了多个任务,看上去“同时”运行多个任务。
并发:是一个对假的多任务的描述;
并行:是真的多任务的描述;
二、进程与线程
计算机程序只是存储在磁盘上的可执行二进制(或其它类型)文件。只有把它们加载到内存中从被操作系统调用,才拥有其生命期。
进程(process)则是一个执行中的程序。每个进程都拥有自己的地址空间、内存、数据栈以及其它用于跟踪执行的辅助数据。操作系统管理其上所有进程的执行,并为这些进程合理分配时间。进程也可以通过派生新的进程来执行其它任务,不过因为每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信的方式共享数据;
线程(thread)与进程类似,不过它们是同一个进程下执行的,并共享相同的下上文。线程包括开始、执行顺序和结束三部分。它有一个指令指针,用于记录当前运行的上下文。当其它线程运行时,它可以被抢占(中断)和临时挂起(也称为睡眠)—— 这种做法叫做让步(yielding)。
一个进程中的各个线程与主线程共享同一片数据空间。线程一般是以并发方式执行的。在单核 CPU 系统中,因为真正的并发是不可能的,所以线程的执行实际上是这样规划的:每个线程运行一小会,然后让步给其它线程(再次排队等待更多的 CPU 时间)。在整个进程的执行过程中,每个线程执行它自己特定的任务,在必要时和其它线程进行结果通信。
但是这种共享数据也是存在风险的。如果两个或多个线程访问同一片数据,由于数据访问顺序不同,可能导致结果不一致。这种情况通常称为 “竞态条件”(race condition)。另一个需要注意的问题时,线程无法给予公平的执行时间。这是因为一些函数会在完成前保持阻塞状态,如果没有专门为多线程情况进行修改,会导致 CPU 的时间分配向这些贪婪的函数倾斜。
线程是计算机中可以被 CPU 调度的最小单元,进程是计算机资源分配的最小单元;进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域;
一个程序,至少有一个进程,一个进程中至少有一个线程,最终是线程在工作;
一个进程内可以开设多个线程,在用一个进程内开设多个线程无需再次申请空间及拷贝代码的操作,开设线程的开销远远的要小于进程的开销;
单核 CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。但是因为 CPU 时间单元特别短,因此感觉不出来;
三、线程的生命周期
要想实现多线程,必须在主线程中创建新的线程对象。Python 中使用 threading 模块或者 Thread 子类来表示线程,在它的一个完整的生命周期中通常要经过如下的五种状态:
- 创建:当一个 Thread 类或及其子类的对象被声明并创建时,新生的线程就处于新建状态;
- 就绪:处于新建的线程被 start() 后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件,只是没分配到 CPU 资源;
- 运行:当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run() 方法定义了线程的操作和功能;
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态;
- 退出:线程完成了它的全部或线程被提前强制性中止或出现异常导致结束;

四、线程的基本操作
C++ 11 标准提供了 thread 类模板用于创建线程,该类模板定义在 thread 标准库中,因此在创建线程时,需要包含 thread 头文件。thread 类模板定义了一个无参构造函数和一个变参构造函数,因此在创建线程对象时,可以为线程传入参数,也可以不传入参数。需要注意的是,thread 类模板不提供拷贝构造函数、赋值运算符重载等函数,因此线程对象之间不可以进行拷贝、赋值等操作。
除了构造函数,thread 类模板还定义了两个常用的成员函数:join() 函数和 detach() 函数。
join()函数:该函数将线程和线程对象连接起来,即将子线程加入程序执行。join() 函数是阻塞的,它可以阻塞主线程(当前线程),等待子线程工作结束之后,再启动主线程继续执行任务。detach()函数:该函数分离线程与线程对象,即主线程和子线程可同时进行工作,主线程不必等待子线程结束。但是,detach() 函数分离的线程对象不能再调用 join() 函数将它与线程连接起来。
#include <iostream>
#include <thread>using namespace std;void func(string name)
{cout << name << "程开始工作" << endl;cout << name << "结束工作" << endl;
}int main(void)
{cout << "主线程开始工作" << endl;// 创建线程并执行函数thread t(func, "线程1");// 判断一个线程是否可以使用join()// 如果一个线程不可以使用join()但强行使用join()编译器会报错if (t.joinable()){// 等待线程结束t.join();}cout << "主线程结束工作" << endl;return 0;
}
第 7~11 行代码定义了函数 func()。第 18 行代码创建线程对象 t,传入 func() 函数名作为参数,即创建一个子线程去执行 func() 函数的功能。第 25 行代码调用 join() 函数阻塞主线程。主线程等待子线程工作结束之后才结束工作。
在 C++ 多线程中,线程对象与线程是相互关联的,线程对象出了作用域之后就会被析构,如果此时线程函数还未执行完,程序就会发生错误,因此需要保证线程函数的生命周期在线程对象生命周期之内。一般通过调用 thread 中定义的 join() 函数阻塞主线程,等待子线程结束,或者调用 thread 中的 detach() 函数将线程与线程对象进行分离,让线程在后台执行,这样即使线程对象生命周期结束,线程也不会受到影响。
在上述代码中,将 join() 函数替换为 detach() 函数,将线程对象与线程分离,让线程在后台运行,再次运行程序,运行结果就可能发生变化。即使 main() 函数(主线程)结束,子线程对象 t 生命周期结束,子线程依然会在后台将 func() 函数执行完毕。
C++ 11 标准定义了 this_thread 命名空间,该空间提供了一组获取当前线程信息的函数,分别如下所示:
get_id()函数:获取当前线程id。yeild()函数:放弃当前线程的执行权。操作系统会调度其他线程执行未用完的时间片,当时间片用完之后,当前线程再与其他线程一起竞争 CPU 资源。sleep_until()函数:让当前线程休眠到某个时间点。sleep_for()函数:让当前线程休眠一段时间。
#include <iostream>
#include <thread>using namespace std;void func(string &name)
{cout << name << "开始工作" << endl;cout << name << "结束工作" << endl;
}int main(void)
{cout << "主线程开始工作" << endl;// 创建线程并执行函数string name = "线程A";thread t(func, ref(name));t.join();cout << "主线程结束工作" << endl;return 0;
}
上述程序中,如果我们把 t.join() 替换成 t.detach(),程序可以运行错误。这是因为 t.detach() 函数分离线程与线程对象,即主线程和子线程可同时进行工作,主线程不必等待子线程结束。如果这是主线程先执行完毕,引用或指针指向的 name 变量的内存会释放。
如果我们传递的参数是引用或指针类型的变量时,需要注意引用或指针类型指向的变量的内存地址是否已经被释放了。
五、线程同步问题
如果一个程序有多个线程,每个线程可以单独执行自己的任务。如果多个线程之间需要数据共享,我们可以通过全局变量的方式实现。一个线程修改了全局变量,其它线程可以读取这个修改后的全局变量。
多个线程操作同一份数据时,可能会出现数据错乱的问题。例如,有 3 个线程,其中线程 1 和线程 2 修改全局变量,线程 3 获取全局变量的值。可能会出现第 线程 1 刚刚将数据存放到了全局变量中,本意是想让线程 3 获取它的数据,但是因为操作系统的调度原因导致线程 3 没有被调度,而线程 2 被调度了,恰巧线程 2 也对全局变量进行了修改。而当线程 3 去读取数据时,读取到的是线程 2 修改的数据,而不是线程修改的数据。
多个线程操作同一份数据时,可能会出现数据错乱的问题。针对上述问题,解决方式就是加锁处理:将并发变成串行,牺牲效率但保证了数据的安全。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为 “锁定” ,其它线程不能更改;直到该线程释放资源,将资源的状态变成 “非锁定”,其它的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
C++ 11 标准提供了互斥锁 mutex,用于为共享资源加锁,让多个线程互斥访问共享资源。mutex 是一个类模板,定义在 mutex 标准库中,使用时要包含 mutex 头文件。mutex 类模板定义了三个常用的成员函数:lock() 函数、unlock() 函数和 try_lock() 函数,用于实现上锁、解锁功能。
lock()函数:用于给共享资源上锁。如果共享资源已经被其他线程上锁,则当前线程被阻塞;如果共享资源已经被当前线程上锁,则产生死锁。unlock()函数:用于给共享资源解锁,释放当前线程对共享资源的所有权。try_lock()函数:也用于给共享资源上锁,但它是尝试上锁,如果共享资源已经被其他线程上锁,try_lock() 函数返回 false,当前线程并不会被阻塞,而是继续执行其他任务;如果共享资源已经被当前线程上锁,则产生死锁。
#include <iostream>
#include <thread>
#include <mutex>using namespace std;int ticket = 100;
mutex mtx; // 互斥锁对象void task(string name);
void sell(string name);int main(void)
{thread t1(task, "窗口1");thread t2(task, "窗口2");thread t3(task, "窗口3");t1.join();t2.join();t3.join();return 0;
}void task(string name)
{while (ticket > 0){mtx.lock(); // 加锁sell(name);mtx.unlock(); // 解锁}
}void sell(string name)
{if (ticket > 0){cout << name << "卖票,票号为:" << ticket << endl;ticket--;this_thread::sleep_for(chrono::milliseconds(100));}
}
第 8 行代码定义了互斥锁 mtx。第 26~34 行代码定义了函数 task(),在 task() 函数内部,通过对象 mtx 调用 lock() 函数,为后面的代码上锁;第 32 行代码通过对象 mtx 调用 unlock() 函数解锁。当某个线程获取互斥锁 mtx 时,该线程会为第 36~ 44 行代码上锁,即拥有了 buy() 函数的所有权,在解锁之前,其他线程不能执行 buy() 函数。
不知道为什么大部分都是只有一个窗口卖票,但是多运行几次或把 ticket 改大一些会发现其它窗口也卖票;
六、lock_guard和unique_lock
通过 mutex 的成员函数为共享资源上锁、解锁,能够保证共享资源的安全性。但是,通过 mutex 上锁之后必须要手动解锁,如果忘记解锁,当前线程会一直拥有共享资源的所有权,其他线程不得访问共享资源,造成程序错误。此外,如果程序抛出了异常,mutex 对象无法正确地析构,导致已经被上锁的共享资源无法解锁。
为此,C++ 11 标准提供了 RAII 技术的类模板:lock_guard 和 unique_lock。lock_guard 和 unique_lock 可以管理 mutex 对象,自动为共享资源上锁、解锁,不需要程序设计者手动调用 mutex 的 lock() 函数和 unlock() 函数。即使程序抛出异常,lock_guard 和 unique_lock 也能保证 mutex 对象正确解锁,在简化代码的同时,也保证了程序在异常情况下的安全性。
6.1、lock_guard
lock_guard 可以管理一个 mutex 对象,在创建 lock_guard 对象时,传入 mutex 对象作为参数。在 lock_guard 对象生命周期内,它所管理的 mutex 对象一直处于上锁状态;lock_guard 对象生命周期结束之后,它所管理的 mutex 对象也会被解锁。
- 当构造函数被调用时,该互斥量会自动被锁定。
- 当析构函数被调用时,该互斥量会自动解锁
- std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。
#include <iostream>
#include <thread>
#include <mutex>using namespace std;int ticket = 1000;
mutex mtx; // 互斥锁对象void task(string name);
void buy(string name);int main(void)
{thread t1(task, "窗口1");thread t2(task, "窗口2");thread t3(task, "窗口3");t1.join();t2.join();t3.join();return 0;
}void task(string name)
{while (ticket > 0){lock_guard<mutex> locker(mtx);buy(name);}
}void buy(string name)
{if (ticket > 0){cout << name << "卖票,票号为:" << ticket << endl;ticket--;this_thread::sleep_for(chrono::milliseconds(100));}
}
第 30 行代码创建了 lock_guard 对象 locker,传入互斥锁 mtx 作为参数,即对象 locker 管理互斥锁 mtx。当线程执行 buy() 函数时,locker 会自动完成对 buy() 函数的上锁、解锁功能。
需要注意的是,lock_guard 对象只是简化了 mutex 对象的上锁、解锁过程,但它并不负责 mutex 对象的生命周期。在上述例子中,当 buy() 函数执行结束时,lock_guard 对象 locker 析构,mutex 对象 mtx 自动解锁,线程释放 buy() 函数的所有权,但对象 mtx 的生命周期并没有结束。
6.2、unique_lock
lock_guard 只定义了构造函数和析构函数,没有定义其他成员函数,因此它的灵活性太低。为了提高锁的灵活性,C++ 11 标准提供了另外一个 RAII 技术的类模板 unique_lock。unique_lock 与 lock_guard 相似,都可以很方便地为共享资源上锁、解锁,但 unique_lock 提供了更多的成员函数,它有多个重载的构造函数,而且 unique_lock 对象支持移动构造和移动赋值。需要注意的是,unique_lock 对象不支持拷贝和赋值。
lock()函数:为共享资源上锁,如果共享资源已经被其他线程上锁,则当前线程被阻塞;如果共享资源已经被当前线程上锁,则产生死锁。try_lock()函数:尝试上锁,如果共享资源已经被其他线程上锁,该函数返回 false,当前线程继续其他任务;如果共享资源已经被当前线程上锁,则产生死锁。try_lock_for()函数:尝试在某个时间段内获取互斥锁,为共享资源上锁,如果在时间结束之前一直未获取互斥锁,则线程会一直处于阻塞状态。try_lock_until()函数:尝试在某个时间点之前获取互斥锁,为共享资源上锁,如果到达时间点之前一直未获取互斥锁,则线程会一直处于阻塞状态。unlock()函数:解锁。
#include <iostream>
#include <thread>
#include <mutex>using namespace std;int ticket = 100;
timed_mutex mtx; // 时间互斥锁对象void task(string name);
void buy(string name);int main(void)
{thread t1(task, "窗口1");thread t2(task, "窗口2");thread t3(task, "窗口3");t1.join();t2.join();t3.join();return 0;
}void task(string name)
{while (ticket > 0){// defer_lock取消自动加锁unique_lock<timed_mutex> locker(mtx, defer_lock);// 延迟加锁locker.try_lock_for(chrono::seconds(1));buy(name);}
}void buy(string name)
{if (ticket > 0){cout << name << "卖票,票号为:" << ticket << endl;ticket--;this_thread::sleep_for(chrono::milliseconds(100));}
}
七、死锁问题
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的 死锁;出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续;
#include <iostream>
#include <thread>
#include <mutex>using namespace std;mutex mutexA;
mutex mutexB;void task1(void);
void task2(void);int main(void)
{thread t1(task1);thread t2(task2);t1.join();t2.join();return 0;
}void task1(void)
{mutexA.lock();cout << "task1抢到A锁" << endl;this_thread::sleep_for(chrono::seconds(3));mutexB.lock();cout << "task1抢到B锁" << endl;mutexA.unlock();mutexB.unlock();
}void task2(void)
{mutexB.lock();cout << "task2抢到B锁" << endl;this_thread::sleep_for(chrono::seconds(3));mutexA.lock();cout << "task2抢到A锁" << endl;mutexA.unlock();mutexB.unlock();
}
此时,我们可以使用延迟加锁来解决死锁问题。
八、条件变量
在多线程编程中,多个线程可能会因为竞争资源而导致死锁,一旦产生死锁,程序将无法继续运行。为了解决死锁问题,C++ 11 标准引入了条件变量 condition_variable 类模板,用于实现线程间通信,避免产生死锁。
condition_variable 类模板定义了很多成员函数,用于实现进程通信的功能,下面介绍几个常用的成员函数。
wait() 函数:会阻塞当前线程,直到其他线程调用唤醒函数将线程唤醒。当线程被阻塞时,wait() 函数会释放互斥锁,使得被阻塞在互斥锁上的其他线程能够获取互斥锁以继续执行代码。一旦当前线程被唤醒,它就会重新夺回互斥锁。
wait() 函数有两种重载形式,函数声明分别如下所示:
void wait(unique_lock<mutex>& __lock) noexcept;
template<typename _Predicate>
void wait(unique_lock<mutex>& __lock, _Predicate __p);
第一种重载形式称为无条件阻塞,它以 mutex 对象作为参数,在调用 wait() 函数阻塞当前线程时,wait() 函数会在内部自动通过 mutex 对象调用 unlock() 函数解锁,使得阻塞在互斥锁上的其他线程恢复执行。
第二种重载形式称为有条件阻塞,它有两个参数,第一个参数是 mutex 对象,第二个参数是一个条件,只有当条件为 false 时,调用 wait() 函数才能阻塞当前线程;在收到其他线程的通知后,只有当条件为 true 时,当前线程才能被唤醒。
wait_for()函数:也用于阻塞当前线程,但它可以指定一个时间段,当收到通知或超过时间段时,线程就会被唤醒。wait_for() 函数声明如下所示:
template<typename _Rep, typename _Period>
cv_status wait_for(unique_lock<mutex>& __lock, const chrono::duration<_Rep, _Period>& __rtime);
在上述函数声明中,wait_for() 函数第一个参数为 unique_lock 对象,第二个参数为设置的时间段。函数返回值为 cv_status 类型,cv_status 是 C++ 11 标准定义的枚举类型,它有两个枚举值:no-timeout 和 timeout。no-timeout 表示没有超时,即在规定的时间段内,当前线程收到了通知;timeout 表示超时。
wait_until() 函数:可以指定一个时间点,当收到通知或超过时间点时,线程就会被唤醒。wait_until() 函数声明如下所示:
template<typename _Duration>
cv_status wait_until(unique_lock<mutex>& __lock, const chrono::time_point<__clock_t, _Duration>& __atime);
在上述函数声明中,wait_until() 函数第一个参数为 unique_lock 对象,第二个参数为设置的时间点。函数返回值为 cv_status 类型。
notify_one() 函数:用于唤醒某个被阻塞的线程。如果当前没有被阻塞的线程,则该函数什么也不做;如果有多个被阻塞的线程,则唤醒哪一个线程是随机的。notify_one() 函数声明如下所示:
void notify_one() noexcept;
在上述函数声明中,notify_one() 函数没有参数,没有返回值,并且不抛出任何异常。
notify_all() 函数:用于唤醒所有被阻塞的线程。如果当前没有被阻塞的线程,则该函数什么也不做。notify_all() 函数声明如下所示:
void notify_all() noexcept;
九、生产者消费者模型
假如有两个进程 A 和 B,它们共享一个 固定大小的缓冲区 ,A 进程产生数据放入缓冲区,B 进程从缓冲区中取出数据进行计算,那么这里其实就是一个生产者和消费者的模式,A 相当于生产者,B 相当于消费者。
在多线程开发中,如果生产者生产数据的速度很快,而消费者消费数据的速度很慢,那么生产者就必须等待消费者消费完了数据才能够继续生产数据,因为生产那么多也没有地方放;同理如果消费者的速度大于生产者那么消费者就会经常处理等待状态,所以为了达到生产者和消费者生产数据和消费数据之间的 平衡,那么就需要一个缓冲区用来存储生产者生产的数据,所以就引入了 生产者-消费者模式。
我们需要保证生产者不会在缓冲区满的时候继续向缓冲区放入数据,而消费者也不会在缓冲区空的时候,消耗数据。当缓冲区满的时候,生产者会进入休眠状态,当下次消费者开始消耗缓冲区的数据时,生产者才会被唤醒,开始往缓冲区中添加数据;当缓冲区空的时候,消费者也会进入休眠状态,直到生产者往缓冲区中添加数据时才会被唤醒。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>using namespace std;mutex mtx; // 互斥锁用于保护队列
condition_variable cv; // 条件变量用于等待和通知
queue<string> q; // 队列用于存储数据
const int capacity = 10; // 缓冲区容量
bool finished = false; // 标记生产者完成void productor(string name, string food);
void consumer(string name);int main(void)
{thread p1(productor, "星光", "包子");thread p2(productor, "冰心", "寿司");thread c1(consumer, "小樱");thread c2(consumer, "小娜");p1.join();p2.join();c1.join();c2.join();return 0;
}void productor(string name, string food)
{string data;srand(time(nullptr));for (int i = 0; i < 10; i++){this_thread::sleep_for(chrono::seconds((rand() % 3) + 1)); // 模拟延迟unique_lock<mutex> locker(mtx); // 加锁cv.wait(locker, [](){ return q.size() < capacity; }); // 等待队列不满data = "【" + name + "】生产了第 " + to_string(i + 1) + " 个 【" + food + "】";q.push(data);cout << data << endl;cv.notify_all(); // 通知消费者生产好了食物locker.unlock(); // 解锁}finished = true; // 标记生产者完成cv.notify_all(); // 通知消费者生产者已完成
}void consumer(string name)
{string data;srand(time(nullptr));while (!q.empty() || !finished){this_thread::sleep_for(chrono::seconds((rand() % 3) + 1)); // 模拟延迟unique_lock<mutex> locker(mtx); // 加锁cv.wait(locker, [](){ return !q.empty() || finished;}); // 等待队列非空或生产者未完成if (!q.empty()){data = q.front(); // 获取队头元素q.pop(); // 弹出队头元素cout << "【" << name << "】吃了:" << data << endl;}cv.notify_all(); // 通知生产者生产食物locker.unlock(); // 解锁}
}
在这个例子中,生产者线程生产数据并将其放入队列中,而消费者线程从队列中取出数据并消费。互斥锁用于保护队列,防止多个线程同时访问。条件变量用于线程的等待和通知,生产者在队列满时等待,消费者在队列空时等待。当生产者完成生产时,它会设置一个标志并通知所有消费者,消费者在队列为空且生产者已完成时退出循环。
十、原子操作
在多线程编程中,原子操作是一种不可分割的操作,它在执行过程中不会被其他线程中断。这意味着一旦开始,原子操作就会在所有其他线程看来是瞬间完成的。在 C++ 中,原子操作由 <atomic> 头文件提供,这是 C++ 11 及以后版本的一部分。原子操作对于实现无锁编程和数据结构的并发控制至关重要。它们可以确保对共享数据的操作是安全的,即使在多个线程同时访问该数据时也是如此。
C++ 提供了多种原子类型,如 atomic<int>, atomic<bool>, atomic<float> 等,以及对应的指针原子类型,如 atomic<int*>, atomic<void*> 等。这些原子类型支持一系列原子操作,包括:
store(value):将一个值存储到原子对象中。load():从原子对象中读取值。exchange(value):替换原子对象的值,并返回旧值。compare_exchange_weak(expected, value)和compare_exchange_strong(exprected, value):条件地替换原子对象的值。fetch_add(value)和fetch_sub(value):原子地增加或减少原子对象的值,并返回旧值。operator++和operator--:原子地增加或减少原子对象的值。
#include <iostream>
#include <thread>
#include <atomic>using namespace std;atomic<int> ticket(100);void sell_ticket(string name);int main(void)
{thread t1(sell_ticket, "窗口1");thread t2(sell_ticket, "窗口2");thread t3(sell_ticket, "窗口3");t1.join();t2.join();t3.join();return 0;
}void sell_ticket(string name)
{int temp = 0;while (ticket.load() > 0){if (ticket.load() > 0) // 获取当前票数{temp = ticket.fetch_sub(1); // 原子减法操作,用于减少原子变量的值,并返回原始值cout << name << "卖票,票号为:" << temp << endl;this_thread::sleep_for(chrono::milliseconds(1000));}else{break;}}
}
相关文章:
30. 并发编程
一、什么是多任务 如果一个操作系统上同时运行了多个程序,那么称这个操作系统就是 多任务的操作系统,例如:Windows、Mac、Android、IOS、Harmony 等。如果是一个程序,它可以同时执行多个事情,那么就称为 多任务的程序。…...
【包教包会】CocosCreator3.x框架——带翻页特效的场景切换
一、效果演示 二、如何获取 1、https://gitee.com/szrpf/TurnPage 2、解压,导入cocos creator(版本3.8.2),可以直接运行Demo演示 三、算法思路 1、单场景 页面预制体 通过loadScene来切换页面,无法实现页面特效。…...
k8s上面的Redis集群链接不上master的解决办法
问题描述 之前在k8s上面部署了一台node,然后创建了6个redis的pod,构建了一个redis的集群,正常运行。 最近添加了一台slave node,然后把其中的几个redis的pod调度到了slave node上面,结果集群就起不来了,…...
<项目代码>YOLOv8 瞳孔识别<目标检测>
YOLOv8是一种单阶段(one-stage)检测算法,它将目标检测问题转化为一个回归问题,能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法(如Faster R-CNN),YOLOv8具有更高的…...
网络编程-002-UDP通信
1.UDP通信的简单介绍 1.1不需要通信握手,无需维持连接,网络带宽需求较小,而实时性要求高 1.2 包大小有限制,不发大于路径MTU的数据包 1.3容易丢包 1.4 可以实现一对多,多对多 2.客户端与服务端=发送端与接收端 代码框架 收数据方一般都是客户端/接收端 3.头文件 #i…...
MySQL更换瀚高语法更换
MySQL更换瀚高语法更换 一、前言二、语句 一、前言 水一篇,mysql更换瀚高之后,一些需要更换的语法介绍 > 二、语句 MySQL瀚高MySQL用法瀚高用法说明ifnull(x,y)coalesce(x,y)相同相同用于检查两个表达式并返回第一个非空表达式。如果第一个表达式不是 NULL&…...
Object.prototype.hasOwnProperty.call(item, key) 作用与用途
在 JavaScript 中,Object.prototype.hasOwnProperty.call(item, key) 是一种检查对象 item 是否具有特定属性 key 作为自身的属性(而不是继承自原型链)的方法。这种调用方式是安全的,特别是在处理可能被修改过原型链的对象时。 解…...
DNS的10种资源记录
前言 在DNS(域名系统)中,常见的资源记录(Resource Records, RR)用于存储域名与IP地址、邮件服务器等网络资源之间的映射关系。以下是几种常见的DNS资源记录: 1. A记录(Address Record…...
【数据分享】1981-2024年我国逐日最低气温栅格数据(免费获取)
气象数据一直是一个价值很高的数据,它被广泛用于各个领域的研究当中。之前我们分享过来源于美国国家海洋和大气管理局(NOAA)下设的国家环境信息中心(NCEI)发布的1929-2024年全球站点的逐日最低气温数据(可查看之前的文章获悉详情&…...
Kafka进阶_1.生产消息
文章目录 一、Controller选举二、生产消息2.1、创建待发送数据2.2、创建生产者对象,发送数据2.3、发送回调2.3.1、异步发送2.3.2、同步发送 2.4、拦截器2.5、序列化器2.6、分区器2.7、消息可靠性2.7.1、acks 02.7.2、acks 1(默认)2.7.3、acks -1或all 2.8、部分重…...
百度世界2024:智能体引领AI应用新纪元
在近日盛大举行的百度世界2024大会上,百度创始人李彦宏以一场题为“文心一言”的精彩演讲,再次将全球科技界的目光聚焦于人工智能(AI)的无限可能。作为一名科技自媒体,我深感这场演讲不仅是对百度AI技术实力的一次全面…...
NIST 发布后量子密码学转型战略草案
美国国家标准与技术研究所 (NIST) 发布了其初步战略草案,即内部报告 (IR) 8547,标题为“向后量子密码标准过渡”。 该草案概述了 NIST 从当前易受量子计算攻击的加密算法迁移到抗量子替代算法的战略。该草案于 2024 年 11 月 12 日发布,开放…...
同向双指针
长度最小的子数组 力扣209 #define MIN(a, b) ((b) < (a) ? (b) : (a)) int minSubArrayLen(int target, int* nums, int numsSize) {int ans numsSize 1;int left 0;int right 0;int sum 0;for (right 0; right < numsSize; right){sum nums[right];while (su…...
小鹏汽车大数据面试题及参考答案
抽象类与接口的区别是什么? 抽象类是一种不能被实例化的类,它可以包含抽象方法和非抽象方法。抽象方法是没有具体实现的方法,必须在子类中被实现。抽象类主要用于为一组相关的类提供一个通用的模板,子类可以继承抽象类并实现其中的抽象方法,也可以使用抽象类中的非抽象方法…...
华为再掀技术革新!超薄膜天线设计路由器首发!
随着Wi-Fi技术的不断进步,新一代的Wi-Fi 7路由器凭借其高速率、低延迟、更稳定的性能受到了广泛关注。它能够更好地满足现代家庭对网络性能的高要求,带来更加流畅、高效的网络体验。9月24日,华为在其秋季全场景新品发布会上推出了全新Wi-Fi 7…...
CREO TOOLKIT二次开发学习之字符转换
在tk中,有很多都是可以直接强制转换的,本文章只列举字符相关的转换。 不建议使用tk官方手册的函数进行转换,因此下文均以原生c进行举例。 //double转wstring wstring a; double b; ato_wstring(b);//wstring转double wstring wstr L"…...
vmware虚拟机安装Windows11提示电脑不符合要求?
vmware虚拟机安装Win11提示电脑不符合要求? 安装问题能进入选择语言界面,请看这不能进入选择语言界面,请看这 安装问题 Vmware虚拟机安装Windows11时提示电脑不符合要求,如下: 修改了虚拟机的硬件配置还是不行&#x…...
【金融风控项目-08】:特征构造
文章目录 1.数据准备1.1 风控建模特征数据1.2 人行征信数据1.3 据之间的内在逻辑 2 样本设计和特征框架2.1 定义观察期样本2.2 数据EDA(Explore Data Analysis)2.3 梳理特征框架 3 特征构造3.1 静态信息和时间截面特征3.2 未来信息问题3.2.1 未来信息案例3.2.2 时间序列特征的未…...
计算机网络 (2)计算机网络的类别
计算机网络的类别繁多,根据不同的分类原则,可以得到各种不同类型的计算机网络。 一、按覆盖范围分类 局域网(LAN): 定义:局域网是一种在小区域内使用的,由多台计算机组成的网络。覆盖范围&#…...
10.《滑动窗口篇》---②长度最小的子数组(中等)
有了上一篇的基础。这道题我们就可以轻易分析可以使用滑动窗口来解决了 方法一:滑动窗口 这里注意 ret 在while循环外部更新 在 while 外部更新 ret,确保窗口在满足条件后再计算长度,避免错误计入正在调整中的窗口长度。 class Solution {pub…...
树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法
树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作,无需更改相机配置。但是,一…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制
在数字化浪潮席卷全球的今天,数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具,在大规模数据获取中发挥着关键作用。然而,传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时,常出现数据质…...
GitHub 趋势日报 (2025年06月06日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 590 cognee 551 onlook 399 project-based-learning 348 build-your-own-x 320 ne…...
关于easyexcel动态下拉选问题处理
前些日子突然碰到一个问题,说是客户的导入文件模版想支持部分导入内容的下拉选,于是我就找了easyexcel官网寻找解决方案,并没有找到合适的方案,没办法只能自己动手并分享出来,针对Java生成Excel下拉菜单时因选项过多导…...
Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...
从物理机到云原生:全面解析计算虚拟化技术的演进与应用
前言:我的虚拟化技术探索之旅 我最早接触"虚拟机"的概念是从Java开始的——JVM(Java Virtual Machine)让"一次编写,到处运行"成为可能。这个软件层面的虚拟化让我着迷,但直到后来接触VMware和Doc…...
es6+和css3新增的特性有哪些
一:ECMAScript 新特性(ES6) ES6 (2015) - 革命性更新 1,记住的方法,从一个方法里面用到了哪些技术 1,let /const块级作用域声明2,**默认参数**:函数参数可以设置默认值。3&#x…...
