【C++】【multithreading】chapter3 Sharing data between threads

文章详细解释了多线程编程中的竞态条件(racecondition)现象,展示了由于多个线程并发访问共享数据可能导致的不一致结果。通过一个钱包类的例子,说明了竞态条件如何导致计数错误,并提出使用互斥锁(mutex)来解决这一问题。此外,文章还介绍了操作系统中的死锁概念,阐述了产生死锁的四个必要条件,并给出了死锁的实例。
摘要由CSDN通过智能技术生成
时间:20234151803分
知识点:1. race condition,什么是race condition?为什么发生?如何解决?
        2. 什么是死锁?deadlock 的四个必要条件。
3.1 Problems with sharing data between threads.

   两个线程同时读的时候,不会任何问题,问题都出现在写上。eg:如果一个线程在写,另一个线程在读。因为写是需要步骤的,假设,写需要四步,1),2),3),4),如果写线程刚好处于第二步的时候,这时,读线程过来读数据了,这时就有可能会产生读出来的数据不对的情况。

3.1.1 Race Conditions

   什么是race condition ?
   Race condition is a kind of a bug that occurs in multithreaded applications.
   When two or more threads perform a set of operations in parallel, that access the same memory location. Also, one or more thread out of them modifies the data in that memory location, then this can lead to an unexpected results some times.
   This is called race condition.
   Race conditions are usually hard to find and reproduce because they don’t occur every time. They will occur only when relative order of execution of operations by two or more threads leads to an unexpected result. Let’s understand by an example.

3.1.2 a race condition example.

   Let’s Create a Wallet class that internally maintains money and provides a service/function i.e. addMoney(). This member function increments the internal money of wallet object by specified count.

class Wallet
{
    int mMoney;
public:
    Wallet() :mMoney(0){}
    int getMoney() { return mMoney; }
    void addMoney(int money)
    {
       for(int i = 0; i < money; ++i)
       {
          mMoney++;
       }
    }
};

   Now Let’s create 5 threads and all these threads will share a same object of class Wallet and add 1000 to internal money using it’s addMoney() member function in parallel.
   So, if initially money in wallet is 0. Then after completion of all thread’s execution money in Wallet should be 5000.
   But, as all threads are modifying the shared data at same time, it might be possible that in some scenarios money in wallet at end will be much lesser than 5000.
   let’s test it.

int testMultithreadedWallet()
{
   Wallet walletObject;
   std::vector<std::thread> threads;
   for(int i = 0; i < 5; ++i){
        threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
   }

   for(int i = 0; i < threads.size() ; i++)
   {
       threads.at(i).join();
   }
   return walletObject.getMoney();
}

int main()
{

  int val = 0;
  for(int k = 0; k < 1000; k++)
  {
     if((val = testMultithreadedWallet()) != 5000)
     {
       std::cout << "Error at count = "<<k<<" Money in Wallet = "<<val << std::endl;
     }
  }
  return 0;
}

   As addMoney() member function of same Wallet class object is executed 5 times hence it’s internal money is expected to be 5000. But as addMoney() member function is executed in parallel hence in some scenarios mMoney will be much lesser than 5000 i.e.
   The ouput is:
在这里插入图片描述
   if the error didn’t occur, run more times. finally you will get one.
   This is a race condition, as here two or more threads were trying to modify the same memory location at same time and lead to unexpected result.

3.1.3 Why this happened?

   Each thread increments the same “mMoney” member variable in parallel. Although it seems a single line but this “mMoney++” is actually converted into three machine commands,
   1. Load “mMoney” variable value in Register
   2.Increment register’s value
   3. Update variable “mMoney” with register’s value
   Now suppose in a special scenario, order of execution of above these commands is as follows,
在这里插入图片描述
   In this scenario one increment will get neglected because instead of incrementing the “mMoney” variable twice, different registers got incremented and “mMoney” variable’s value was overwritten.
   Suppose prior to this scenario mMoney was 46 and as seen in above image it is incremented 2 times, so expected result is 48. But due to race condition in above scenario final value of mMoney will be 47 only.

3.1.4 How to fix a race condition?

   using mutex(mutual exclusion). 这个mutual exclusion 真的是一目了然。
   To fix race conditions in multi-threaded environment we need mutex i.e. each thread needs to lock a mutex before modifying or reading the shared data and after modifying the data each thread should unlock the mutex.
   std::mutex
   In the C++11 threading library, the mutexes are in the header file. The class representing a mutex is the std::mutex class.
   There are two important methods of mutex class
   1. mutex.lock()
   2. mutex.unlock()

   we will see how to use std::mutex to fix the race condition in that multithreaded wallet.

   As, Wallet provides a service to add money in Wallet and same Wallet object is used between different threads, so we need to add Lock in addMoney() method of the Wallet i.e.

   Acquire lock before increment the money of Wallet and release lock before leaving that function. Let’s see the code,

   Wallet class that internally maintains money and provides a service/function i.e. addMoney().

   This member function, first acquires a lock then increments the internal money of wallet object by specified count and then releases the lock.

