『 C++ 』线程库

news/2024/9/14 5:48:16/ 标签: c++, 开发语言, linux

文章目录

    • 线程库
      • 线程的创建与销毁
      • 成员函数
      • this_thread 命名空间
      • 线程的引用传值
    • 互斥锁
      • 互斥锁的基本操作
      • 递归锁(可重入锁)
      • 定时互斥锁
      • 互斥锁管理器与互斥锁抛异常所引发的死锁问题
    • 条件变量
      • 条件变量的等待
      • 条件变量的唤醒
      • 两个线程交替打印奇偶数


线程库

请添加图片描述

C++标准库提供了一套完整的线程支持库,从C++11开始引入,并在后续版本中不断增强;

这些库包括用于创建和管理线程的类,以及多种并发工具,如互斥锁,条件变量,原子操作等;

C++中的线程库根据不同的操作系统平台其底层支持不同,以Linux为例用的线程库为原生pthread线程库提供,即在Linux下使用C++的线程库时必须链接对应的pthread;

g++ -o a.out main.cc -lpthread -std=c++11

pthread线程库是一个POSIX标准的线程库(POSIX指可以指操作系统接口),可适用于Linux,Unix,MacOS X等操作系统;

Windows下其也提供了一个单独的线程库,Windows下使用C++的线程库即为Windows线程库的封装;

LinuxC++线程库为对pthread线程库的封装;

语言层面和系统层面进行解耦合,在使用C++提供的线程库时只需要包含<thread>头文件即可(Linux下还是需要在编译时链接pthread库,否则无法使用);

将操作系统提供的线程库通过封装为一个类(std::thread)以面向对象;

其提供了一系列的成员函数用于线程的使用(参考 [ Link - C++ Reference thread ]);


线程的创建与销毁

请添加图片描述

C++线程库提供了一系列的构造函数用于创建线程;

  • 带参构造

    template <class Fn, class... Args>
    explicit thread (Fn&& fn, Args&&... args);
    

    线程库提供了一个万能引用与带有一个可变参数包的构造函数用于构造一个带参数的线程对象实例;

    • Fn&& fn

      该参数是一个万能引用参数,可以根据传递的数据属性自动推导其左值或是右值属性;

      这个参数表示需要传入一个可调用对象,这个可调用对象可以是仿函数,函数指针,bind绑定后的函数对象,function包装器包装后的函数对象以及Lambda表达式等;

      所传入的可调用对象将称为该线程的入口点;

    • Args&&... args

      这个可变参数包同样的是一个万能引用参数,既能够自动推导所传入参数的左右值属性,同时作为可变参数包可自动推导所传入的参数类型;

      这个可变参数包表示传入给可调用对象Fn&& fn的参数;

    void Print(int n) { // 定义一个打印函数for (int i = 0; i < n; ++i) {cout << n << " ";}cout << endl;
    }int main() {thread td1(Print, 5); // 线程 td1 调用 Print 作为可调用对象thread td2( [](int n) { // 线程 td2 调用 lambda 表达式作为可调用对象for (int i = 0; i < n; ++i) {cout << "td2" << endl;}},3);// join 用于等待线程结束td1.join();td2.join();return 0;
    }
    

    当一个线程带参构造时对应的可调用对象将成为该线程的入口点,对应的存在可调用对象时线程对象在实例化后将自行启动;

  • 无参构造

    thread() noexcept;
    

    线程库提供了一个无参构造函数用于实例化一个空的线程对象;

    空的线程对象可通过移动构造或是移动赋值将一个非空线程实例资源转移至该空线程实例上以进行使用;

    thread td; // 实例化一个空的线程对象
    

    实例化的空线程不会启动;

  • 拷贝构造

    thread (const thread&) = delete;
    

    线程的拷贝是一个危险的动作,thread线程不支持任何拷贝操作(无论是拷贝构造还是拷贝赋值);

  • 移动构造

    thread (thread&& x) noexcept;
    

    线程支持移动(移动构造或是移动拷贝)操作,即将一个将亡值线程对象中的属性资源转移给另一个线程;

    int main() {thread td1([](int n) {for (int i = 0; i < n; ++i) {cout << "td2" << endl;}},3);thread td2(move(td1)); // 移动构造thread td3;td3 = move(td2); // 移动赋值 td3.join();return 0;
    }
    

    在使用移动时必须保证接收移动资源的线程实例必须是一个空线程,否则会因为目标线程对象已经管理了一个活动线程而导致资源双重管理或丢失产生的未定义行为;

    int main() { // 未定义行为thread td1(Print, 5);thread td2([](int n) {for (int i = 0; i < n; ++i) {cout << "td2" << endl;}},3);td2 = move(td1); // 线程 td2 为非空线程td2.join();return 0;
    }
    /*运行结果为:$ ./thread terminate called without an active exceptionAborted# 运行崩溃
    */
    
  • 析构函数

    ~thread();
    

    析构函数用于销毁该线程实例;

    在调用析构函数时必须保证该线程实例是join后的,否则将存在未定义行为;


