[C++11] make multithreading development simple -- threads

Before C++11, C + + language did not provide support. If you want to develop multithreaded programs, you need to use the multithreaded interface provided by the operating system. However, this can not develop cross platform portable concurrent programs. C++11 provides multithreaded language support, which greatly improves the portability of programs.

1 thread

Thread is the smallest unit that the operating system can schedule operations. It is included in the process and is the actual operation unit in the process. It has the advantages of concurrency, less resources and resource sharing. Similarly, when using threads for coding, we should also pay attention to some disadvantages of multithreading, such as result differences caused by variable sharing, multithreading debugging, deadlock and many other practical problems. Therefore, we should pay special attention to multithreading coding.

1.1 creating threads

Creating a thread in C++ 11 is very simple. You can easily create a thread by using std::thread. All we need to do is provide a thread function or function object. When creating a thread, you can also specify parameters for the thread function. The code is as follows:

void foo() 
{
  std::cout<<"foo"<<std::endl;
}

void bar(int x)
{
   std::cout<<"bar::x"<<x<<std::endl;
}

int main() 
{
  std::thread first (foo); 
  std::thread second (bar,0);
  std::cout << "main, foo and bar now execute concurrently...\n";
  first.join();
  second.join();
  std::cout << "foo and bar completed.\n";
  return 0;
}

The above code creates two threads, one with parameters and the other without parameters. The output after the thread runs is as follows:

main, foo and bar now execute concurrently...
foo
bar::x=0
foo and bar completed.

However, there may be other output forms. The reason will not be explained first, and the answer will be revealed later. In this code, the created thread objects call the join method respectively. The function of this method is to block the thread until the thread function is executed. If the thread function has a return value, the return value will be ignored.

In addition to join, thread also provides another way: detach, after the thread is created, the detach method is called, the thread will be separated from the main thread, and a background thread will be programmed to execute, and the main thread will not be blocked. It should be noted that once separated, there will be no association between the two threads, and you cannot wait for the execution of thread functions through join. The method of use is as follows:

void foo() 
{
    while(1)
    {
        std::cout<<"foo"<<std::endl;
        Sleep(1000);
    }
  
}

int main() 
{
  std::thread first (foo); 
  while(1)
  {
     std::cout << "main thread.\n";
      Sleep(1000);
  }
  first.detach();
  return 0;
}

After the above code thread is created, the main thread outputs "main thread". Thread first will be separated from the main thread, execute thread functions in the background, and cross print logs with each other.

Creating a thread according to the above method is very simple, but it also has disadvantages: if std::thread is destructed before the thread function returns, unexpected errors will occur. Therefore, it is necessary to ensure that the thread function is executed before the thread is destructed. For example, you can save threads to a container. For example:

void foo() 
{
  std::cout<<"foo"<<std::endl;
}
void bar(int x)
{
   std::cout<<"bar::x="<<x<<std::endl;
}
std::vector<std::thread> g_list;
std::vector<std::shared_ptr<std::thread>> g_list2;

void CreateThread()
{
    //std::thread t(foo);
   // g_list.push_back(std::move(t));
   g_list2.push_back(std::make_shared<std::thread>(bar,2));
}

int main() 
{
 CreateThread();
 for(auto &thread : g_list)
 {
     thread.join();
 }
 for(auto &thread : g_list2)
 {
     thread->join();
 }
  return 0;
}

1.2 basic thread usage

1) Get current information: get thread ID and CPU core number

void foo() 
{
    
}

int main() 
{
  std::thread first (foo); 
  //Get thread ID
  std::cout<<first.get_id()<<std::endl;
  //Get the number of CPU cores
  std::cout<<std::thread::hardware_concurrency()<<std::endl;
  return 0;
}

2) Thread sleep

void foo() 
{
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout<<"time out"<<std::endl;
}

int main() 
{
  std::thread first (foo); 
  first.join();
  return 0;
}

2 mutex

Mutex is a means of thread synchronization to protect shared data accessed by multiple threads at the same time. In C++ 11, a variety of mutex are provided, as follows:

  • std::mutex: exclusive and mutually exclusive
  • std::timed_mutex: mutex with timeout
  • std::recursive_mutex: recursive mutex
  • std::recursive_timed_mutex: recursive mutex to be timed out

