C++11跨平台多线程编程与线程池

1.C++11 Thread线程库的基本使用

进程是运行中程序。线程就是进程中的进程。

C++11 Thead线程库的基本使用,包括如何创建线程、启动线程、等待线程完成以及如何分离线程等

1.1.创建线程

要创建线程,我们需要一个可调用的函数或函数对象,作为线程的入口点。在C++11中,我们可以使用函数指针、函数对象或lambda表达式来实现。创建线程的基本语法如下:

#include<thread>
std::thread t(function_name, args...);

function_name是线程入口点的函数或可调用对象

args...是传递给函数的参数

创建线程后,我们可以使用t.join()等待线程完成,或者使用t.detach()分离线程,让它在后台运行。

例如,下面的代码创建了一个线程,输出一条消息:

#include <iostream>
#include <thread>

void print_message() 
{    
	std::cout << "Hello, world!" << std::endl;
}

int main() 
{    
    std::thread t(print_message);    
    t.join();    
    return 0;
}

1.2.传递参数

我们可以使用多种方式向线程传递参数,例如使用函数参数、全局变量、引用等。如:

#include <iostream>
#include <thread>

void print_message(const std::string& message) 
{    
	std::cout << message << std::endl;
}

int main() 
{    
    std::thread t(print_message, "hello world");    
    t.join();    
    return 0;
}

1.3.等待线程完成

当我们创建一个线程后,我们可能需要等待它完成,以便获取线程的执行结果或执行清理操作。我们可以使用t.join()方法来等待线程完成。

1.4.分离线程

有时候我们可能不需要等待线程完成,而是希望它在后台运行。这时候我们可以使用t.detach()方法来分离线程。

#include <iostream>
#include <thread>

void print_message(const std::string& message) 
{    
	std::cout << message << std::endl;
}

int main() 
{    
    std::thread t(print_message, "hello world");    
    t.detach();    
    return 0;
}

在这个例子中,我们创建了一个名为t的线程,调用print_message函数输出一条消息。然后,我们使用t.detach()方法分离线程,让它在后台运行。最后,我们输出一条消息,表示线程已经被分离。

需要注意的是,一旦线程被分离,就不能再使用t.join()方法等待它完成。而且,我们需要确保线程不会在主线程结束前退出,否则可能会导致未定义行为。

1.5.joinable()

joinable()方法返回一个布尔值,如果线程可以被join()或detach(),则返回true,否则返回false。如果我们试图对一个不可加入的线程调用join()或detach(),则会抛出一个std::system_error异常。

#include <iostream>
#include <thread>
void foo() {
    std::cout << "Thread started" << std::endl;
}
int main() {
    std::thread t(foo);
    if (t.joinable()) {
        t.join();
    }
    std::cout << "Thread joined" << std::endl;
    return 0;
}

2.线程函数中的数据未定义错误

2.1.传递临时变量的问题

#include <iostream>
#include <thread>
void foo(int& x) {
    x += 1;
}
int main() {
    std::thread t(foo, 1); // 传递临时变量
    t.join();
    return 0;
}

在这个例子中,我们定义了一个名为foo的函数,它接受一个整数引用作为参数,并将该引用加1。然后,我们创建了一个名为t的线程,将foo函数以及一个临时变量1作为参数传递给它。这样会导致在线程函数执行时,临时变量1被销毁,从而导致未定义行为。

解决方案是将变量复制到一个持久的对象中,然后将该对象传递给线程。例如,我们可以将1复制到一个int类型的变量中,然后将该变量的引用传递给线程。

#include <iostream>
#include <thread>
void foo(int& x) {
    x += 1;
}
int main() {
    int x = 1; // 将变量复制到一个持久的对象中
    std::thread t(foo, std::ref(x)); // 将变量的引用传递给线程
    t.join();
    return 0;
}

2.2.传递指针或引用指向局部变量的问题

std::thread t;

void foo(int* ptr)
{
	std::cout << *ptr << std::endl;
}

void test()
{
	int x = 1;
	t = std::thread(foo, &x);
}


int main()
{
	test();
	t.join();

	return 0;
}
输出不是1而是随机数

