C++ Thread线程

一、线程使用基本函数

        要创建线程,需要一个可调用的函数或函数对象,作为线程的入口函数。在C++ 11中,可以使用函数指针,函数对象或lambda表达式来实现。

        创建线程的基本语法如下所示:

std::thread  t(function_name, args...);

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

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

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

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

        joinable()方法返回一个布尔值,如果线程可以被join()或detach(),则返回true,否则返回false。

void printString(const char* str) {
    cout << str << endl;
}

int main(){
    char str[] = "wangyun";
    // 创建线程,并传递参数
    thread t1(printString,str);

    // 判断线程是否可以调用join函数
    bool isJoin = t1.joinable();
    
    if (isJoin) {
        // 主线程中调用t.join()
        //      阻塞主线程,让主线程等待t子线程执行完成,再继续。
        t1.join();
    }

     分离线程,主线程结束后,子线程仍在后台执行
    //t1.detach();
}

 常见错误

  • 忘记等待线程完成或分离线程:如果我们创建了一个线程,但没有等待它完成或分离它,那么在主线程结束时,可能会导致未定义行为。
  • 访问共享数据时没有同步:如果我们在多个线程中访问共享数据,但没有使用同步机制,那么可能会导致数据竞争、死锁等问题。
  • 异常传递问题:如果在线程中发生了异常,但没有处理它,那么可能会导致程序崩溃。因此,我们应该在线程中使用try-catch块来捕获异常,并在适当的地方处理它。

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

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

错误示例:

#include <iostream>
#include <thread>

void foo(int& x) {
    x += 1;
}

void test() {
    int a = 1;
    
    // a是局部变量,只在test内部有效
    // 线程传递引用,指向了该局部变量
    // 线程使用该变量的时候,变量已经释放了。
    // 解决方法是将局部变量改为全局变量
    t = thread(foo,ref(a));
}