2.1 exclusive mutex

The mutex is usually blocked by the lock method. After the control right is obtained and executed, unlock is called to release it. In this process, lock and unlock need to appear in pairs. This method is synchronous. Similarly, there is an asynchronous method, try_lock, true will be returned after obtaining the mutex, and false will be returned if it is not obtained. It is non blocking. The usage of std::mutex is as follows:

std::mutex g_lock;
void foo() 
{
    g_lock.lock();
    std::cout<<"entry thread and get lock"<<std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout<<"leave thread and release lock"<<std::endl;
    g_lock.unlock();
}

int main() 
{
  std::thread first (foo); 
  std::thread second (foo);
  std::thread three (foo); 
  first.join();
  second.join();
  three.join();
  return 0;
}

The above code realizes the resource sharing of three threads through mutual exclusion. The running results are as follows:

entry thread and get lock
leave thread and release lock
entry thread and get lock
leave thread and release lock
entry thread and get lock
leave thread and release lock

Lock and unlock must appear in pairs. If they are missing, the first thread may not release the lock resource after it obtains it, resulting in the waiting of subsequent threads and the false death of the thread. To prevent this phenomenon, you can use lock_ The guard is simplified. It will obtain the lock resource during construction and release the lock resource after exceeding the life cycle. For example:

std::mutex g_lock;
void foo() 
{
    std::lock_guard<std::mutex> locker(g_lock);
    std::cout<<"entry thread and get lock"<<std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout<<"leave thread and release lock"<<std::endl;
}

2.2 recursive exclusive and mutually exclusive variables

Recursive locks are generally not recommended for the following reasons:

  • The use of recursive locks will complicate the logic and lead to more obscure problems in multi-threaded synchronization;
  • The efficiency of recursive lock is lower than that of non recursive lock;
  • Recursive lock allows the same mutex to be obtained at the same time. Calling again after a certain number of times will produce system exceptions

Therefore, in view of the above reasons, recursive locks are not recommended in the actual coding process. Here, in order to demonstrate the use of recursive locks, the example code is as follows:

struct Complex{
    std::recursive_mutex mutex;
    int i;
    Complex():i(2){};
    void mul(int x)
{
        std::lock_guard<std::recursive_mutex> lock(mutex);
        i *= x;
        std::cout<<"mul::i="<<i<<std::endl;
    }
    
    void div(int x)
{
        std::lock_guard<std::recursive_mutex> lock(mutex);
        i /= x;
        std::cout<<"div::i="<<i<<std::endl;
    }
    
    void both(int x,int y)
{
        std::lock_guard<std::recursive_mutex> lock(mutex);
        mul(x);
        div(y);
    }    
};

int main() 
{
  Complex complex;
  complex.both(4,8);
  return 0;
}

2.3 mutex with timeout

The mutex with timeout is mainly to add a timeout waiting function on the basis of the original mutex, so that you don't have to obtain the mutex all the time. In addition, if you haven't obtained the lock resource within the waiting time, you can continue to deal with other things after the timeout.

Timeout mutexes have two more interfaces than ordinary mutexes: try_lock_for and try_lock_until, the function of these two interfaces is to set the waiting timeout for obtaining the mutex. The method of use is as follows:

std::timed_mutex mtx;

void fireworks (int pid) {
  // waiting to get a lock: each thread prints "-" every 200ms:
  while (!mtx.try_lock_for(std::chrono::milliseconds(200))) {
    std::cout << "-";
  }
  // got a lock! - wait for 1s, then this thread prints "*"
  std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  std::cout << pid <<"*\n";
  mtx.unlock();
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(fireworks,i);

  for (auto& th : threads) th.join();

  return 0;
}

The above code continuously obtains the timeout lock through the while loop. If the timeout time is reached and the lock is not obtained, it will output one - - if the lock is obtained, it will sleep for 1000 milliseconds and output * and thread number. The running results of the code are as follows:

------------------------------------0*
----------------------------------------3*
-----------------------------------9*
------------------------------8*
-------------------------6*
--------------------1*
---------------4*
----------7*
-----5*
2*

Posted on Tue, 16 Nov 2021 02:52:22 -0500 by mediabox