c++11 多线程编程

一.线程库的基本使用

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

创建线程

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

#include <thread>std::thread t(function_name, args...);
`function_name`是线程入口点的函数或可调用对象

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

从一个示例的代码进行分析

#include<iostream>
#include<thread>//多线程
using namespace std;

void printHelloWorkd(string msg)
{
	for (int i = 0; i < 10000; i++)
	{
		cout << i << " ";
	}
	cout <<msg<< endl;

	return;
}

int main()
{
	//1.创建线程
	thread thread1(printHelloWorkd,"hello thread");
	bool isJoin = thread1.joinable();//判断是否可以阻塞

	if (isJoin)
	{
		thread1.join();//等待线程结束,主线程再结束
	}

	cout << "is over" << endl;
	
	
	//thread1.detach();//分离子线程
	//system("pause");
	return 0;
}
  • 创建线程后,我们可以使用t.join()等待线程完成,或者使用t.detach()分离线程,让它在后台运行。

  • joinable()

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

使用线程库常见的错误

在使用C++11线程库时,有一些常见的错误需要注意。例如:

  • 忘记等待线程完成或分离线程:如果我们创建了一个线程,但没有等待它完成或分离它,那么在主线程结束时,可能会导致未定义行为。

  • 访问共享数据时没有同步:如果我们在多个线程中访问共享数据,但没有使用同步机制,那么可能会导致数据竞争、死锁等问题。

  • 异常传递问题:如果在线程中发生了异常,但没有处理它,那么可能会导致程序崩溃。因此,我们应该在线程中使用try-catch块来捕获异常,并在适当的地方处理它。

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

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. 传递指针或引用指向局部变量的问题:

  • 错误代码
  • 这段代码再编译器运行也不会显示错误,因为x是再主线程里面,他本身和程序的生命周期是一样的
#include <iostream>
#include <thread>
void foo(int* ptr) {
    std::cout << *ptr << std::endl; // 访问已经被销毁的指针
}
int main() {
    int x = 1;
    std::thread t(foo, &x); // 传递指向局部变量的指针
    t.join();
    return 0;
}

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

解决方案是将指针或引用指向堆上的变量,或使用std::shared_ptr等智能指针来管理对象的生命周期。例如,我们可以使用new运算符在堆上分配一个整数变量,并将指针指向该变量。

  • 合理的方法
#include <iostream>
#include <thread>
void foo(int* ptr) {
    std::cout << *ptr << std::endl;
    delete ptr; // 在使用完指针后,需要手动释放内存
}
int main() {
    int* ptr = new int(1); // 在堆上分配一个整数变量
    std::thread t(foo, ptr); // 将指针传递给线程
    t.join();
    return 0;
}

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;
}
  • 由于主线程提前释放掉了ptr,此时传入线程的ptr是未知的地址。

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 仍然会保持对象的生命周期,直到线程结束。

以下是使用 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 仍然会保持对象的生命周期,直到线程结束。

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 函数。在创建线程时,需要将类对象的指针作为参数传递给线程。

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

数据共享问题

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

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

以下是一个简单的数据共享问题的示例代码

#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 < 10000; ++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;
}

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

四.互斥量死锁

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

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

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

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

死锁案例

#include<iostream>
#include<thread>
#include<mutex>
using namespace 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();
		m2.unlock();//解锁
		m1.unlock();
	}
}

int main()
{
	thread t1(func1);

	thread t2(func2);

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

	system("pause");
	return 0;
}

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

  • 修正后代码如下
#include<iostream>
#include<thread>
#include<mutex>
using namespace 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()
{
	thread t1(func1);

	thread t2(func2);

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

	system("pause");
	return 0;
}

五.lock_guard 与 std::unique_lock

lock_guard

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

std::lock_guard 的特点如下:

  • 当构造函数被调用时,该互斥量会被自动锁定。

  • 当析构函数被调用时,该互斥量会被自动解锁。

  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。

示例代码如下:

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

int shared_data = 0;
mutex mtx;//锁

void func1()
{
	for (int i = 0; i < 10000; i++)
	{
		lock_guard<mutex> lg(mtx);
		shared_data++;
	}
}

void func2()
{
	for (int i = 0; i < 10000; i++)
	{
		lock_guard<mutex> lg(mtx);
		shared_data++;
	}
}


int main()
{
	thread t1(func1);
	thread t2(func2);

	t1.join();
	t2.join();
	cout << shared_data << endl;
	

	system("pause");
	return 0;
}

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():对互斥量进行解锁操作。

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

int shared_data = 0;
mutex mtx;//锁

void func1()
{
	for (int i = 0; i < 10000; i++)
	{
		unique_lock<mutex> lg(mtx);
		shared_data++;
	}
}

void func2()
{
	for (int i = 0; i < 10000; i++)
	{
		lock_guard<mutex> lg(mtx);
		shared_data++;
	}
}


int main()
{
	thread t1(func1);
	thread t2(func2);

	t1.join();
	t2.join();
	cout << shared_data << endl;
	

	system("pause");
	return 0;
}

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

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

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

