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_guardunique_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-timeouttimeoutno-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;}}
}

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

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

相关文章

ElasticSearch学习篇17_《检索技术核心20讲》最邻近检索-局部敏感哈希、乘积量化PQ思路

目录 场景在搜索引擎和推荐引擎中&#xff0c;对相似文章去重是一个非常重要的环节&#xff0c;另外是拍照识花、摇一摇搜歌等场景都可以使用它快速检索。 基于敏感性哈希的检索更擅长处理字面上的相似而不是语义上的相似。 向量空间模型ANN检索加速思路 局部敏感哈希编码 随…

mongodb多表查询,五个表查询

需求是这样的&#xff0c;而数据是从mysql导入进来的&#xff0c;由于mysql不支持数组类型的数据&#xff0c;所以有很多关联表。药剂里找药物&#xff0c;需要药剂与药物的关联表&#xff0c;然后再找药物表。从药物表里再找药物与成分关联表&#xff0c;最后再找成分表。 这里…

《机器人控制器设计与编程》考试试卷**********大学2024~2025学年第(1)学期

消除误解&#xff0c;课程资料逐步公开。 复习资料&#xff1a; Arduino-ESP32机器人控制器设计练习题汇总_arduino编程语言 题-CSDN博客 试卷样卷&#xff1a; 开卷考试&#xff0c;时间&#xff1a; 2024年11月16日 001 002 003 004 005 ……………………装………………………

DataWorks快速入门

DataWorks基于MaxCompute、Hologres、EMR、AnalyticDB、CDP等大数据引擎&#xff0c;为数据仓库、数据湖、湖仓一体等解决方案提供统一的全链路大数据开发治理平台。本文以DataWorks的部分核心功能为例&#xff0c;指导您使用DataWorks接入数据并进行业务处理、周期调度以及数据…

0基础跟德姆(dom)一起学AI NLP自然语言处理01-自然语言处理入门

1 什么是自然语言处理 自然语言处理&#xff08;Natural Language Processing, 简称NLP&#xff09;是计算机科学与语言学中关注于计算机与人类语言间转换的领域. 2 自然语言处理的发展简史 3 自然语言处理的应用场景 语音助手机器翻译搜索引擎智能问答...

Python Matplotlib 安装指南:使用 Miniconda 实现跨 Linux、macOS 和 Windows 平台安装

Python Matplotlib 安装指南&#xff1a;使用 Miniconda 实现跨 Linux、macOS 和 Windows 平台安装 Matplotlib是Python最常用的数据可视化工具之一&#xff0c;结合Miniconda可以轻松管理安装和依赖项。在这篇文章中&#xff0c;我们将详细介绍如何使用Miniconda在Linux、mac…

Cmakelist.txt之win-c-udp-client