我们定义了一个名为foo的函数,它接受一个整型指针作为参数,并输出该指针所指向的整数值。然后,我们创建了一个名为t的线程,将foo函数以及指向局部变量x的指针作为参数传递给它。这样会导致在线程函数执行时,指向局部变量x的指针已经被销毁,从而导致未定义行为。

解决方案是将指针或引用指向堆上的变量,或使用std::shared_ptr等智能指针来管理对象的生命周期。

2.3.传递指针或引用指向已释放的内存的问题

#include <iostream>
#include <thread>
void foo(int& x) {
    std::cout << x << std::endl; // 访问已经被释放的内存
}
int main() {
    int* ptr = new int(1);
    std::thread t(foo, *ptr); // 传递已经释放的内存
    delete ptr;
    t.join();
    return 0;
}

我们定义了一个名为foo的函数,它接受一个整数引用作为参数,并输出该引用的值。然后,我们创建了一个名为t的线程,将foo函数以及一个已经被释放的指针所指向的整数值作为参数传递给它解决方案是确保在线程函数执行期间,被传递的对象的生命周期是有效的。例如,在主线程中创建并初始化对象,然后将对象的引用传递给线程。

#include <iostream>
#include <thread>
void foo(int& x) {
    std::cout << x << std::endl;
}
int main() {
    int x = 1;
    std::thread t(foo, std::ref(x)); // 将变量的引用传递给线程
    t.join();
    return 0;
}

2.4. 类成员函数作为入口函数,类对象被提前释放

#include <iostream>
#include <thread>

class MyClass {
public:
    void func() {
        std::cout << "Thread " << std::this_thread::get_id() 
        << " started" << std::endl;
        // do some work
        std::cout << "Thread " << std::this_thread::get_id() 
        << " finished" << std::endl;
    }
};

int main() {
    MyClass obj;
    std::thread t(&MyClass::func, &obj);
    // obj 被提前销毁了,会导致未定义的行为
    return 0;
}

上面的代码中,在创建线程之后,obj 对象立即被销毁了,这会导致在线程执行时无法访问 obj 对象,可能会导致程序崩溃或者产生未定义的行为。

为了避免这个问题,可以使用 std::shared_ptr 来管理类对象的生命周期,确保在线程执行期间对象不会被销毁。具体来说,可以在创建线程之前,将类对象的指针封装在一个 std::shared_ptr 对象中,并将其作为参数传递给线程。这样,在线程执行期间,即使类对象的所有者释放了其所有权,std::shared_ptr 仍然会保持对象的生命周期,直到线程结束。

#include <iostream>
#include <thread>

#include <memory>

class MyClass {
public:
    void func() {
        std::cout << "Thread " << std::this_thread::get_id()
            << " started" << std::endl;
        // do some work
        std::cout << "Thread " << std::this_thread::get_id()
            << " finished" << std::endl;
    }
};

int main() {

    std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
    std::thread t(&MyClass::func, obj);
    
    t.join();
    return 0;
}

使用 std::make_shared 创建了一个 MyClass 类对象,并将其封装在一个 std::shared_ptr 对象中。然后,将 std::shared_ptr 对象作为参数传递给线程。这样,在线程执行期间,即使 obj 对象的所有者释放了其所有权,std::shared_ptr 仍然会保持对象的生命周期,直到线程结束。

2.5.入口函数为类的私有成员函数

#include <iostream>
#include <thread>

class MyClass {
private:
friend void myThreadFunc(MyClass* obj);
void privateFunc(){
std::cout << "Thread " 
<< std::this_thread::get_id() << " privateFunc" << std::endl;
}
};

void myThreadFunc(MyClass* obj) {
obj->privateFunc();
}

int main() {
MyClass obj;
std::thread thread_1(myThreadFunc, &obj);
thread_1.join();
return 0;
}

上面的代码中,将 myThreadFunc 定义为 MyClass 类的友元函数,并在函数中调用 privateFunc 函数。在创建线程时,需要将类对象的指针作为参数传递给线程。

3.互斥量解决多线程数据共享问题