日志类

#include<iostream>
using namespace std;

class Log
{
public:
	Log(const Log& log) = delete;//禁用拷贝构造
	Log& operator=(const Log& log) = delete;//禁用赋值操作

	static Log& GetInstance()
	{
		static Log log;//懒汉模式
		return log;
	}

	void printLog(string msg)
	{
		cout <<__TIME__<< msg << endl;
	}

private:
	Log() {};
};

int main()
{
	Log::GetInstance().printLog("error");

	system("pause");
	return 0;
}

在这个单例类中,我们使用了一个静态成员函数 getInstance() 来获取单例实例,该函数使用了一个静态局部变量 instance 来存储单例实例。由于静态局部变量只会被初始化一次,因此该实现可以确保单例实例只会被创建一次。

但是,该实现并不是线程安全的。如果多个线程同时调用 getInstance() 函数,可能会导致多个对象被创建,从而违反了单例模式的要求。此外,如果多个线程同时调用 setData() 函数来修改单例对象的数据成员 m_data,可能会导致数据不一致或不正确的结果。

为了解决这些问题,我们可以使用 std::call_once 来实现一次性初始化,从而确保单例实例只会被创建一次。下面是一个使用 std::call_once 的单例实现:

class Singleton {
public:
    static Singleton& getInstance() {
            std::call_once(m_onceFlag, &Singleton::init);
            return *m_instance;
    }    
    void setData(int data) {
        m_data = data;
    }    
    int getData() const {        
    return m_data;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;    
    static void init() {
        m_instance.reset(new Singleton);
    }    
    static std::unique_ptr<Singleton> m_instance;    
    static std::once_flag m_onceFlag;    
    int m_data = 0;
};
std::unique_ptr<Singleton> Singleton::m_instance;
std::once_flag Singleton::m_onceFlag;

在这个实现中,我们使用了一个静态成员变量 m_instance 来存储单例实例,使用了一个静态成员变量 m_onceFlag 来标记初始化是否已经完成。在 getInstance() 函数中,我们使用 std::call_once 来调用 init() 函数,确保单例实例只会被创建一次。在 init() 函数中,我们使用了 std::unique_ptr 来创建单例实例。

使用 std::call_once 可以确保单例实例只会被创建一次,从而避免了多个对象被创建的问题。此外,使用 std::unique_ptr 可以确保单例实例被正确地释放,避免了内存泄漏的问题。

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 函数,而其他线程则会等待该函数执行完成。

七.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() 函数通知等待的线程。

在这里插入图片描述

生产者消费者模型

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex g_mutex;//锁
std::condition_variable g_cv;//条件变量
std::queue<int> g_queue;//任务队列

void Producer() {
    for (int i = 0; i < 10; i++) {
        {
            std::unique_lock<std::mutex> lock(g_mutex);
            g_queue.push(i);
            std::cout << "Producer: produced " << i << std::endl;
        }

        //通知一个消费者来取任务
        g_cv.notify_one();

        //进行休眠
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
void Consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(g_mutex);

        //如果队列为空,则消费者等待
        //bool isempty = g_queue.empty();
        g_cv.wait(lock, []() { return !g_queue.empty(); });
        int value = g_queue.front();
        g_queue.pop();//将任务取出
        std::cout << "Consumer: consumed " << value << std::endl;
    }
}
int main() {
    std::thread producer_thread(Producer);
    std::thread consumer_thread(Consumer);
    producer_thread.join();
    consumer_thread.join();
    return 0;
}

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

八.c++11 跨平台线程池

跨平台线程池实现

它使用 C++11 标准库中的 std::thread、std::mutex、std::condition_variable、std::function 和 std::queue 等组件实现。
在这里插入图片描述

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <queue>

class ThreadPool {
public:
    ThreadPool(int numThreads) : stop(false) {
        for (int i = 0; i < numThreads; ++i) {
            threads.emplace_back([this] {
                while (true) {
                    std::unique_lock<std::mutex> lock(mutex);
                    condition.wait(lock, [this] { return stop || !tasks.empty(); });
                    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(mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread& thread : threads) {
            thread.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(mutex);
            tasks.emplace(std::move(task));
        }
        condition.notify_one();
    }

private:
    std::vector<std::thread> threads;
    std::queue<std::function<void()>> tasks;
    std::mutex mutex;
    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;
}

在这个示例中,我们同样定义了一个 ThreadPool 类,并且在构造函数中创建了指定数目的线程。在每个线程中,我们不断地从任务队列中获取任务并执行,直到线程池被停止。在 enqueue() 函数中,我们将任务封装成一个 std::function 对象,并将它添加到任务队列中。在 ThreadPool 的析构函数中,我们等待所有线程执行完成后再停止所有线程。

在主函数中,我们创建了一个 ThreadPool 对象,并向任务队列中添加了 8 个任务。每个任务会输出一些信息,并且在执行完后等待 1 秒钟。由于线程池中有 4 个线程,因此这 8 个任务会被分配到不同的线程中执行。在任务执行完成后,程序会退出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值