[C++11] 语言层面多线程完整解析

说明:C++11标准引入了一个新的线程库,这是C++历史上第一次在语言层面提供原生的并发编程支持。这个线程库主要包括了以下几个部分:

  • std::thread:用于创建和管理线程。
  • std::mutex 和 std::recursive_mutex:用于提供互斥锁,保护共享资源。
  • std::lock_guard 和 std::unique_lock:用于简化互斥锁的管理。
  • std::condition_variable:用于线程间的条件同步。
  • std::async:用于异步执行函数和获取函数的返回值。
  • std::future 和 std::promise:用于线程间的值传递和同步。

而在这之前,就已经出现了传统的POSIX线程(pthread),但C++11线程库与之相比,具有以下几个优势:

  • 语言级别的支持:C++11线程库是C++语言的一部分,这意味着它被集成到了语言的标准库中。这提供了一种语言级别的并发编程解决方案,使得C++程序员可以在不依赖平台特定库的情况下编写跨平台的多线程程序。以往是需要调用某个系统的API来实现,一旦更换平台,这些多线程操作都要更换API,苦不堪言。
  • 简洁的API设计:C++11线程库的API设计更加简洁和直观。例如,std::lock_guard 和 std::unique_lock 可以自动管理互斥锁的加锁和解锁,减少了出错的可能性。std::async 提供了一种简单的方法来执行函数并获取其结果,而不需要手动创建和同步线程。
  • 与STL的整合:C++11线程库与标准模板库(STL)紧密整合,提供了一致的接口和行为。例如,std::future 和 std::promise 可以用来在线程间传递值,这与STL容器的使用方式非常相似。
  • 异常安全的保证:C++11线程库在设计时考虑了异常安全。例如,std::thread 在析构时会自动加入(join)线程,确保线程正常退出,避免了资源泄露和未定义的行为。
  • 支持移动语义:C++11线程库支持移动语义,std::thread 对象可以被移动,但不能被复制。这避免了不必要的线程复制,同时保证了资源的唯一性。
  • 更好的可维护性和可读性:由于C++11线程库提供了更高级别的抽象,使用它编写的并发代码通常更加简洁、易于理解和维护。这降低了并发编程的复杂性,减少了潜在的错误。

尽管C++11线程库提供了上述优势,但需要注意的是,它并不是要取代pthread或其他并发编程库。在某些情况下,例如需要更底层的控制或者特定的平台特性时,pthread或其他库可能仍然是必要的。此外,C++11线程库在某些平台上可能不如pthread成熟或高效,因此在实际应用中需要根据具体情况进行选择。

在对C++11有线程库有一定了解,以及相比传统的POSIX线程(pthread)有一定优势后,我们再来详细了解下除此之外还有什么原因导致C++11引入新的线程库。

1 为什么引入新的线程库?

C++11引入新的线程库是为了满足现代多核和多线程编程的需求,并提供一种标准的方式来编写并发程序。以下是引入新线程库的几个主要原因:

  • 多核处理器的普及:随着多核处理器技术的普及,编写能够充分利用多核优势的并行程序变得越来越重要。C++11之前的版本并没有提供标准线程库,程序员需要依赖平台特定的库或者第三方库来实现多线程编程,这导致了代码的可移植性和维护性问题。
  • 统一并发编程接口:引入新的线程库使得C++有了统一的并发编程接口,这有助于程序员更容易地学习和使用多线程编程技术。标准库提供了线程创建、同步、互斥等基本的并发操作,使得并发编程更加简单和一致。
  • 提高代码的可移植性:通过提供标准线程库,C++11确保了跨平台的并发编程能力。程序员可以编写一次代码,然后在支持C++11的任何平台上运行,而不需要担心平台相关的线程库差异。
  • 增强程序性能:新的线程库提供了高效的线程管理和同步机制,有助于提高程序的性能。通过合理地分配任务到多个线程,可以更好地利用CPU资源,提高程序的执行效率。
  • 支持高级并发特性:C++11的线程库不仅仅是基本的线程管理,还包括了高级的并发特性,如futures、promises、std::async、锁、条件变量等。这些特性使得程序员可以更容易地编写复杂的并发程序。
  • 与其他现代编程语言接轨:许多现代编程语言都内置了对多线程和并发的支持。C++11引入线程库使得这门语言能够与其他现代编程语言保持竞争力,满足现代软件开发的需求。
  • 促进并行编程研究和发展:标准线程库的引入促进了并行编程技术的研究和发展。程序员和研究人员可以基于这一标准库开发新的并发模式和算法,推动并行编程技术的进步。