数据共享问题分析

在多个线程中共享数据时,需要注意线程安全问题。如果多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,那么就会出现数据竞争问题。数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。

为了避免数据竞争问题,需要使用同步机制来确保多个线程之间对共享数据的访问是安全的。常见的同步机制包括互斥量、条件变量、原子操作等。

#include <iostream>
#include <thread>
int shared_data = 0;
void func() {
    for (int i = 0; i < 100000; ++i) {
        shared_data++;
    }
}
int main() {
    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();
    std::cout << "shared_data = " << shared_data << std::endl;    
    return 0;
}

上面的代码中,定义了一个名为 shared_data 的全局变量,并在两个线程中对其进行累加操作。在 main 函数中,创建了两个线程,并分别调用了 func 函数。在 func 函数中,对 shared_data 变量进行了累加操作。

由于 shared_data 变量是全局变量,因此在两个线程中共享。对于这种共享的情况,需要使用互斥量等同步机制来确保多个线程之间对共享数据的访问是安全的。如果不使用同步机制,就会出现数据竞争问题,导致得到错误的结果。

互斥量概念

互斥量(mutex)是一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题。

互斥量提供了两个基本操作:lock()unlock()。当一个线程调用 lock() 函数时,如果互斥量当前没有被其他线程占用,则该线程获得该互斥量的所有权,可以对共享资源进行访问。如果互斥量当前已经被其他线程占用,则调用 lock() 函数的线程会被阻塞,直到该互斥量被释放为止。

上面的代码中,使用互斥量 mtx 来确保多个线程对 shared_data 变量的访问是安全的。在 func 函数中,先调用 mtx.lock() 来获取互斥量的所有权,然后对 shared_data 变量进行累加操作,最后再调用 mtx.unlock() 来释放互斥量的所有权。这样就可以确保多个线程之间对 shared_data 变量的访问是安全的。

#include <iostream>
#include <thread>
#include <mutex>
int shared_data = 0;
std::mutex mtx;
void func(int n) {
    for (int i = 0; i < 100000; ++i) {
        mtx.lock();
        shared_data++;        
        std::cout << "Thread " << n 
        << " increment shared_data to " << shared_data << std::endl;
        mtx.unlock();
    }
}
int main() {
    std::thread t1(func, 1);
    std::thread t2(func, 2);

    t1.join();
    t2.join();    
    std::cout << "Final shared_data = " << shared_data << std::endl;    
    return 0;
}

输出值

200000

定义了一个名为 shared_data 的全局变量,并使用互斥量 mtx 来确保多个线程对其进行访问时的线程安全。在两个线程中,分别调用了 func 函数,并传递了不同的参数。在 func 函数中,先获取互斥量的所有权,然后对 shared_data 变量进行累加操作,并输出变量的当前值。最后再释放互斥量的所有权。

4.互斥量死锁

假设有两个线程 T1 和 T2,它们需要对两个互斥量 mtx1 和 mtx2 进行访问,而且需要按照以下顺序获取互斥量的所有权:

- T1 先获取 mtx1 的所有权,再获取 mtx2 的所有权。

- T2 先获取 mtx2 的所有权,再获取 mtx1 的所有权。

如果两个线程同时执行,就会出现死锁问题。因为 T1 获取了 mtx1 的所有权,但是无法获取 mtx2 的所有权,而 T2 获取了 mtx2 的所有权,但是无法获取 mtx1 的所有权,两个线程互相等待对方释放互斥量,导致死锁。

#include <iostream>
#include <thread>

#include <mutex>


std::mutex m1,m2;

void func1()
{
    for (int i=0; i<50; i++)
    {
        m1.lock();
        m2.lock();

        m1.unlock();
        m2.unlock();
    }
   
}

void func2()
{
    for (int i = 0; i < 50; i++)
    {
        m2.lock();
        m1.lock();

        m1.unlock();
        m2.unlock();
    }
 
}


int main() {

    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();
   
    std::cout << "over" << std::endl;

    return 0;
}

上述代码会出现死锁现象。