成员函数

请添加图片描述

  • thread::detach()

    void detach();
    

    该成员函数用于分离一个线程,被分离的线程不需要显式join,将会成为一个不可联结的线程;

    void Print(int n) {for (int i = 0; i < n; ++i) {cout << i << " ";}cout << endl;
    }int main() {thread t1(Print, 3);t1.detach();sleep(1);  // 防止主线程过早结束return 0;
    }
    /*运行结果:$ ./thread 0 1 2 
    */
    
  • thread::get_id()

    id get_id() const noexcept;
    

    该函数用于返回该线程的线程ID;

    其中类型idC++线程库中自定义的一个类型,为一个自定义类型;

    Linux - CentOS7中被一个哈希表所存储,其中结构体中保存着该线程的基本属性,通过重载operator<<流插入实现打印哈希表对应的key值,即线程id;

    int main() {thread t1(Print, 3);printf("printf id : %llu\n", t1.get_id());cout << "cout id : " << t1.get_id() << endl;cout << typeid(t1.get_id()).name() << endl; // 打印 id 的类型名t1.join();return 0;
    }/*运行结果:// Linux$ ./thread printf id : 140061102868224cout id : 140061102868224NSt6thread2idE0 1 2 // Windowsprintf id : 28396cout id : 28396class std::thread::id0 1 2
    */
    
  • thread::join()

    void join();
    

    该函数用于等待线程结束;

    已经被detach()的线程不能使用join进行等待,否则会出现未定义行为;

    int main() {thread t1([] { cout << "t1" << endl; });thread t2([] { cout << "t2" << endl; });t1.detach(); // t1 进行 detach// 对两个线程进行 joint1.join();t2.join();return 0;
    }/*运行结果:$ ./thread t2    // t2 正常terminate called after throwing an instance of 'std::system_error'what():  Invalid argumentAborted // t1 已经被 detach , 再次 join 时出现未定义行为
    */
    

    运行结果中t1正常运行,t2已经被detach,对t2进行join时出现未定义行为,程序异常退出;

  • thread::joinable()

    bool joinable() const noexcept;
    

    该函数用于判断该线程是否为一个可联结(未join/detach且已经启动)线程,是则返回true,否则返回false;

    int main() {thread t1([] { cout << "t1" << endl; });thread t2([] { cout << "t2" << endl; });t1.detach();cout << t1.joinable() << endl;cout << t2.joinable() << endl;t2.join();return 0;
    }
    /*运行结果:$ ./thread 01t2t1
    */
    

    运行结果t1detach后不为可联结线程返回false,t2join且未detach为一个可联结线程,返回true;

  • thread::swap()

    void swap (thread& x) noexcept;
    

    该函数用于交换两个线程;

    int main() {thread t1([] { cout << "t1" << endl; });thread t2([] { cout << "t2" << endl; });t1.swap(t2);t1.join();t2.join();return 0;
    }
    
  • operator=()

    重载了赋值操作符;

    thread& operator= (thread&& rhs) noexcept;
    thread& operator= (const thread&) = delete;
    

    不支持拷贝赋值,支持移动赋值;

    可通过其他容器,无参构造和移动赋值来实现管理多个线程;

    void Print(const string& str, int n) { cout << str << n << endl; }int main() {int n = 5;vector<thread> vths(n);for (int i = 0; i < n; ++i) {vths[i] = thread(Print, "线程", i); // 其中 thread(Print, "线程", i) 为将亡值}
    for(auto& th:vths){th.join();
    }return 0;
    }
    /*运行结果为:$ ./thread 线程1线程3线程4线程2线程0
    */
    

this_thread 命名空间

请添加图片描述

std::this_thread命名空间是C++标准库中的一个命名空间,提供了一系列与当前执行线程相关的函数;

  • this_thread::get_id()

    thread::id get_id() noexcept;
    

    该函数用于获取当前执行线程中的线程id;

    void Print(const string& str, int n) {cout << str << n << " the id : " << this_thread::get_id() << endl; // 获取当前线程 id
    }int main() {int n = 5;vector<thread> vths(n);for (int i = 0; i < n; ++i) {vths[i] = thread(Print, "线程", i);}for (auto& th : vths) {th.join();}return 0;
    }
    /*运行结果为:$ ./thread 线程3 the id : 140560757815040线程1 the id : 140560774600448线程4 the id : 140560749422336线程2 the id : 140560766207744线程0 the id : 140560782993152
    */
    
  • this_thread::sleep_for()

    template <class Rep, class Period>void sleep_for (const chrono::duration<Rep,Period>& rel_time);
    

    该函数用于阻塞当前线程一段时间;

    其中chrono为一个命名空间,提供一系列用于处理时间和时钟的工具和类型;

  • this_thread::sleep_until()

    template <class Clock, class Duration>void sleep_until (const chrono::time_point<Clock,Duration>& abs_time);
    

    该函数用于阻塞当前执行线程时间至abs_time时间点;

  • this_thread::yield()

    void yield() noexcept;
    

    该函数的作用为在多线程环境中允许当前线程主动让出处理器的执行权使得操作系统调度其他线程执行;

    当一个线程调用该函数时将会提示操作系统当前线程愿意让出处理器的时间片,允许调度器切换到另一个线程执行以提升多线程程序的整体性能和响应性;


线程的引用传值

请添加图片描述

在对线程进行引用传值时需要在传值中使用ref()以确保所传递的引用被完美转发;

因为在进行传引用传值时所传数据不会直接被线程的入口函数接收,而是首先被线程thread的构造函数接收;

使用ref以确保被构造函数接收后还能保持其左值或右值引用属性向下传递给线程的入口函数;

void Func1(mutex &mtx, int &x) {for (int i = 0; i < 10000; ++i) {mtx.lock();++x;mtx.unlock();}cout << this_thread::get_id() << " : Func1" << endl;
}int main() {mutex mtx;int x = 0;thread t1(Func1, ref(mtx), ref(x));thread t2(Func1, ref(mtx), ref(x));t1.join();t2.join();cout << x << endl;return 0;
}
/*运行结果:$ ./thread 139676657420032 : Func1139676665812736 : Func120000
*/

互斥锁

请添加图片描述

C++在引入线程库时也引入了对应的用于同步操作的锁,即mutex,用于支持多线程情况下的相关操作;

如一些需要保护临界资源避免产生竞态条件的情况;

mutex需要包含<mutex>头文件;


互斥锁的基本操作

请添加图片描述

  • 互斥锁的创建

    C++中互斥锁也被封装为一个类,在使用锁之前需要实例化一个锁对象;

    mutex mtx; // 实例化一个互斥锁对象
    
  • 加锁与解锁

    调用成员函数mutex::lock()用于互斥锁的加锁,若是互斥锁被其他线程占有则阻塞等待;

    调用成员函数mutex::unlock()用于互斥锁的解锁;

    调用成员函数mutex::try_lock()用于互斥锁的加锁,若是互斥锁被其他线程占有则调用失败函数返回;

mutex互斥锁同样的不支持拷贝操作只支持移动操作(移动构造mutex(mutex&&)与移动赋值operator=(mutex&&));

int main() {mutex mtx;size_t a = 0, b = 0, x = 0;cin >> a >> b;thread t1([a, &x,&mtx] { // 以引用的方式捕获互斥锁for (size_t i = 0; i < a; i++) {mtx.lock(); // 加锁++x;mtx.unlock(); // 解锁}});thread t2([b, &x, &mtx] {for (size_t i = 0; i < b; i++) {mtx.lock();++x;mtx.unlock();}});t1.join();t2.join();cout << x << endl;
}
/*运行结果:$ ./thread 100000 100000200000
*/

在这个例子中实例化一个互斥锁对象并且使用lockunlock成员函数用于保护临界资源从而保证线程安全;


递归锁(可重入锁)

请添加图片描述

递归锁是C++引入的一个用于避免在递归状态下导致死锁问题的锁;

class recursive_mutex;

其成员函数与mutex相同;

当一个线程在使用mutex互斥锁时将会因为重入互斥锁导致死锁问题;

void Func1(mutex &mtx, int &x) {if (!x) return;mtx.lock();cout << this_thread::get_id() << " : Func1" << endl;Func1(mtx, --x);mtx.unlock();
}int main() {mutex mtx; // 实例化一个互斥锁int x = 5; thread t1(Func1, ref(mtx), ref(x));t1.join();return 0;
}
/*运行结果:$ ./thread 139666051618560 : Func1^C // 死锁 - ctrl + C 结束程序
*/

递归锁则在递归调用过程中判断lock时的线程是不是同一个线程,若是一个持有锁的线程再次lock时会判断是否为同一个线程,为同一个线程则不再次获取锁或是等待,非同一线程则阻塞;

void Func1(recursive_mutex &rmtx, int &x) {if (!x) return;rmtx.lock();cout << this_thread::get_id() << " : Func1" << endl;Func1(rmtx, --x);rmtx.unlock();
}int main() {recursive_mutex rmtx; // 实例化一个可重入互斥锁int x = 5;thread t1(Func1, ref(rmtx), ref(x));t1.join();return 0;
}
/*运行结果:$ ./thread 140547591325440 : Func1140547591325440 : Func1140547591325440 : Func1140547591325440 : Func1140547591325440 : Func1
*/

定时互斥锁

请添加图片描述

class timed_mutex; // 定时互斥锁
class recursive_timed_mutex; // 可重入定时互斥锁

定时互斥锁与可重入定时互斥锁使用方式与互斥锁/可重入互斥锁相同;

定时获取互斥锁的方式提供了两种,分别为try_lock_fortry_lock_until,一个用于定时时间段,一个用于定时具体时间点;

这两个获取互斥锁的方式都是以try的方式,即尝试获取锁(定时互斥锁/可重入定时互斥锁),若是在对应的时间段过后或者具体时间点时该锁被其他线程占有则调用失败返回false;

通常情况下定时互斥锁用于在多线程环境中,允许线程在有限的时间内尝试获取资源而不是无限期的等待;


互斥锁管理器与互斥锁抛异常所引发的死锁问题

请添加图片描述

互斥锁在锁定后抛异常将出现死锁问题;

int main() {mutex mtx;thread t1([&mtx] {try {mtx.lock();  // 占用互斥锁throw "t1 Throw an exception";  // 抛出一个异常mtx.unlock();  // 解锁操作 - 抛出异常后该操作无法进行} catch (const char* str) {cout << str << endl;}});thread t2([&mtx] {try {mtx.lock();                  throw "t2 Throw an exception";  mtx.unlock();  } catch (const char* str) {cout << str << endl;}});t1.join();t2.join();return 0;
}
/*运行结果:$ ./thread t1 Throw an exception^C // 发生死锁
*/

在这个例子中两个线程t1t2在执行过程中必现死锁现象,原因是无论是哪个线程占有互斥锁后都会抛出一个异常跳到对应的catch位置处理异常从而忽略unlock解锁;

该问题通常需要使用RAII来解决问题;

class testGuard { // RAII public:testGuard(mutex& mtx) : _mtx(mtx) { mtx.lock(); } // 资源创建即初始化~testGuard() { _mtx.unlock(); } // private:mutex& _mtx;
};int main() {mutex mtx;thread t1([&mtx] {try {testGuard(ref(mtx));throw "t1 Throw an exception";} catch (const char* str) {cout << str << endl;}});thread t2([&mtx] {try {testGuard(ref(mtx));throw "t2 Throw an exception";} catch (const char* str) {cout << str << endl;}});t1.join();t2.join();return 0;
}

这个例子中使用了RAII的模式来管理互斥锁的生命周期以避免资源泄露和死锁问题;

当资源初始化时则锁定互斥锁,出了该作用域调用析构时则自动释放互斥锁;

在标准库中提供了对应的用于管理互斥锁生命周期的类,即std::lock_guardstd::unique_lock;

  • std::lock_guard

    该互斥锁管理器是一个轻量级的,非可重入的互斥锁管理器;

    当一个lock_guard对象被创建时将会自动尝试获取一把给定的锁;

    当该对象销毁时(通常为作用域结束)时将会自动释放该锁;

    int main() {mutex mtx;thread t1([&mtx] {try {lock_guard<mutex>(ref(mtx)); // 使用互斥锁管理器管理互斥锁的生命周期以避免抛异常后产生的死锁问题throw "t1 Throw an exception";} catch (const char* str) {cout << str << endl;}});thread t2([&mtx] {try {lock_guard<mutex>(ref(mtx));throw "t2 Throw an exception";} catch (const char* str) {cout << str << endl;}});t1.join();t2.join();return 0;
    }
    /*运行结果:$ ./thread t1 Throw an exceptiont2 Throw an exception
    */
    
  • std::unique_lock

    该互斥锁管理器比lock_guard更加灵活,提供了延迟锁定,提前解锁或显式重新锁定,并且支持不同种类的同步机制,如条件变量;

    更加灵活提供了更多功能也表示其对lock_guard更高的开销;

    同样当unique_lock生命周期结束时将会解锁与之关联的互斥锁;

    int main() {mutex mtx;thread t1([&mtx] {try {unique_lock<mutex> m(ref(mtx));m.unlock(); // 解锁m.lock(); // 重新锁定// 使用互斥锁管理器管理互斥锁的生命周期以避免抛异常后产生的死锁问题throw "t1 Throw an exception";} catch (const char* str) {cout << str << endl;}});thread t2([&mtx] {try {unique_lock<mutex>(ref(mtx));throw "t2 Throw an exception";} catch (const char* str) {cout << str << endl;}});t1.join();t2.join();return 0;
    }/*运行结果:$ ./thread t1 Throw an exceptiont2 Throw an exception
    */
    

条件变量

请添加图片描述

C++标准在引入线程库时为了支持线程间的同步操作不仅提供了互斥锁同样的也提供了条件变量std::condition_variable;

class condition_variable;

同样的条件变量只支持构造不支持拷贝操作;

default (1)	condition_variable();
copy [deleted] (2)	condition_variable (const condition_variable&) = delete;

条件变量的等待

请添加图片描述

C++的条件变量提供了三种等待(wait)的方式,分别为condition_variable::wait,condition_variable::wait_for,condition_variable::wait_until;

无论是哪种wait方式所传入的参数必须是一个使用unique_lock互斥锁管理器的互斥锁;

原因是通常情况下条件变量的使用是与互斥锁相互配合的,在判断条件变量条件时需要将互斥锁提前进行解锁;

lock_guard互斥锁管理器没有提供相应的提前解锁操作,因此在使用条件变量等待场景中必须使用unique_lock;

  • wait

    	void wait (unique_lock<mutex>& lck);template <class Predicate>void wait (unique_lock<mutex>& lck, Predicate pred);
    
    • void wait (unique_lock<mutex>& lck)

      该条件变量等待为传入一个使用unique_lock互斥锁管理器的互斥锁,当一个持有该互斥锁的线程wait时将会进行一次unlock解锁操作;

      若该线程未持有该互斥锁则直接进行wait等待,不进行其他操作;

    • void wait (unique_lock<mutex>& lck, Predicate pred)

      该条件变量等待是一个重载版本,可以根据传入的谓词pred来等待特定条件;

      该谓词再等待和唤醒时会被持续调用,以确保条件满足后才继续执行;

  • wait_for

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

      该等待操作允许线程等待指定的时间段,同时管理互斥锁和条件变量;

      这个函数将会返回一个枚举类型cv_status表明它是被唤醒(通过notify_onenotify_all)还是超时了;

    • bool wait_for (unique_lock<mutex>& lck,const chrono::duration<Rep,Period>& rel_time, Predicate pred)

      同样的该等待方式提供了一个添加传入 谓词 来检查是否满足条件的一种等待方式;

      这个版本的wait_for返回一个bool类型标识再指定时间内谓词是否变为true;

  • wait_until

    template <class Clock, class Duration>cv_status wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time);template <class Clock, class Duration, class Predicate>bool wait_until (unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time, Predicate pred);
    
    • cv_status wait_until (unique_lock<mutex>& lck,const chrono::time_point<Clock,Duration>& abs_time)

      提供了一个wait_until方法,该方法允许线程等待直到某个特定的绝对时间点;

      这个方法返回一个枚举类型表明该线程是被唤醒还是超时;

    • bool wait_until (unique_lock<mutex>& lck, const chrono::time_point<Clock,Duration>& abs_time, Predicate pred)

      提供了一个带有谓词参数的wait_until方法;

      允许线程等待直到某个特定的绝对时间点并在等待过程中检查特定条件(谓词)是否满足;

      该方法返回了一个bool类型指示再给定时间内谓词是否为true;


条件变量的唤醒

请添加图片描述

C++提供的条件变量提供了两种唤醒的方式,分别为notify_onenotify_all,分别表示唤醒条件变量等待队列中的一个线程与唤醒条件变量等待队列中的所有线程;

  • notify_one()

    void notify_one() noexcept;
    

    用于唤醒条件变量等待队列中的一个线程;

    如果等待队列中不存在任何线程则什么都不做;

  • notify_all()

    void notify_all() noexcept;
    

    用于唤醒条件变量等待队列中的所有线程;

    如果等待队列中不存在任何线程则什么都不做;

通常情况下在使用notify_all应谨慎以避免产生竞态条件与无效唤醒导致无意义的增加系统开销;


两个线程交替打印奇偶数

请添加图片描述

假设存在两个线程为t1t2,这两个线程需要交替打印奇偶数;

在使用交替打印的时候必须使用条件变量进行控制;

#include <iostream>
#include <thread>
#include <condition_variable>using namespace std;int main() {int last = 0;cin >> last;  // 获取用户输入的最后一个数condition_variable cv;  // 条件变量,用于线程同步int x = 1;              // 临界资源,表示当前计数值bool isOdd = true;      // 状态变量,用于控制线程应该打印的是奇数还是偶数mutex mtx;              // 互斥锁,保护临界资源// 线程1:用于打印奇数thread t1([&, last] {int tmp = last / 2;  // 循环次数为输入的一半for (int i = 0; i < tmp; ++i) {unique_lock<mutex> umtx(mtx);  // 使用互斥锁保护共享资源// 使用传统的while循环进行条件等待while (!isOdd) cv.wait(umtx);// 提示:你也可以直接使用带谓词的`wait`方法代替上面的while循环// cv.wait(umtx, [&] { return isOdd; });// 带谓词的`wait`方法,当lambda表达式返回`false`时线程会被阻塞cout << "t1 : " << x << endl;  // 打印当前奇数++x;  // 增加计数isOdd = false;  // 设置状态为false,下一个应该打印偶数cv.notify_one();  // 通知等待中的线程t2}});// 线程2:用于打印偶数thread t2([&, last] {int tmp = last / 2;  // 循环次数为输入的一半for (int i = 0; i < tmp; ++i) {unique_lock<mutex> umtx(mtx);  // 使用互斥锁保护共享资源// 使用传统的while循环进行条件等待while (isOdd) cv.wait(umtx);// 提示:你也可以直接使用带谓词的`wait`方法代替上面的while循环cv.wait(umtx, [&] { return !isOdd; });  // 带谓词的 wait 方法,当lambda表达式返回 false 时线程会被阻塞cout << "t2 : " << x << endl;  // 打印当前偶数++x;   // 增加计数isOdd = true;   // 设置状态为true,下一个应该打印奇数cv.notify_one();   // 通知等待中的线程t1}});t1.join();   // 等待线程t1执行完毕t2.join();   // 等待线程t2执行完毕
}
/*运行结果:$ ./thread 6t1 : 1t2 : 2t1 : 3t2 : 4t1 : 5t2 : 6
*/

在这个例子中的两个线程,t1用于打印奇数,t2用于打印偶数;

输入了一个值作为标准值,两个线程打印最终会从1开始交替打印奇偶数,最终打印至标准值;

  • 共享资源和同步机制

    • mutex mtx

      两个线程共用一个互斥锁来保护临界资源x和状态变量isOdd;

    • condition_variable cv

      用于协调两个线程对临界资源的访问,确保只有一个线程对共享资源的访问,确保每次只有一个线程能够访问;

  • 线程1(打印奇数)

    • 使用unique_lock<mutex> umtx(mtx)锁定互斥锁;
    • 使用cv.wait(umtx, [&]{ return isOdd; })等待直到isOddtrue;
    • 打印并修改临界资源后将isOdd设置为true并通知另一个线程;
  • 线程2(打印偶数)

    • 使用unique_lock<mutex> umtx(mtx)锁定互斥锁;
    • 使用cv.wait(umtx, [&]{ return isOdd; })等待直到isOddfalse;
    • 打印并修改临界资源后将isOdd设置为false并通知等待另一个线程;

这个例子中t1打印奇数t2打印偶数,如果t2先比t1拿到互斥锁,但由于条件变量条件不满足将会wait进行阻塞;

t2进行阻塞时将会释放互斥锁,t1将会占用互斥锁并通过条件变量进行一轮操作直至下一轮操作时条件变量条件不满足时将会被阻塞,但在此前的一轮操作中已经使用notify_one唤醒了另一个线程;

如此往复;


http://www.ppmy.cn/news/1516900.html

相关文章

探索 AWS Lightsail 与 EC2:如何选择适合你的云计算服务?

探索 AWS Lightsail 与 EC2&#xff1a;如何选择适合你的云计算服务&#xff1f; 随着云计算的广泛应用&#xff0c;AWS 提供了多种计算服务以满足不同的用户需求。对于初次接触 AWS 的用户来说&#xff0c;可能会在选择 AWS Lightsail 和 EC2 时感到困惑。这两者都提供了虚拟…

webpack打包优化方案

调试工具&#xff1a;安装webpack-bundle-analyzer打包可视化工具&#xff0c;可以看到打包文件大小&#xff0c;从而有针对性的优化。 npm install --save-dev webpack-bundle-analyzer。 方案一&#xff1a;将第三方依赖包使用cdn进行引入减小文件包体积&#xff08;例&…

Git的使用教程及常用语法01

git安装可以到官网上下载并安装&#xff0c;一直点点点就行 安装成功后可以在任意地方右键以终端的形式打开。 打开命令终端&#xff0c;输入git -v 查看git版本 一.配置全局用户名和邮箱 配置全局用户名&#xff1a; git config --global user.name "your username&…

利用TeamCity实现maven项目的CI/CD

1.什么是TeamCity&#xff1f; TeamCity 是一款由 JetBrains 开发的强大的持续集成&#xff08;Continuous Integration&#xff0c;CI&#xff09;和持续部署&#xff08;Continuous Deployment&#xff0c;CD&#xff09;工具。它帮助开发团队自动化构建、测试和部署过程&am…

Scratch的无限可能:突破项目大小与复杂度的界限

Scratch的无限可能&#xff1a;突破项目大小与复杂度的界限 Scratch&#xff0c;这个由麻省理工学院媒体实验室开发的编程平台&#xff0c;以其独特的图形化编程方式&#xff0c;激发了全球数百万孩子的创造力和逻辑思维能力。然而&#xff0c;随着孩子们创意的不断扩展&#…

centos7解决病毒入侵 getty

首先使用top命令查看 找到文件地址 查看是否有自启动服务 关闭、停止、删除 tmp 病毒文件删除 清除标记 [roothost-192-168-0-66 bin]# chattr -ia /tmp/newsvc.sh [roothost-192-168-0-66 bin]# chattr -ia /tmp/redis2 [roothost-192-168-0-66 bin]# chattr -ia /tmp/svc* [r…

C++开发IDE用VisualStudio好还是QtCreator好?

在熟练使用了VisualStudio和QtCreator之后,我依然认为QtCreator作为C++项目开发IDE的便捷性真的相当杰出。 当然了,VisualStudio和QtCreator本身就不是一个量级,VS越做越大,庞大的插件库也使得他能够支持从嵌入式到手机端,从web到脚本,甚至游戏,仿真等等各个领域的开发…

Leetcode 1047-删除字符串中的所有相邻重复项

给出由小写字母组成的字符串 S&#xff0c;重复项删除操作会选择两个相邻且相同的字母&#xff0c;并删除它们。 在 S 上反复执行重复项删除操作&#xff0c;直到无法继续删除。 在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。 题解 题目链接 //先进后出&a…

网络-VPN

VPN&#xff08;Virtual Private Network&#xff0c;虚拟专用网络&#xff09;是一种网络技术&#xff0c;用于在公共网络&#xff08;如互联网&#xff09;上建立一个安全的、加密的连接通道&#xff0c;以保护数据传输的安全性和隐私。通过使用 VPN&#xff0c;用户可以在不…

线程面试题

1.JDK自带的线程池有哪些&#xff1f; 2.线程池中核心线程数与最大线程数与缓冲任务队列的关系&#xff1f; 先使用核心线程执行任务。 当核心线程不足时&#xff0c;新任务入队列等待。 当队列满且线程数未达最大值时&#xff0c;增加非核心线程执行任务。 当队列满且线程…

xss-labs通关攻略 11-15关

第十一关&#xff1a;less-11 步骤一&#xff1a;利用burp抓包 步骤二&#xff1a;添加referer:click me!" type"button" οnmοuseοver"alert(/xss/)进行放包 第十二关&#xff1a;less-12 步骤一&#xff1a;利用burp抓包 步骤二&#xff1a;修改User A…

熟悉Labview工具用

目录复制 目录 0.0&#xff1a;快捷键0.1&#xff1a;全局非图标显示0.2&#xff1a;小技巧&#xff1a;图片导入为程序1.2&#xff1a;事件结构1.2.0&#xff1a;超时分支&#xff1a;当事件结构框左上角设置为1时&#xff0c;单位毫秒&#xff0c;即理解为1ms内没有其他的事件…

ReadAgent,一款具有要点记忆的人工智能阅读代理

人工智能咨询培训老师叶梓 转载标明出处 现有的大模型&#xff08;LLMs&#xff09;在处理长文本时受限于固定的最大上下文长度&#xff0c;并且当输入文本越来越长时&#xff0c;性能往往会下降&#xff0c;即使在没有超出明确上下文窗口的情况下&#xff0c;LLMs 的性能也会随…

Python爬虫技术终端协议分析与抓包应用

内容导读 分析PC客户端抓包 分析App客户端抓包 一、分析PC客户端抓包 以爬取某音乐PC客户端为例&#xff0c;实现网页内容爬取与分析。 任务描述&#xff1a; 终端协议是网络上设备之间通信规则的集合&#xff0c;通过终端协议可以获取PC客户端和App客户端数据。常用的PC…

Node.js中的pipe方法:深入解析与应用指南

在Node.js中&#xff0c;pipe方法是处理流&#xff08;Stream&#xff09;数据的一种非常高效的方式。它允许你将一个可读流&#xff08;Readable Stream&#xff09;的数据直接传输到一个可写流&#xff08;Writable Stream&#xff09;中&#xff0c;而无需手动编写读取和写入…

React 实现请求接口封装

接口封装 一、正常接口请求 useEffect(()>{//获取下拉数据async function fetchDateWrapper() {try{const res await 接口();console.log("res",res.data);const value res.data.map(item {return { label: item.valueName,value:valueCode }});setData(value…

大数据5v特性、集群、分布式

目录 数据分析六部曲 大数据的特点 &#xff08;5v特征&#xff09; 分布式与集群的区别 常用的分布式方案 数据分析六部曲 明确分析目的和思路&#xff1a;确保分析框架的体系化和逻辑性&#xff0c;简单来说就是先分析什么&#xff0c;后分析什么&#xff0c;使得各个分析…

PHPShort轻量级网址缩短程序源码开心版,内含汉化包

需要网址缩短并且想获得更多有关链接点击率和流量的数据分析&#xff0c;那么 PHPShort 可能是一个非常好的选择。PHPShort 是一款高级的 URL 缩短器平台&#xff0c;可以帮助你轻松地缩短链接&#xff0c;并根据受众群体的位置或平台来定位受众。 该程序基于 Laravel 框架编写…

不同应用场景对于HDR ISP的需求

文章目录 HDR ISP HDR ISP 需求方面应用场景具体需求动态范围宽度安全监控系统需要非常宽的动态范围&#xff0c;以应对高对比度场景&#xff0c;如强光直射、夜间监控等&#xff0c;确保在极端光照条件下获取清晰图像。汽车辅助驾驶系统&#xff08;ADAS&#xff09;要求动态…

Java学习Day33:HTML 第三章:挟魂崖

1.js的DOM介绍 JavaScript中&#xff0c;dom是一套操作文档内容的方法&#xff0c;可以完成查、增、删、改&#xff08;先删后增&#xff09;的操作。而且dom的开发效率很高&#xff0c;还可以用于服务器渲染、weex 开发等。本文向大家介绍JavaScript中的dom。 1、什么是dom&am…