1.cmakelist.txt cmake_minimum_required(VERSION 3.16) ​ project(c_udp_client LANGUAGES C) ​ add_executable(c_udp_client main.c) ​ target_link_libraries(c_udp_client wsock32) ​ ​ include(GNUInstallDirs) install(TARGETS c_udp_clientLIBRARY DESTINATION $…

移动充储机器人“小奥”的多场景应用(上)

一、高速公路服务区应用 在高速公路服务区&#xff0c;新能源汽车的充电需求得到“小奥”机器人的及时响应。该机器人配备有储能电池和自动驾驶技术&#xff0c;能够迅速定位至指定充电点&#xff0c;为待充电的新能源汽车提供服务。得益于“小奥”的机动性&#xff0c;其服务…

Mono Repository方案与ReactPress的PNPM实践

ReactPress Github项目地址&#xff1a;https://github.com/fecommunity/reactpress 欢迎Star。 Mono Repository方案与ReactPress的PNPM实践 在当今软件开发领域&#xff0c;Mono Repository&#xff08;简称Monorepo&#xff09;已成为一种流行的代码管理方式&#xff0c;特…

人工智能(AI)与机器学习(ML)基础知识

目录 1. 人工智能与机器学习的核心概念 什么是人工智能&#xff08;AI&#xff09;&#xff1f; 什么是机器学习&#xff08;ML&#xff09;&#xff1f; 什么是深度学习&#xff08;DL&#xff09;&#xff1f; 2. 机器学习的三大类型 &#xff08;1&#xff09;监督式学…

ROS之什么是Node节点和Package包?

1.什么是ROS&#xff1f; 官方术语&#xff1a;ROS&#xff08;Robot Operating System&#xff0c;机器人操作系统&#xff09;是一个开源的、模块化的机器人软件框架。它为机器人开发提供了一套工具和库&#xff0c;用于实现硬件抽象、设备驱动、消息传递、多线程管理等功能…

【1.4 Getting Started--->Support Matrix】

主页&#xff1a;支持矩阵 这些支持矩阵概述了 TensorRT API、解析器和层支持的平台、特性和硬件功能。 Support Matrix Abstract 这些支持矩阵概述了 TensorRT API、解析器和层所支持的平台、功能和硬件功能。 有关之前发布的 TensorRT 文档&#xff0c;请参阅 TensorRT 档…

C语言教程指针笔记整理(二)

https://www.bilibili.com/video/BV1cx4y1d7Ut?spm_id_from333.788.videopod.episodes&vd_sourcee8984989cddeb3ef7b7e9fd89098dbe8&p107 本篇为贺宏宏老师C语言教程指针部分笔记整理 //8-19 一维数组和二维数组 // int arr[4] [][][][] //含义&#xff1a; //1.arr…

Java 对象头、Mark Word、monitor与synchronized关联关系以及synchronized锁优化

1. 对象在内存中的布局分为三块区域&#xff1a; &#xff08;1&#xff09;对象头&#xff08;Mark Word、元数据指针和数组长度&#xff09; 对象头&#xff1a;在32位虚拟机中&#xff0c;1个机器码等于4字节&#xff0c;也就是32bit&#xff0c;在64位虚拟机中&#xff0…

Consumer Group

不&#xff0c;kafka-consumer-groups.sh 脚本本身并不用于创建 Consumer Group。它主要用于管理和查看 Consumer Group 的状态和详情&#xff0c;比如列出所有的 Consumer Group、查看特定 Consumer Group 的详情、删除 Consumer Group 等。 Consumer Group 是由 Kafka 消费者…

C语言数据结构——详细讲解 双链表

从单链表到双链表&#xff1a;数据结构的演进与优化 前言一、单链表回顾二、单链表的局限性三、什么是双链表四、双链表的优势1.双向遍历2.不带头双链表的用途3.带头双链表的用途 五、双链表的操作双链表的插入操作&#xff08;一&#xff09;双链表的尾插操作&#xff08;二&a…

ElasticSearch学习篇18_《检索技术核心20讲》LevelDB设计思想

目录 一些常见的设计思想以及基于LSM树的LevelDB是如何利用这些设计思想优化存储、检索效率的。 几种常见的设计思想 索引和数据分离减少磁盘IO读写分离分层思想 LevelDB的设计思想 读写分离设计分层设计与延迟合并LRU缓存加速检索 几种常见设计思想 索引与数据分离 索引…

JavaWeb之综合案例

前言 这一节讲一个案例 1. 环境搭建 然后就是把这些数据全部用到sql语句中执行 2.查询所有-后台&前台 我们先写后台代码 2.1 后台 2.2 Dao BrandMapper&#xff1a; 注意因为数据库里面的名称是下划线分割的&#xff0c;我们类里面是驼峰的&#xff0c;所以要映射 …

043 商品详情

文章目录 详情页数据表结构voSkuItemVo.javaSkuItemSaleAttrVo.javaAttrValueAndSkuIdVo.javaSpuAttrGroupVo.javaGroupAttrParamVo.java pom.xmlSkuSaleAttrValueDao.xmlSkuSaleAttrValueDao.javaAttrGroupDao.xmlAttrGroupServiceImpl.javaSkuInfoServiceImpl.javaSkuSaleAtt…

Django启用国际化支持(2)—实现界面内切换语言:activate()

文章目录 ⭐注意⭐1. 配置项目全局设置&#xff1a;启用国际化2. 编写视图函数3. 配置路由4. 界面演示5、扩展自动识别并切换到当前语言设置语言并保存到Session设置语言并保存到 Cookie ⭐注意⭐ 以下操作依赖于 Django 项目的国际化支持。如果你不清楚如何启用国际化功能&am…