为了解决这个问题,可以让两个线程按照相同的顺序获取互斥量的所有权。例如,都先获取 mtx1 的所有权,再获取 mtx2 的所有权,或者都先获取 mtx2 的所有权,再获取 mtx1 的所有权。这样就可以避免死锁问题。

#include <iostream>
#include <thread>

#include <mutex>


std::mutex m1,m2;

void func1()
{
    for (int i=0; i<50; i++)
    {
        m1.lock();
        m2.lock();

        m1.unlock();
        m2.unlock();
    }
   
}

void func2()
{
    for (int i = 0; i < 50; i++)
    {
        m1.lock();
        m2.lock();
       
        m1.unlock();
        m2.unlock();
    }
 
}


int main() {

    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();
   
    std::cout << "over" << std::endl;

    return 0;
}

5.lock_guard 与 std::unique_lock

5.1 lock_guard

std::lock_guard 是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。

std::lock_guard 的特点如下:

  • 当构造函数被调用时,该互斥量会被自动锁定。
  • 当析构函数被调用时,该互斥量会被自动解锁。
  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。
#include <iostream>
#include <thread>

#include <mutex>


int share_data = 0;
std::mutex mtx;

void func()
{
    for (int i=0; i<10000; i++)
    {
        std::lock_guard<std::mutex> lg(mtx); //自动加解锁
        share_data++;
    }
   
}

int main() {

    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();
   
    std::cout << "shared_data:"<<share_data << std::endl;

    return 0;
}

lock_gurad源码:

template <class _Mutex>
class _NODISCARD lock_guard { // class with destructor that unlocks a mutex
public:
    using mutex_type = _Mutex;

    explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock
        _MyMutex.lock();
    }

    lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) {} // construct but don't lock

    ~lock_guard() noexcept {
        _MyMutex.unlock();
    }

    lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;

private:
    _Mutex& _MyMutex;
};

5.2 std::unique_lock

std::unique_lock 是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。

std::unique_lock 提供了以下几个成员函数:

  • lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。
  • try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回 false,否则返回 true
  • try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。
  • try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
  • unlock():对互斥量进行解锁操作。

除了上述成员函数外,std::unique_lock 还提供了以下几个构造函数:

  • unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的 std::unique_lock 对象。
  • explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量 m 进行初始化,并对该互斥量进行加锁操作。
  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,但不对该互斥量进行加锁操作。
  • unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的 std::unique_lock 对象不与任何互斥量关联。
  • unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并假设该互斥量已经被当前线程成功加锁。

std::unique_lock 使用非常灵活方便

int share_data = 0;
std::mutex mtx;

void func()
{
    for (int i=0; i<10000; i++)
    {
        std::unique_lock<std::mutex> lg(mtx, std::defer_lock); //不加锁
        share_data++;
    }
}

int main() {

    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();
   
    std::cout << "shared_data:"<<share_data << std::endl;

    return 0;
}

输出shared_data:19998 错误结果

修改手动加锁

int share_data = 0;
std::mutex mtx;

void func()
{
    for (int i=0; i<10000; i++)
    {
        std::unique_lock<std::mutex> lg(mtx, std::defer_lock); //不加锁
        lg.lock();
        share_data++;
    }
}

int main() {

    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();
   
    std::cout << "shared_data:"<<share_data << std::endl;

    return 0;
}

try_lock_for延迟加锁代码

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

int share_data = 0;
std::timed_mutex mtx;

void func()
{
    for (int i=0; i<2; i++)
    {
        std::unique_lock<std::timed_mutex> lg(mtx, std::defer_lock); //lock
        
        if (lg.try_lock_for(std::chrono::seconds(2)))
        {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            share_data++;
        }
       
    }
   
}

int main() {

    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();
   
    std::cout << "shared_data:"<<share_data << std::endl;

    return 0;
}

输出结果 shared_data:3

6.std::call_once与其使用场景

单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。

下面是一个简单的单例模式的实现:

#include <iostream>
#include <thread>
#include <mutex>
#include <string>

class Log
{
public:
    Log() {};
    Log(const Log& log) =delete;
    Log& operator=(const Log& log)=delete;