总的来说,C++11引入新的线程库是为了满足现代多核处理器环境下的并发编程需求,提供一种标准、高效、可移植的方式来编写多线程程序,同时支持并行编程技术的发展和创新。

2 C++11线程库使用详解

这里针对C++11新特性中的线程库,给出更多详细且有效的例子,说明其用法。以下是一些详细的例子,展示了如何使用C++11线程库中的不同组件。

2.1 创建和使用线程(std::thread)

参考代码如下:

#include <iostream>
#include <thread>

// 定义一个简单的函数,用于线程执行
void printThread(int id) {
    std::cout << "Thread " << id << " is running." << std::endl;
}

int main() {
    // 创建两个线程
    std::thread t1(printThread, 1);
    std::thread t2(printThread, 2);

    // 等待线程完成
    t1.join();
    t2.join();

    return 0;
}

这里我们创建了线程t1和t2,执行printThread函数。join方法用于等待线程完成执行。

2.2 使用互斥锁(std::mutex)保护共享资源

参考代码如下:

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

std::mutex mtx;
int counter = 0;

// 定义一个函数,用于线程执行,增加计数器
void increment(int n) {
    for (int i = 0; i < n; ++i) {
        std::lock_guard<std::mutex> guard(mtx);
        ++counter;
    }
}

int main() {
    std::thread t1(increment, 1000);
    std::thread t2(increment, 1000);

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

    std::cout << "Counter: " << counter << std::endl;

    return 0;
}

 这里我们使用std::mutex来保护对共享变量counter的访问,确保即使在多线程环境下,对counter的增加操作也是原子的。

2.3 使用条件变量(std::condition_variable)进行线程间的同步

参考代码如下:

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

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

// 等待条件变量
void waitForSignal() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });
    // 条件满足后执行操作
    std::cout << "Condition variable signaled." << std::endl;
}

// 发送条件变量
void sendSignal() {
    std::unique_lock<std::mutex> lock(mtx);
    ready = true;
    cv.notify_all(); // 通知所有等待的线程
}

int main() {
    std::thread waiter(waitForSignal);
    std::thread sender(sendSignal);

    waiter.join();
    sender.join();

    return 0;
}

在这个例子中,我们使用std::condition_variable来实现线程间的条件同步。一个线程等待条件变量,另一个线程在条件满足时发送信号。

2.4 使用std::async和std::future进行异步操作

参考代码如下:

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

// 一个返回值的函数
int square(int x) {
    return x * x;
}

int main() {
    // 异步执行函数
    std::future<int> result = std::async(std::launch::async, square, 10);

    // 做其他工作,直到结果准备好
    std::cout << "Waiting for result..." << std::endl;

    // 获取异步操作的结果
    std::cout << "Result: " << result.get() << std::endl;

    return 0;
}

在这个例子中,我们使用std::async来异步执行square函数,并将结果存储在std::future对象中。我们可以通过调用get方法来获取异步操作的结果。

2.5 使用std::packaged_task创建可重用的异步任务

参考代码如下:

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

// 一个接受回调的函数
void doSomethingWithCallback(int x, std::function<void(int)> callback) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    callback(x * x);
}

int main() {
    // 创建一个任务
    std::packaged_task<int(int)> task(doSomethingWithCallback);

    // 获取任务的future
    auto future = task.get_future();

    // 启动任务
    task(std::move(future), 10);

    // 等待任务完成并获取结果
    std::cout << "Result: " << future.get() << std::endl;

    return 0;
}

在这个例子中,我们使用std::packaged_task来创建一个可重用的异步任务。我们将一个函数和它的参数打包进packaged_task对象,并在需要时启动任务。

