自学内容网 自学内容网

C++11 简单手撕多线程编程

如何使用线程库

std::thread 创建线程
thread1.join(); 阻塞主线程
thread1.detach(); 线程分离

#include<iostream>
#include<thread>

void helloworld(std::string msg) {
for (int i = 0; i < 10000; i++)
{
std::cout << i << std::endl;
}
//std::cout << msg << std::endl;
return;
}

int main() {
//1. 创建线程
std::thread thread1(helloworld, "hello Tread");
//thread1.join();
//thread1.detach();
bool isJoin = thread1.joinable();
if (isJoin) 
{
thread1.join();
};
std::cout << "over" << std::endl;
}

线程中常见的数据传递错误

临时变量

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

传递指针,需要将指针或引用指向堆上的变量,或使用std::shared_ptr等智能指针来管理对象的生命周期。

#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;
}
#include <iostream>
#include <thread>
#include <memory> // 引入shared_ptr的头文件

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

int main() {
    std::shared_ptr<int> ptr = std::make_shared<int>(1); // 使用make_shared分配内存
    std::thread t(foo, ptr); // 传递shared_ptr到线程
    t.join();
    return 0;
}

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

#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)); 
    // 将变量的引用传递给线程,在线程函数执行期间,变量`x`的生命周期是有效的。
    t.join();
    return 0;
}

类对象被提前释放

#include <iostream>
#include <thread>
#include <memory>

class MyClass {
public:
    void func() {
        std::cout << "Thread " << std::this_thread::get_id() 
        << " started" << std::endl;
        
        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);
    //避免obj被提前销毁,导致未定义的行为
    t.join();
    return 0;
}

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

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

多线程数据共享

互斥锁 (mutex)

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

int shared_data = 0;
std::mutex mtx;

void func(int n) {
    for (int i = 0; i < 1000; ++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;
}

互斥量死锁

如果 T1 获取了 mtx1 的所有权,但是无法获取 mtx2 的所有权,而 T2 获取了 mtx2 的所有权,但是无法获取 mtx1 的所有权,两个线程互相等待对方释放互斥量,就会导致死锁。解决办法,需要资源顺序化,或者通过一次性尝试锁定所有指定的互斥锁,确保要么成功获取所有锁,要么释放已持有的锁并重新尝试,从而避免了死锁。

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

std::mutex mtx1, mtx2;

void func1() {
    // 锁定 mtx2,然后再锁定 mtx1
    mtx2.lock();
    std::cout << "Thread 1 locked mutex 2" << std::endl;

    mtx1.lock();
    std::cout << "Thread 1 locked mutex 1" << std::endl;

    // 解锁 mtx1 和 mtx2
    mtx1.unlock();
    std::cout << "Thread 1 unlocked mutex 1" << std::endl;

    mtx2.unlock();
    std::cout << "Thread 1 unlocked mutex 2" << std::endl;
}

void func2() {
    // 锁定 mtx2,然后再锁定 mtx1
    mtx2.lock();
    std::cout << "Thread 2 locked mutex 2" << std::endl;

    mtx1.lock();
    std::cout << "Thread 2 locked mutex 1" << std::endl;

    // 解锁 mtx1 和 mtx2
    mtx1.unlock();
    std::cout << "Thread 2 unlocked mutex 1" << std::endl;

    mtx2.unlock();
    std::cout << "Thread 2 unlocked mutex 2" << std::endl;
}

int main() {
    // 启动两个线程,分别执行 func1 和 func2
    std::thread t1(func1);
    std::thread t2(func2);

    // 等待两个线程结束
    t1.join();
    t2.join();

    return 0;
}

lock_guard 与 std::unique_lock

使用 std::lock 和 std::lock_guard 或 std::unique_lock,它们能确保同时尝试锁定多个互斥量,并且不会死锁。

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

std::mutex mtx1, mtx2;

void func1() {
    std::lock(mtx1, mtx2); // 同时锁定两个互斥量,避免死锁
    std::lock_guard<std::mutex> lk1(mtx1, std::adopt_lock); // adopt_lock表示互斥量已经被锁定
    std::lock_guard<std::mutex> lk2(mtx2, std::adopt_lock);
    
    std::cout << "Thread 1 locked mutexes 1 and 2" << std::endl;
    // 自动解锁
}

void func2() {
    std::lock(mtx1, mtx2); // 同时锁定两个互斥量
    std::lock_guard<std::mutex> lk1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(mtx2, std::adopt_lock);
    
    std::cout << "Thread 2 locked mutexes 1 and 2" << std::endl;
    // 自动解锁
}

int main() {
    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();
    return 0;
}

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

call_once

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

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

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

static Log& GetInstance() {
//static Log log;
//return log;

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");
}

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

#include<iostream>
#include<string>
#include <thread>
#include <mutex>
class Log
{
public:
static Log& GetInstance()
{
//static Log log; //饿汉模式
//return log;

std::call_once(once, initfunc);

return *log;
}

static void initfunc()
{
if (!log)
{
log = new Log;
}
}

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

private:
Log() {}
~Log() {}
Log(const Log&) = delete;
Log& operator=(const Log&) = delete;

//静态成员必须类外初始化
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;
}

condition_variable

生产者与消费者模型

跨平台线程池

异步并发

原子操作


原文地址:https://blog.csdn.net/weixin_45178274/article/details/142850370

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!