    static Log& GetInstance()
    {
        static Log *log = nullptr; 
        if (!log) log = new Log;

        return *log
    }

    void PrintLog(std::string msg)
    {
        std::cout<<__TIME__ <<" " << msg << std::endl;
    }
};

int main() {

    Log::GetInstance().PrintLog("error");
    return 0;
}

当多线程时调用时如

void print_error()
{
    Log::GetInstance().PrintLog("error");
}


int main() {

    std::thread t1(print_error);
    std::thread t2(print_error);

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

    return 0;
}

会变成多例模式

因此需要使用std::call_once` 是 C++11 标准库中的一个函数,用于确保某个函数只会被调用一次。

template<class Callable, class... Args>

void call_once(std::once_flag& flag, Callable&& func, Args&&... args);

其中,flag 是一个 std::once_flag 类型的对象,用于标记函数是否已经被调用;func 是需要被调用的函数或可调用对象;args 是函数或可调用对象的参数。

std::call_once的作用是,确保在多个线程中同时调用call_once时,只有一个线程能够成功执行func` 函数,而其他线程则会等待该函数执行完成。

使用 std::call_once 的过程中,需要注意以下几点:

  1. flag 参数必须是一个 std::once_flag 类型的对象,并且在多次调用 call_once 函数时需要使用同一个 flag 对象。

  2. func 参数是需要被调用的函数或可调用对象。该函数只会被调用一次,因此应该确保该函数是幂等的。

  3. args 参数是 func 函数或可调用对象的参数。如果 func 函数没有参数,则该参数可以省略。

  4. std::call_once 函数会抛出 std::system_error 异常,如果在调用 func 函数时发生了异常,则该异常会被传递给调用者。

#include <iostream>
#include <thread>
#include <mutex>
#include <string>

class Log
{
public:
 
    Log(const Log& log) = delete;
    Log& operator=(const Log& log) = delete;

    static Log& GetInstance()
    {
        if (!log) log = new Log();
        std::call_once(once, init);

        return *log;
    }

    void PrintLog(std::string msg)
    {
        std::cout<<__TIME__ <<" " << msg << std::endl;
    }

private:
    Log() {};
 
    static void init() {
        if (!log) log = new Log();
    }
    
    static Log* log;
    static std::once_flag once;
};

Log* Log::log = nullptr;
std::once_flag Log::once;

void print_error()
{
    Log::GetInstance().PrintLog("error");
}


int main() {

    std::thread t1(print_error);
    std::thread t2(print_error);

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

    return 0;
}

7.condition_variable 与其使用场景

std::condition_variable 的步骤如下:

  1. 创建一个 std::condition_variable 对象。

  2. 创建一个互斥锁 std::mutex 对象,用来保护共享资源的访问。

  3. 在需要等待条件变量的地方

    使用 std::unique_lock<std::mutex> 对象锁定互斥锁

    并调用 std::condition_variable::wait()std::condition_variable::wait_for()std::condition_variable::wait_until() 函数等待条件变量。

  4. 在其他线程中需要通知等待的线程时,调用 std::condition_variable::notify_one()std::condition_variable::notify_all() 函数通知等待的线程。

生产者与消费者模型

下面是一个简单的生产者-消费者模型的案例,其中使用了 std::condition_variable 来实现线程的等待和通知机制:

#include <string>

#include <condition_variable>
#include <queue>

std::queue<int> g_queue;
std::condition_variable g_cv;
std::mutex mtx;