#include<iostream>
#include<thread>
#include<vector>
#include<mutex>

class Wallet
{
    int mMoney;
    std::mutex mutex;
public:
    Wallet() :mMoney(0){}
    int getMoney()   {     return mMoney; }
    void addMoney(int money)
    {
        mutex.lock();
        for(int i = 0; i < money; ++i)
        {
            mMoney++;
        }
        mutex.unlock();
    }
};

   Now Let’s create 5 threads and all these threads will share a same object of class Wallet and add 1000 to internal money using it’s addMoney() member function in parallel.

   So, if initially money in wallet is 0. Then after completion of all thread’s execution money in Wallet should be 5000.

   And this mutex lock guarantees that Money in the Wallet will be 5000 at end.

   let’s test it.

int testMultithreadedWallet()
{
    Wallet walletObject;
    std::vector<std::thread> threads;
    for(int i = 0; i < 5; ++i){
        threads.push_back(std::thread(&Wallet::addMoney, &walletObject, 1000));
    }

    for(int i = 0; i < threads.size() ; i++)
    {
        threads.at(i).join();
    }
    return walletObject.getMoney();
}

int main()
{

    int val = 0;
    for(int k = 0; k < 1000; k++)
    {
        if((val = testMultithreadedWallet()) != 5000)
        {
            std::cout << "Error at count = "<<k<<"  Money in Wallet = "<<val << std::endl;
            //break;
        }
    }
    return 0;
}

   It’s guaranteed that it will not found a single scenario where money in wallet is less than 5000.
   Because mutex lock in addMoney makes sure that once one thread finishes the modification of money then only any other thread modifies the money in Wallet.

   But what if we forgot to unlock the mutex at the end of function. In such scenario, one thread will exit without releasing the lock and other threads will remain in waiting.
   This kind of scenario can happen in case some exception came after locking the mutex. To avoid such scenarios we should use std::lock_guard.
   std::lock_guard
   std::lock_guard is a class template, which implements the RAII for mutex.
   It wraps the mutex inside it’s object and locks the attached mutex in its constructor. When it’s destructor is called it releases the mutex.
   当创建lock guard 对象的时候,这个对象在构造器中会自动lock mutex。
   当函数退出的时候,会自动调用 lock_guard 的destructor,在destructor中会调用unlock。
   即便是中间出现了异常,stack unwinding 也会保证destructor一定会运行,从而保证 unlock 一定会运行。

class Wallet
{
    int mMoney;
    std::mutex mutex;
public:
    Wallet() :mMoney(0){}
    int getMoney()   {     return mMoney; }
    void addMoney(int money)
    {
        std::lock_guard<std::mutex> lockGuard(mutex);
        // In constructor it locks the mutex

        for(int i = 0; i < money; ++i)
        {
            // If some exception occurs at this
            // poin then destructor of lockGuard
            // will be called due to stack unwinding.
            //
            mMoney++;
        }
        // Once function exits, then destructor
        // of lockGuard Object will be called.
        // In destructor it unlocks the mutex.
    }
 };
3.2 DeadLock
3.2.1 Introduction of deadlock

   A process in operating system uses resources in the following way.
   1. Requests a resource
   2. Use the resource
   3. Releases the resource
   A deadlock is a situation where a set of processes are blocked because each process is holding a resource and waiting for another resource acquired by some other process.
   Consider an example when two trains are coming toward each other on the same track and there is only one track, none of the trains can move once they are in front of each other. A similar situation occurs in operating systems when there are two or more processes that hold some resources and wait for resources held by other(s). For example, in the below diagram, Process 1 is holding Resource 1 and waiting for resource 2 which is acquired by process 2, and process 2 is waiting for resource 1.
在这里插入图片描述

3.2.2 Examples of deadlock

   1. The system has 2 tape drives. P1 and P2 each hold one tape drive and each needs another one.
   2. Semaphores A and B, initialized to 1, P0, and P1 are in deadlock as follows:
   P0 executes wait(A) and preempts.
   P1 executes wait(B).
   Now P0 and P1 enter in deadlock.

P0P1
wait(A);Wait(B);
Wait(B)Wait(A);

   3. Assume the space is available for allocation of 200K bytes, and the following sequence of events occurs.

P0P1
Request 80KB;Request 70KB;
Request 60KB;Request 60KB;
3.2.3 Four necessary conditions that cause deadlock(hold simultaneously)

   Mutual Exclusion: Two or more resources are non-shareable (Only one process can use at a time)
   Hold and Wait : A process is holding at least one resource and waiting for resources.
   No Preemption : A resource cannot be taken from a process unless the process releases the resource.
   Circular Wait : A set of processes waiting for each other in circular form.

参考文献:

   《C++ concurrency in action》
   https://thispointer.com/c11-multithreading-part-4-data-sharing-and-race-conditions/
   https://thispointer.com//c11-multithreading-part-5-using-mutex-to-fix-race-conditions/
   https://www.geeksforgeeks.org/introduction-of-deadlock-in-operating-system/

免责声明:

   本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值