通过以上这些例子,我们可以看到C++11线程库提供了一套完整的工具,使得多线程编程变得更加简单、高效和安全。这些工具不仅简化了线程的创建和管理,还提供了强大的同步和异步操作机制,使得程序员可以更容易地编写并发程序。

3 关于C++11中多线程库的其它说明

3.1 std::lock_guard 和 std::unique_lock之间的区别

std::lock_guardstd::unique_lock都是C++标准库中定义的模板类,用于管理互斥锁(如std::mutex)。它们的主要目的是帮助程序员以一种安全和方便的方式来保护对共享资源的访问,防止多线程环境下的竞态条件。尽管它们的目的相似,但它们在使用方式和灵活性方面存在一些关键区别:

@1 std::lock_guard解读:

  1. RAII(Resource Acquisition Is Initialization)风格std::lock_guard是一个RAII风格的互斥锁包装器,它在构造时自动获取互斥锁,并在析构时自动释放锁。
  2. 不可复制std::lock_guard对象不能被复制,这确保了互斥锁不会被意外复制,导致未定义的行为。
  3. 简单使用:由于其RAII特性,std::lock_guard通常用于简单的锁保护场景,你只需要在作用域开始处创建一个std::lock_guard对象,它就会自动管理锁的生命周期。
  4. 所有权转移std::lock_guard不支持所有权转移,即不支持将互斥锁从一个std::lock_guard对象转移到另一个。

@2 std::unique_lock解读:

  1. RAII风格:与std::lock_guard类似,std::unique_lock也是一个RAII风格的互斥锁包装器,但它提供了更多的灵活性。
  2. 可复制std::unique_lock可以被复制构造和复制赋值,但它使用的是一种称为“转移所有权”的语义。这意味着当一个std::unique_lock对象被复制时,互斥锁的所有权会从原始对象转移到新对象,原始对象不再持有锁。
  3. 解锁和再锁std::unique_lock提供了lock()unlock()方法,允许程序员在需要时显式地锁定和解锁互斥锁。这使得std::unique_lock可以在更复杂的场景中使用,例如,当需要临时释放锁以等待某个条件变量时。
  4. 回收锁:由于std::unique_lock支持所有权转移,它可以在复制后回收锁,这在需要将锁的所有权从一个作用域传递到另一个作用域时非常有用。

std::lock_guard是用于简单场景的互斥锁包装器,它通过RAII确保锁的正确管理,但不提供额外的控制。而std::unique_lock提供了更多的控制和灵活性,允许锁的所有权转移和显式地锁定/解锁,适用于更复杂的并发场景。

下面是一个C++11的示例代码,展示了std::lock_guardstd::unique_lock在实际使用中的区别:

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

std::mutex mtx;

void printThread(int id) {
    // 使用 std::lock_guard 锁定互斥锁
    std::lock_guard<std::mutex> guard(mtx);
    std::cout << "Thread " << id << " is running." << std::endl;
}

void releaseLock() {
    // 使用 std::unique_lock 锁定互斥锁
    std::unique_lock<std::mutex> uniqueGuard(mtx);
    // 显式解锁,允许其他线程进入这个作用域
    uniqueGuard.unlock();
    std::cout << "Lock is released." << std::endl;
    // 等待一段时间,模拟其他线程的操作
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    // 重新加锁
    uniqueGuard.lock();
    std::cout << "Lock is acquired again." << std::endl;
}

int main() {
    std::thread t1(printThread, 1);
    std::thread t2(printThread, 2);
    std::thread t3(releaseLock);

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

    return 0;
}

在这个示例中,我们定义了两个函数printThreadreleaseLock,它们都会尝试打印一条消息到控制台。这两个函数都使用了互斥锁mtx来保护对共享资源(在这个例子中是标准输出流std::cout)的访问。

printThread函数使用std::lock_guard来自动管理互斥锁。当guard对象被创建时,它会自动锁定mtx,当guard对象超出作用域(即函数结束时)时,它会自动释放锁。这种方式简单且安全,但不允许你在锁定后释放锁。