void Producer()
{
    for (int i=0; i<10; i++)
    {
        {
            std::unique_lock<std::mutex> lock(mtx);
            g_queue.push(i);
            //通知消费者来领取任务
            g_cv.notify_one();
            std::cout << "Producer :" << i << std::endl;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void Consumer()
{
    while (true)
    {
        std::unique_lock<std::mutex> lock(mtx);

        bool isempty = g_queue.empty();
        //如果队列为空,就要等待
        g_cv.wait(lock, []() { return !g_queue.empty(); });
        int value = g_queue.front();
        g_queue.pop();

        std::cout << "Consumer :" << value << std::endl;
    }
}


int main() 
{
    std::thread producer_thread(Producer);
    std::thread consumer_thread(Consumer);
    producer_thread.join();
    consumer_thread.join();

    return 0;
}

输出

Producer :0
Consumer :0
Producer :1
Consumer :1
Producer :2
Consumer :2
Producer :3
Consumer :3
Producer :4
Consumer :4
Producer :5
Consumer :5
Producer :6
Consumer :6
Producer :7
Consumer :7
Producer :8
Consumer :8
Producer :9
Consumer :9

使用 std::condition_variable 可以实现线程的等待和通知机制,从而在多线程环境中实现同步操作。在生产者-消费者模型中,使用 std::condition_variable 可以让消费者线程等待生产者线程生产数据后再进行消费,避免了数据丢失或者数据不一致的问题。

8.C++11实现跨平台线程池

线程池:是一种线程的使用模式,它为了降低线程使用中频繁的创建和销毁所带来的资源消耗与代价。
通过创建一定数量的线程,让他们时刻准备就绪等待新任务的到达,而任务执行结束之后再重新回来继续待命。

如下图所示

image

使用 C++11 标准库中的 std::thread、std::mutex、std::condition_variable、std::function 和 std::queue 等组件实现。

#include <iostream>
#include <thread>
#include <mutex>
#include <string>

#include <condition_variable>
#include <queue>
#include<functional>

class ThreadPool {
public:
    ThreadPool(int numThread) :stop(false){
        for (int i=0;i<numThread; i++){
            //lambda表达式
            threads.emplace_back([this] {
                while (true){
                    std::unique_lock<std::mutex> lock(mtx);
                    condition.wait(lock, [this] {
                        return !tasks.empty() || stop;
                    });

                    if (stop && tasks.empty()) {
                        return;
                    }

                    //取任务
                    std::function<void()> task(std::move(tasks.front()));
                    tasks.pop();
                    lock.unlock();

                    //完成任务
                    task();
                }
                });
        }
    }

    ~ThreadPool(){
        {
            std::unique_lock<std::mutex> lock(mtx);
            stop = true;
        }
        //通知队列所有任取完
        condition.notify_all();
        //让所有任务执行完成
        for (auto& t : threads){
            t.join();
        }
    }

    //加任务
    template<typename F, typename... Args>
    void enqueue(F&& f, Args&&... args)
    {
        //函数绑定
        std::function<void()> task(std::bind(std::forward<F>(f), std::forward<Args>(args)...));

        {
            std::unique_lock<std::mutex> lock(mtx);
            tasks.emplace(std::move(task));
        }
     
        //通知线程
        condition.notify_one();
    }

private:
    std::vector<std::thread> threads;
    std::queue<std::function<void()>> tasks;

    //线程池符合生产消费者模式
    std::mutex mtx;
    std::condition_variable condition;

    bool stop;
};

int main() 
{
    ThreadPool pool(4);
    //加任务
    for (int i=0; i<8; i++)
    {
        pool.enqueue([i] {
            std::cout << "Task " << i << " is running in thread " << std::this_thread::get_id() << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "Task " << i << " is done" << std::endl;
            });
    }

    return 0;
}

输出

Task 0 is running in thread 6060
Task 2 is running in thread 17748
Task 3 is running in thread 14480
Task 1 is running in thread 18036
Task 3 is done
Task 4 is running in thread 14480
Task 1 is done
Task 5 is running in thread Task 18036
0 is doneTask
2 is doneTask
6 is running in thread Task 6060
7 is running in thread 17748
Task 6 is done
Task 7 is done
Task 5 is done
Task 4 is done

9.异步并发—— async future packaged_task promise

9.1. async 、 future

是C++11引入的一个函数模板,用于异步执行一个函数,并返回一个std::future对象,表示异步操作的结果。使用std::async可以方便地进行异步编程,避免了手动创建线程和管理线程的麻烦。下面是一个使用std::async的案例:

#include <iostream>
#include <future>

int func()
{
    int i = 0;
    for (i=0; i<1000; i++)
    {
        i++;
    }
    return i;
}

int main() 
{
    std::future<int> future_result = std::async(std::launch::async, func);

    ///相当于两个线程同时运行
    std::cout <<  func() << std::endl;
    //
    std::cout << future_result.get() << std::endl;
    return 0;
}

我们使用std::async函数异步执行了一个耗时的计算,这个计算可以在另一个线程中执行,不会阻塞主线程。同时,我们也避免了手动创建线程和管理线程的麻烦。

输出

1000
1000

9.2. packaged_task

在C++中,packaged_task是一个类模板,用于将一个可调用对象(如函数、函数对象或Lambda表达式)封装成一个异步操作,并返回一个std::future对象,表示异步操作的结果。packaged_task可以方便地将一个函数或可调用对象转换成一个异步操作,供其他线程使用。

以下是packaged_task的基本用法:

#include <iostream>
#include <future>
#include<thread>

//1. 定义可调用对象
int calculate(int x, int y) {
    return x + y;
}

int main() 
{
    //2. 创建packaged_task对象
    std::packaged_task<int(int, int)> task(calculate);
    std::future<int> future_result = task.get_future();
	//3. 在其他线程中执行异步操作
    std::thread t(std::move(task), 1, 2);
    t.join();

    //4. 获取异步操作的结果
    int result = future_result.get();
    std::cout << result << std::endl; // 输出3
    return 0;
}

在主线程中,我们可以使用future_result.get()方法获取异步操作的结果,并输出到控制台。
在这个例子中,我们成功地将一个函数calculate封装成了一个异步操作,并在其他线程中执行。通过packaged_task和future对象,我们可以方便地实现异步编程,使得代码更加简洁和易于维护。

9.3. promise

在C++中,promise是一个类模板,用于在一个线程中产生一个值,并在另一个线程中获取这个值。promise通常与future和async一起使用,用于实现异步编程。

#include <iostream>
#include <future>
#include<thread>

void func(std::promise<int>& f)
{
    f.set_value(1000);
}

int main() 
{
	//1. 创建promise对象
    std::promise<int> f;
    //2. 获取future对象
    auto future_result = f.get_future();
  
	//3. 在其他线程中设置值
    std::thread t1(func, std::ref(f));
    t1.join();
	//4. 在主线程中获取值
    std::cout << future_result.get() << std::endl;

    return 0;
}

在这个例子中,我们成功地使用promise和future对象实现了跨线程的值传递。通过promise和future对象,我们可以方便地实现异步编程,避免了手动创建线程和管理线程的麻烦。

10.std::atomic 原子操作

std::atomic 是 C++11 标准库中的一个模板类,用于实现多线程环境下的原子操作。它提供了一种线程安全的方式来访问和修改共享变量,可以避免多线程环境中的数据竞争问题。

std::atomic 的使用方式类似于普通的 C++ 变量,但是它的操作是原子性的。也就是说,在多线程环境下,多个线程同时对同一个 std::atomic 变量进行操作时,不会出现数据竞争问题。

以下是一些常用的 std::atomic 操作:

  1. load():将 std::atomic 变量的值加载到当前线程的本地缓存中,并返回这个值。

  2. store(val):将 val 的值存储到 std::atomic 变量中,并保证这个操作是原子性的。

  3. exchange(val):将 val 的值存储到 std::atomic 变量中,并返回原先的值。

  4. compare_exchange_weak(expected, val)compare_exchange_strong(expected, val):比较 std::atomic 变量的值和 expected 的值是否相同,如果相同,则将 val 的值存储到 std::atomic 变量中,并返回 true;否则,将 std::atomic 变量的值存储到 expected 中,并返回 false

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> shared_data = 0;

void func() {
    for (int i = 0; i < 1000000; ++i) {
        shared_data++;
    }
}

int main() 
{
    std::thread t1(func);
    std::thread t2(func);
    t1.join();
    t2.join();

    std::cout << "shared_data=" << shared_data << std::endl;

    return 0;
}

输出

shared_data=2000000

参考资料

C++11 跨平台多线程编程与线程池

posted @ 2023-11-25 23:42  qianglearning  阅读(571)  评论(0)    收藏  举报