int main() {
    test();
    t.join();
    return 0;
}

        上例中,我们在test函数中创建了一个局部变量a,并创建了一个名为`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;
}

        该例中存在的问题与2.2中相同,只是变量在传入线程入口函数后,立马由delete手动释放掉

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 来管理类对象的生命周期,确保在线程执行期间对象不会被销毁

正确示例:

int main(){
    // 可以用智能指针解决该问题
    // 此时obj就是指向MyClass一个对象的指针
    // 此时obj不会被提前销毁,只有obj不再被使用,才会由系统销毁
    shared_ptr <MyClass>  obj = make_shared<MyClass>();
    thread t(&MyClass::func, obj);
    t.join();
}

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

        如果线程入口函数是类的私有成员函数,那么可以使用友元的方式来解决。

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

private:
    void foo() {
        cout << "wangy" << endl;
    }
};
// 友元函数解决入口函数为私有成员函数的问题
void thread_foo() {
    shared_ptr <MyClass>  obj = make_shared<MyClass>();
    thread t(&MyClass::foo, obj);
    t.join();
}

int main(){
    // 入口函数为类的私有成员函数
    // 可以使用友元函数
    //shared_ptr <MyClass>  obj = make_shared<MyClass>();
    //thread t(&MyClass::foo, obj);
    thread_foo();
}

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

3.1 互斥量

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

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

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

错误示例:

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

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

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

 正确示例:

int a = 0;
mutex mtx;  // 互斥锁
void func() {
    for (int i = 0; i < 100000; i++) {
        // 对变量a的写操作加互斥锁,同一时刻,只能有一个线程执行a+=1语句
        mtx.lock();
        a += 1;
        mtx.unlock();
    }
}

3.2 互斥量死锁

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

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

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

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

mutex mtx1;  // 互斥锁
mutex mtx2;

void func_1() {
    mtx1.lock();
    mtx2.lock();
    mtx1.unlock();
    mtx2.unlock();
}
void func_2() {
    mtx2.lock();
    mtx1.lock();
    mtx2.unlock();
    mtx1.unlock();
}

int main(){
    thread t1(func_1);
    thread t2(func_2);
    t1.join();
    t2.join();

    // 解决死锁,改变顺序
}

3.3 lock_guard 与 std::unique_lock

lock_guard

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

std::lock_guard 的特点如下

  • 当构造函数被调用时,该互斥量会被自动锁定。
  • 当析构函数被调用时,该互斥量会被自动解锁。
  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。
mutex mtx;
int shared_data = 0;
void func() {
    for (int i = 0; i < 100000; i++) {
        // 对变量a的写操作加互斥锁,同一时刻,只能有一个线程执行a+=1语句
        lock_guard<mutex> lg(mtx);
        shared_data += 1;
    }
}

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 进行初始化,并假设该互斥量已经被当前线程成功加锁。
void func2() {
    for (int i = 0; i < 100000; i++) {
        // 对变量a的写操作加互斥锁,同一时刻,只能有一个线程执行a+=1语句
        unique_lock<mutex> lg(mtx,defer_lock);
        lg.lock();
        shared_data += 1;
    }
}

四、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 <list>
#include <iostream>
#include <thread>
#include <mutex>
#include <memory>
#include <condition_variable>
#include <queue>

using namespace std;

#pragma region condition_variable与其使用场景
queue<int> q_queue;
condition_variable q_cv;
mutex mtx;
// 生产者(负责向队列中添加任务)
void Producer() {
    for (int i = 0; i < 20; i++) {
        // 加锁,防止生产者和消费者竞争队列
        // 即生产者放任务的同时,消费者再取任务
        unique_lock<mutex> lg(mtx);
        q_queue.push(i);
        // 任务添加之后,此处需要通知消费者来消费任务
        q_cv.notify_one();
        cout << "Producer: " << i << endl;
        // 休眠
        this_thread::sleep_for(chrono::microseconds(100));
    }
}
// 消费者(从队列中消费任务)
void Consumer() {
    unique_lock<mutex> lock(mtx);
    cout << " Create a consumer" << this_thread::get_id() << endl;
    lock.unlock();
    while (1) {
        unique_lock<mutex> lg(mtx);
        // 如果队列为空,需要此处需要等待
        // 等到q_cv调用notify_one()函数,才会继续
        //q_cv.wait(lg, !q_queue.empty());
        q_cv.wait(lg, []() {
            return !q_queue.empty();
            });
        int value = q_queue.front();
        q_queue.pop();
        cout << "consumer:" << value << " " << this_thread::get_id() << endl;
    }
}
#pragma endregion

int main(){
    /**
     * 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() 函数等待条件变量。
     *  在其他线程中需要通知等待的线程时,调用 std::condition_variable::notify_one() 或 std::condition_variable::notify_all() 函数通知等待的线程。
     */
    thread t1(Producer);
    thread t2(Consumer);
    thread t3(Consumer);
    thread t4(Consumer);
    t1.join();
    t2.join();
    t3.join();
    t4.join();
}

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

五 线程池

        线程池的基本思想是预先创建一定数量的线程,并将它们放入一个池中。线程池负责管理线程的生命周期,并将任务分配给空闲线程执行。这样可以避免每次任务执行时都创建和销毁线程的开销。

线程池的组成如下

  1. 线程池管理器:负责创建、销毁线程,维护线程池状态(如空闲线程、忙碌线程)。
  2. 任务队列:用于存储待执行的任务。任务通常以函数对象(std::function)的形式存储。
  3. 工作线程:线程池中的实际线程,他们从任务队列中取出任务并执行。
  4. 同步机制:用于保护任务队列和线程池状态的线程安全操作,通常使用互斥锁和条件变量。

 在设计线程池时,我们需要考虑以下几个重要原则:

1. 线程池大小管理
        固定大小:线程池中的线程数量固定不变。适用于负载比较稳定的场景。
        动态调整:根据任务负载动态调整线程池大小。适用于负载变化较大的场景。

2. 任务队列管理

        FIFO 队列:最常用的任务队列实现方式,按照任务提交的顺序执行任务。

        优先级队列:根据任务的优先级执行任务,适用于需要按优先级处理任务的场景。

3. 线程安全

        互斥锁:用于保护共享资源(如任务队列)的访问。

        条件变量:用于线程之间的通信,如通知空闲线程有新的任务到来。

        原子变量:对原子变量的操作都是原子操作,它是线程安全的。

4. 任务执行与错误处理

        任务执行过程中的异常需要适当处理,以避免线程池中的线程因未捕获异常而终止

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

using namespace std;

class ThreadPool
{
public:
	ThreadPool(int numThreads) :stop(false) {
		// 创建指定数量的线程
		for (int i = 0; i < numThreads; i++) {
			threads.push_back(thread([this]() {
				// 线程循环
				while (true) {
					function<void()> task;
					{
						// 操纵共享任务队列
						unique_lock<mutex> lock(queueMutex);
						// 使用条件变量等待任务到来或者线程池停止信号
						condition.wait(lock, [this]() {
							return stop || taskQueue.empty();
							});
						if (stop && taskQueue.empty()) {
							return;
						}
						task = taskQueue.front();
						taskQueue.pop();
					}
					task();
				}
				})
			);
		}
	}

	~ThreadPool() {
		{
			unique_lock<mutex> lock(queueMutex);
			stop = true;
		}
		// 条件变量通知所有在等待的线程完成任务队列所有任务,然后停止
		for (auto& th : threads) {
			if (th.joinable()) {
				th.join();
			}
		}
	}

	template<typename F,typename... Args>
	void enqueue(F &&f, Args&&...args) {
		// 将函数和函数参数绑定在一起,这样,之后调用该函数,就不用传参了。
		function<void()> task = bind(std::forward<F>(f), std::forward<Args>(args)...);	// forward 完美转换
		{
			unique_lock<std::mutex> lock(queueMutex);
			taskQueue.emplace(move(task));
		}
		condition.notify_one();
	}
private:
	vector<thread> threads;	
	queue<function<void()>> taskQueue;
	mutex queueMutex;
	condition_variable condition;
	bool stop;
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值