相比之下,releaseLock函数使用std::unique_lock来提供更多的控制。首先,它锁定了互斥锁,然后显式调用unlock方法来释放锁,这允许其他线程在这个作用域中进入。在等待了一段时间后,它再次通过调用lock方法来重新获取锁。这种方式提供了更大的灵活性,允许你在需要时释放锁并稍后重新获取。

主函数main中创建了三个线程:两个用于printThread,一个用于releaseLock。这个示例演示了std::lock_guardstd::unique_lock在实际使用中的不同场景和特点。通过这个示例,我们可以看到std::unique_lock提供了更多的控制,而std::lock_guard则提供了一种简单且安全的方式来自动管理锁的生命周期。

3.2 原子操作

C++11标准引入了原子类型(<atomic>头文件中定义),以支持无锁的并发编程。原子类型提供了一种方式,可以保证在多线程环境中对共享数据的操作是原子的,即不可中断的。这意味着当一个线程正在读写原子类型变量时,其他线程不能同时进行读写操作,从而避免了竞态条件。以下是一个使用C++11原子类型的完整示例:

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

// 定义一个原子类型的计数器
std::atomic<int> counter(0);

// 一个简单的线程函数,增加计数器的值
void incrementCounter() {
    for (int i = 0; i < 1000; ++i) {
        counter++; // 原子操作,保证每次增加是原子的
    }
}

int main() {
    // 创建两个线程,同时增加计数器的值
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);

    // 等待两个线程完成
    t1.join();
    t2.join();

    // 输出最终的计数器值
    std::cout << "Counter value: " << counter << std::endl;

    return 0;
}

关于原子操作和互斥锁的解读如下:

  • 原子操作:原子类型提供了对基本操作(如自增、自减、赋值等)的原子性保证。在这个例子中,counter++操作是原子的,这意味着在任何时刻,只有一个线程能够执行这个操作。
  • 线程安全:使用原子类型可以避免在多线程环境中使用互斥锁(mutex)来保护共享数据的传统方法。原子类型提供了一种无锁的并发编程方式,通常可以提供更好的性能,尤其是在低争用的情况下。
  • 性能考虑:虽然原子操作比使用互斥锁更高效,但它们并不是完全无代价的。原子操作通常需要使用硬件的原子指令(如x86的lock前缀指令),这可能会导致缓存一致性操作,从而影响性能。因此,在高争用的情况下,原子操作的性能可能会下降。
  • 使用场景:原子类型最适合用于简单的、低争用的计数器或标志操作。对于更复杂的数据结构或操作,可能需要使用互斥锁或其他并发控制机制。

可见,C++11原子类型简化并发编程并提供一种有效的方式来保证在多线程环境中对共享数据的原子操作。

3.3 std::call_once,仅调用一次

std::call_once 是 C++11 标准库中的一个并发原语,它确保一个可调用对象(如函数、函数对象或 lambda 表达式)只被调用一次。即使有多个线程同时尝试调用它,也只有一个线程会执行该可调用对象,其他线程将等待该调用完成。这个特性在实现单例模式、初始化全局资源或执行一次性任务时非常有用。

以下是一个使用 std::call_once 的示例代码:

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

std::mutex mtx; // 全局互斥锁
bool isInitialized = false; // 初始化标志
std::function<void()> initFunc; // 初始化函数

// 初始化函数,用于设置 initFunc
void initialize() {
    // 这里可以放置一些初始化代码
    std::cout << "Initialization is running..." << std::endl;
    isInitialized = true;
    std::cout << "Initialization is complete." << std::endl;
}

// 线程函数,用于执行初始化
void threadFunction() {
    // 使用 std::call_once 确保 initialize 函数只被调用一次
    std::call_once(&initialize, mtx);
    // 初始化后,可以执行其他任务
    std::cout << "Thread is doing other work..." << std::endl;
}

int main() {
    // 存储初始化函数
    initFunc = &initialize;

    // 创建多个线程,它们都会尝试执行 initialize 函数
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);
    std::thread t3(threadFunction);

    // 等待线程完成
    t1.join();
    t2.join();
    t3.join();

    // 确保初始化完成
    if (isInitialized) {
        std::cout << "Resource has been initialized and is ready for use." << std::endl;
    }

    return 0;
}

