C++线程安全

海哥  金牌会员 | 2024-11-8 13:55:44 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 881|帖子 881|积分 2643

线程安全

什么是线程安全

解释一:
   线程安满是指代码在多线程访问某个类(方法大概对象)时,这个类始终能体现出精确的举动。换种说法,如果一个类大概对象能够在多线程环境下运行,其中内容不会因为多线程的并发访问而输出错误结果或状态,那么它就是线程安全的
  解释二:
   在多线程同时对临界区资源(共享资源), 最终这个临界区资源的最终操作结果的值是精确的,那么就是线程安全,反之就是线程不安全
  线程安全的核心问题



  • 原子性
   这点上,跟数据库事务的原子性概念差不多,即一个操作(可能含有多个子操作)要么全部执行完毕(即生效),要么全部都不执行(都不生效)
关于这个内容有个很简单的转账问题:C有事需要30万,他的余额剩下10万。于是,他想向A借钱,但是A只能借10万,所幸B还能借他10万,他让A和B向他的银行账户转账。A在向C举行转账之前,读取C的余额为10万,加上他向C转账的10万,计算得出此时C的账户应该有20万,但还未来得及将结果写入,此时B的转账请求来了;B同样发现C的余额为10万,然后转入10万后并写入,此时A同样将计算的30万写入到C的余额。这种环境下,C的最终余额为20万,并非预期的30万
  

  • 可见性
   指当一个线程修改了对象的状态大概值的时间,其他线程能够同步举行看到,这称为可见性
如果此时两个线程处于差别的CPU,那么在线程1改变了 i 的值还未刷新到主存,线程2也要改变 i 的值,此时这个变量 i 的值肯定还是之前的值,线程1对变量的修改,线程2并没有看到,这就是可见性问题
  

  • 有序性
   程序执行的次序需要按照代码的先后次序执行,在多线程编程时需要思量这个问题。
  示例:抢票
当多个线程同时共享同一个全局变量或静态变量(局部变量不会),并做写操作时,可能会发生数据冲突问题,也就是线程安全问题。但读操作并不会发生数据冲突问题
  1. // 操作共享变量会有问题的抢票代码
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <unistd.h>
  6. #include <pthread.h>
  7. int ticket = 100;
  8. void *route(void *arg)
  9. {
  10.     char *id = (char*)arg;
  11.     while ( 1 ) {
  12.         if ( ticket > 0 ) {
  13.             usleep(1000);
  14.             printf("%s sells ticket:%d\n", id, ticket);
  15.             ticket--;
  16.         } else {
  17.             break;
  18.         }
  19.     }
  20. }
  21. int main( void )
  22. {
  23.     pthread_t t1, t2, t3, t4;
  24.     pthread_create(&t1, NULL, route, (void*)"thread 1");
  25.     pthread_create(&t2, NULL, route, (void*)"thread 2");
  26.     pthread_create(&t3, NULL, route, (void*)"thread 3");
  27.     pthread_create(&t4, NULL, route, (void*)"thread 4");
  28.     pthread_join(t1, NULL);
  29.     pthread_join(t2, NULL);  
  30.     pthread_join(t3, NULL);
  31.     pthread_join(t4, NULL);
  32.     return 0;
  33. }
复制代码
结果:

从这里的输出结果来看,售票数量出现了负数,体现此时票数已经出现了问题,因为不可能两个人同时在一个位置吧?这就是线程不安全导致出现的问题了。
如何保证线程安全?

在C++中,可以利用一下几种方式来确保 线程安全
   1.利用互斥量(mutex)来对临界资源(共享资源)举行保护。互斥量可以防止多个线程同时访问临界资源,从而制止数据竞争所导致的问题。
