海哥 发表于 5 天前

C++线程安全

线程安全

什么是线程安全

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



[*]原子性
   这点上,跟数据库事务的原子性概念差不多,即一个操作(可能含有多个子操作)要么全部执行完毕(即生效),要么全部都不执行(都不生效)
关于这个内容有个很简单的转账问题: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并没有看到,这就是可见性问题


[*]有序性
   程序执行的次序需要按照代码的先后次序执行,在多线程编程时需要思量这个问题。
示例:抢票
当多个线程同时共享同一个全局变量或静态变量(局部变量不会),并做写操作时,可能会发生数据冲突问题,也就是线程安全问题。但读操作并不会发生数据冲突问题
// 操作共享变量会有问题的抢票代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int ticket = 100;

void *route(void *arg)
{
    char *id = (char*)arg;
    while ( 1 ) {
      if ( ticket > 0 ) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
      } else {
            break;
      }
    }
}

int main( void )
{
    pthread_t t1, t2, t3, t4;

    pthread_create(&t1, NULL, route, (void*)"thread 1");
    pthread_create(&t2, NULL, route, (void*)"thread 2");
    pthread_create(&t3, NULL, route, (void*)"thread 3");
    pthread_create(&t4, NULL, route, (void*)"thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    return 0;
}

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

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

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <stdlib.h>
#include <string>
#include <unistd.h>
#include <thread>
#include <mutex>

int tickets = 100;
std::mutex mutex;

void route()
{
        //这里也采用了RAII思想
    std::lock_guard<std::mutex> lock(mutex);
    while ( 1 ) {
      //共享资源
      if ( tickets > 0 ) {
            std::cout << "get a ticket: " << tickets-- << std::endl;
            usleep(1000);
      } else {
            break;
      }
      // 抢完票的后序动作
      usleep(1000);
    }
}

int main()
{
    std::thread t1(route);
    std::thread t2(route);
    std::thread t3(route);
    std::thread t4(route);

    t1.join();
    t2.join();
    t3.join();
    t4.join();

    return 0;
}
   1.上述例子中,我们定义了一个全局互斥量 mutex 和一个共享资源 ticket。然后在route函数中,我们利用 std::lock_guard对mutex举行加锁。这样就可以保证在同一时间,只能有一个线程可以访问 ticket。
2.在 main 函数中。我们创建了四个线程 t1,t2,t3 和 t4,并让它们都执行route抢票函数操作。由于 在一个线程访问到共享资源之后,会用 mutex 对共享资源举行加锁,以是此时只有一个线程能够举行抢票(修改ticket的值),因此最终结果就是 ticket == 0。
https://i-blog.csdnimg.cn/direct/6727da40adad40e886eacce1b6276725.png
2.利用读写锁(reader-writer lock)来保护共享资源:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <stdlib.h>
#include <string>
#include <thread>
#include <shared_mutex>
#include <Windows.h>

int tickets = 100;
std::shared_mutex mutex; //全局读写锁

void route()
{
    std::unique_lock<std::shared_mutex> lock(mutex); //上写锁
    while (1) {
      //共享资源
      if (tickets > 0) {
            std::cout << "get a ticket: " << tickets-- << std::endl;
            Sleep(10);
      }
      else {
            break;
      }
      // 抢完票的后序动作
      Sleep(10);
    }
}

void readRoute()
{
    std::shared_lock<std::shared_mutex> lock(mutex); //上读锁
    std::cout << "tickets is : " << tickets << std::endl;
}

int main()
{
    std::thread t1(route);
    std::thread t2(route);
    std::thread t3(readRoute);
    std::thread t4(readRoute);

    t1.join();
    t2.join();
    t3.join();
    t4.join();

    return 0;
}
   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)使线程能够更好的协调工作:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <stdlib.h>
#include <string>
#include <thread>
#include <mutex>
#include <Windows.h>
#include <condition_variable>

int tickets = 100;
bool flag = false;
std::mutex mutex; //全局锁
std::condition_variable cv;

void route1()
{
    std::unique_lock<std::mutex> lock(mutex);
    while (1) {
      //共享资源
      if (tickets > 0) {
            std::cout << "get a ticket: " << tickets-- << std::endl;
            Sleep(10);
      }
      else {
            flag = true;//将标志符置为true,并通知线程2
            cv.notify_one();
            break;
      }
      // 抢完票的后序动作
      Sleep(10);
    }
}

void route2()
{
    std::unique_lock<std::mutex> lock(mutex);
    while (!flag)
    {
      cv.wait(lock);
    }
    std::cout << "thread 2 finished" << std::endl;
}

int main()
{
    std::thread t1(route1);
    std::thread t2(route2);

    t1.join();
    t2.join();

    return 0;
}
https://i-blog.csdnimg.cn/direct/5fd036950a004ef0ae3cae8832de722d.png
   
[*]这个例子中,我们定义了一个互斥量 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.利用信号量来保证线程安全

#include <semaphore>
#include <thread>
#include <iostream>

std::semaphore sem(5); // 创建信号量,初始值为 5

void thread_func() {
    sem.wait(); // 等待信号量的值大于 0
    std::cout << "Thread " << std::this_thread::get_id() << " is accessing the resource." << std::endl;
    // 访问共享资源
    sem.post(); // 释放信号量的值
}

int main() {
    std::thread threads; // 创建 10 个线程
    for (int i = 0; i < 10; ++i) {
      threads = std::thread(thread_func);
    }
    for (int i = 0; i < 10; ++i) {
      threads.join();
    }
    return 0;
}

   在这个示例中,我们创建了一个信号量,初始值为 5,体现共享资源可以被 5 个线程同时访问。每个线程在访问共享资源之前会等待信号量的值大于 0,如果值为 0,则会阻塞直到其他线程释放信号量的值。

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