在这个示例中,我们定义了一个全局互斥锁 mtx 和一个初始化标志 isInitialized。我们还定义了一个 initialize 函数,它将作为初始化代码的入口点。initFunc 是一个 std::function 对象,我们将它设置为指向 initialize 函数。

threadFunction 中,我们使用 std::call_once 来确保 initialize 函数只被执行一次,即使有多个线程同时尝试调用它。std::call_once 需要一个可调用对象和一个互斥锁,在这个例子中分别是 &initializemtx。当 initialize 函数被调用时,它会设置 isInitialized 标志,并执行一些模拟的初始化代码。

main 函数中,我们创建了三个线程,它们都会尝试执行 initialize 函数。由于 std::call_once 的保证,只有一个线程会实际执行初始化代码,其他线程将等待直到初始化完成。在所有线程完成后,我们检查 isInitialized 标志来确认初始化是否成功完成。

这个示例展示了 std::call_once 在多线程环境中确保某个操作只执行一次的典型使用场景。这种模式在实际开发中非常有用,特别是在需要确保全局资源或单例对象只被初始化一次的情况下。比如单例模式,首先,我们来看一个传统的单例模式的实现,它通常涉及到一个全局访问点来获取单例对象的实例。在多线程环境中,如果没有适当的同步机制,这种实现可能会导致竞态条件。

@1 传统的单例模式实现(不安全)

代码实现如下:

#include <iostream>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mutex;

    // 私有构造函数确保不能直接实例化
    Singleton() {}

    // 禁止拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 提供一个获取单例对象的静态方法
    static Singleton* getInstance() {
        if (instance == nullptr) {
            std::lock_guard<std::mutex> lock(mutex);
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }
        return instance;
    }

    void doSomething() {
        std::cout << "Doing something." << std::endl;
    }
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;

int main() {
    // 在多线程环境中,这可能会导致竞态条件
    Singleton* singleton = Singleton::getInstance();
    singleton->doSomething();
    return 0;
}

在上面的代码中,Singleton 类使用了一个静态方法 getInstance 来获取单例对象。在多线程环境中,如果多个线程同时调用 getInstance,可能会有两个线程同时检查 instance 是否为 nullptr 并都进入 if 语句,从而创建多个实例。

现在,我们使用 std::call_once 来确保初始化代码只执行一次,这样可以安全地创建单例对象,即使在多线程环境中。

@2 使用 std::call_once 的单例模式实现(安全)

代码实现如下:

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

class Singleton {
private:
    static Singleton* instance;
    static std::once_flag onceFlag;

    // 私有构造函数确保不能直接实例化
    Singleton() {}

    // 禁止拷贝构造和赋值操作
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

public:
    // 提供一个获取单例对象的静态方法
    static Singleton* getInstance() {
        std::call_once(onceFlag, []() {
            instance = new Singleton();
        });
        return instance;
    }

    void doSomething() {
        std::cout << "Doing something." << std::endl;
    }
};

// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
std::once_flag Singleton::onceFlag;

int main() {
    // 在多线程环境中,这个单例模式是安全的
    Singleton* singleton = Singleton::getInstance();
    singleton->doSomething();
    return 0;
}

在这个实现中,我们使用了 std::once_flagstd::call_once 来确保初始化代码块只被执行一次。std::once_flag 是一个特殊的标志,它只能被设置一次。std::call_once 接受这个标志和一个可调用对象(在这个例子中是一个lambda表达式),并确保这个可调用对象只被执行一次,即使有多个线程同时调用 getInstance 方法。

在多线程环境中,使用 std::call_once 的单例模式是安全的,因为 std::call_once 保证了即使多个线程同时尝试获取单例实例,初始化代码也只会执行一次。这避免了竞态条件和潜在的多个实例问题。

总结来说,传统的单例模式在多线程环境中可能不安全,因为它没有适当的同步机制来防止多个线程同时创建实例。而使用 std::call_once 可以确保初始化代码只执行一次,从而在多线程环境中实现安全的单例模式。

  • 26
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

图王大胜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值