2.利用读写锁(reader-writer lock)来对共享资源举行保护。读写锁允许多个读线程同时访问共享资源,但是写线程必须独占资源。这样可以在保证线程安全的同时,尽可能的提高系统的并发性
3.利用条件变量(condition variable)来协调线程之间的协作。条件变量可以用来在线程之间传递信号,从而控制线程的执行流程。
4.利用信号量(POSIX),用于却表多个进程/线程大概差别部分的同一进程在访问共享资源的安全性。他们可以通过在差别的进程或线程之间共享来构建并发程序,从而制止竞争条件和死锁等并发编程问题。
  1.利用互斥量(mutex)来保护共享资源:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include <iostream>
  3. #include <stdlib.h>
  4. #include <string>
  5. #include <unistd.h>
  6. #include <thread>
  7. #include <mutex>
  8. int tickets = 100;
  9. std::mutex mutex;
  10. void route()
  11. {
  12.         //这里也采用了RAII思想
  13.     std::lock_guard<std::mutex> lock(mutex);
  14.     while ( 1 ) {
  15.         //共享资源
  16.         if ( tickets > 0 ) {
  17.             std::cout << "get a ticket: " << tickets-- << std::endl;
  18.             usleep(1000);
  19.         } else {
  20.             break;
  21.         }
  22.         // 抢完票的后序动作
  23.         usleep(1000);
  24.     }
  25. }
  26. int main()
  27. {
  28.     std::thread t1(route);
  29.     std::thread t2(route);
  30.     std::thread t3(route);
  31.     std::thread t4(route);
  32.     t1.join();
  33.     t2.join();
  34.     t3.join();
  35.     t4.join();
  36.     return 0;
  37. }
复制代码
  1.上述例子中,我们定义了一个全局互斥量 mutex 和一个共享资源 ticket。然后在route函数中,我们利用 std::lock_guard对mutex举行加锁。这样就可以保证在同一时间,只能有一个线程可以访问 ticket。
2.在 main 函数中。我们创建了四个线程 t1,t2,t3 和 t4,并让它们都执行route抢票函数操作。由于 在一个线程访问到共享资源之后,会用 mutex 对共享资源举行加锁,以是此时只有一个线程能够举行抢票(修改ticket的值),因此最终结果就是 ticket == 0。

  2.利用读写锁(reader-writer lock)来保护共享资源:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include <iostream>
  3. #include <stdlib.h>
  4. #include <string>
  5. #include <thread>
  6. #include <shared_mutex>
  7. #include <Windows.h>
  8. int tickets = 100;
  9. std::shared_mutex mutex; //全局读写锁
  10. void route()
  11. {
  12.     std::unique_lock<std::shared_mutex> lock(mutex); //上写锁
  13.     while (1) {
  14.         //共享资源
  15.         if (tickets > 0) {
  16.             std::cout << "get a ticket: " << tickets-- << std::endl;
  17.             Sleep(10);
  18.         }
  19.         else {
  20.             break;
  21.         }
  22.         // 抢完票的后序动作
  23.         Sleep(10);
  24.     }
  25. }
  26. void readRoute()
  27. {
  28.     std::shared_lock<std::shared_mutex> lock(mutex); //上读锁
  29.     std::cout << "tickets is : " << tickets << std::endl;
  30. }
  31. int main()
  32. {
  33.     std::thread t1(route);
  34.     std::thread t2(route);
  35.     std::thread t3(readRoute);
  36.     std::thread t4(readRoute);
  37.     t1.join();
  38.     t2.join();
  39.     t3.join();
  40.     t4.join();
  41.     return 0;
  42. }
复制代码
  1.在这个例子中,我们定义类一个读写锁和一个共享资源 tickets。 然后在 route函数中,我们利用std::unique_lock 对 mutex 举行加写锁。这时可以保证在同一时间,只能有一个写线程对 tickets 的值举行修改。
2.在readroute函数中,我们利用 std::shared_lock 对 g_mutex 举行加读锁。这样可以保证在同意是可以,能够有多个读线程可以同时读取 g_counter 的值,但是写线程必须等待全部的读线程结束后才能执行;同理,如果写线程对共享资源举行操作,此时读线程也无法获取到共享资源
  常见的读写锁操作


  • 读锁定(Read Lock):请求对共享资源的读取权限。如果没有线程持有写锁,则允许多个读线程同时得到读锁。
  • 读解锁(Read Unlock):释放读锁,当全部读锁都被释放后,写线程可以请求获取写锁。
  • 写锁定(Write Lock):请求对共享资源的写权限。写锁请求会阻塞,直到没有任何读锁或写锁被其他线程持有。
  • 写解锁(Write Unlock):释放写锁,使得其他读线程或写线程可以对资源举行访问。
3.利用条件变量(condition variable)使线程能够更好的协调工作:

  1. #define _CRT_SECURE_NO_WARNINGS 1
  2. #include <iostream>
  3. #include <stdlib.h>
  4. #include <string>
  5. #include <thread>
  6. #include <mutex>
  7. #include <Windows.h>
  8. #include <condition_variable>
  9. int tickets = 100;
  10. bool flag = false;
  11. std::mutex mutex; //全局锁
  12. std::condition_variable cv;
  13. void route1()
  14. {
  15.     std::unique_lock<std::mutex> lock(mutex);
  16.     while (1) {
  17.         //共享资源
  18.         if (tickets > 0) {
  19.             std::cout << "get a ticket: " << tickets-- << std::endl;
  20.             Sleep(10);
  21.         }
  22.         else {
  23.             flag = true;//将标志符置为true,并通知线程2
  24.             cv.notify_one();
  25.             break;
  26.         }
  27.         // 抢完票的后序动作
  28.         Sleep(10);
  29.     }
  30. }
  31. void route2()
  32. {
  33.     std::unique_lock<std::mutex> lock(mutex);
  34.     while (!flag)
  35.     {
  36.         cv.wait(lock);
  37.     }
  38.     std::cout << "thread 2 finished" << std::endl;
  39. }
  40. int main()
  41. {
  42.     std::thread t1(route1);
  43.     std::thread t2(route2);
  44.     t1.join();
  45.     t2.join();
  46.     return 0;
  47. }
复制代码

   

  • 这个例子中,我们定义了一个互斥量 mutex 和一个条件变量 cv。我们还定义了一个全局变量 flag,用于标志抢票是否完成。
    2.在线程1,在执行完抢票之后,我们将 flag 置为 true,并利用 cv_notify_one()函数来通知线程2
    3.在线程 2 中,我们利用 while (!g_flag) 循环检测 g_flag 的值。如果 flag 为 false,则利用 cv.wait(lock) 函数等待通知,否则执行后续的操作。
    4.当线程 1 通知线程 2 时,线程 2 将被唤醒,并继承往下执行。最终,线程 2 会输出 “thread 2 finished”。
    5.通过这个例子,我们可以看到,利用条件变量可以在线程间协调协作,使得线程可以根据某些条件的改变而被唤醒或等待。
  4.利用信号量来保证线程安全

  1. #include <semaphore>
  2. #include <thread>
  3. #include <iostream>
  4. std::semaphore sem(5); // 创建信号量,初始值为 5
  5. void thread_func() {
  6.     sem.wait(); // 等待信号量的值大于 0
  7.     std::cout << "Thread " << std::this_thread::get_id() << " is accessing the resource." << std::endl;
  8.     // 访问共享资源
  9.     sem.post(); // 释放信号量的值
  10. }
  11. int main() {
  12.     std::thread threads[10]; // 创建 10 个线程
  13.     for (int i = 0; i < 10; ++i) {
  14.         threads[i] = std::thread(thread_func);
  15.     }
  16.     for (int i = 0; i < 10; ++i) {
  17.         threads[i].join();
  18.     }
  19.     return 0;
  20. }
复制代码
  在这个示例中,我们创建了一个信号量,初始值为 5,体现共享资源可以被 5 个线程同时访问。每个线程在访问共享资源之前会等待信号量的值大于 0,如果值为 0,则会阻塞直到其他线程释放信号量的值。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

海哥

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表