C++多线程基本使用方式

一、线程创建

        创建线程的函数    thread  t (函数名f,函数 f 的参数) 或者  用lambda表达式

代码:

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

void output(string input,int a) {
	cout << input << endl;
	cout << a << endl;
}

int main() {
	string text = "Hello World ! My name is XXX . Nice to meet you . bla bla bla\n";
	vector<thread> thread_list;
	int a = 10;
	for (int i = 0; i < 2; i++) {
		thread t(output,text,a);
		//thread t([text](int a) {
		//	cout << text << endl;
		//	cout << a << endl;
		//	},a);
//上面为lambda表示法,效果相同,可提高可读性,简化代码,但是运行速度,效率不如内敛函数incline
		thread_list.push_back(move(t));
	}
	for (auto& t : thread_list) {
		if (t.joinable()) {
			t.join();
		}
	}
	return 0;
}

        由于是多线程,导致可能出现不同线程,交替输出甚至短暂截断输出的情况!                                比如:第一个线程输出到 “ Hello World ! My name is XXX .  ” 时,第二个线程就开始输出,然后第一个线程又继续输出剩余内容,最后就是多个线程抢用一个输出,导致输出结果交叉混杂

        for循环中,给vector添加数据时,使用move函数,相当于将 t 的值直接存入thread_list,不用创建额外thread变量来存入,且move以后,t  就处于未定义状态了

        最后一定要 join 释放资源,否则运行程序会报错!

        

二、线程传参

        左值:等式左边的值

        右值:等式右边的值

        例:a = a+2*b+3;        a是左值,可重复使用;(a+2*b+3)是右值,临时计算的数据

        如果是函数要求左值引用传参,即int& a,则需要用ref()将参数包装起来,否则编译报错

代码:

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

void output(string input,int& a) {//此处  a   为引用传值!
	cout << input << endl;
	cout << ++a << endl;
}

int main() {
	string text = "Hello World ! My name is XXX . Nice to meet you . bla bla bla\n";
	vector<thread> list;
	int a = 10;
	for (int i = 0; i < 2; i++) {
		thread t(output,text,ref(a));//用  ref  包装参数  a
		//thread t([text](int& a) {
		//	cout << text << endl;
		//	cout << ++a << endl;
		//	},ref(a));
		// lambda写法的修改  上同
		list.push_back(move(t));
	}
	cout <<"before join: "<< a << endl;
	for (auto& t : list) {
		if (t.joinable()) {
			t.join();
		}
	}
	cout << "after join: " << a << endl;

	return 0;
}

输出:

        还有一点,左值引用传递时,不可传递右值,否则报错

        如果是右值引用传参,即int&& a ,则不能用ref(),否则会报错

        输出结果中,先打印     before join:  10  ,是因为线程异步,当for循环结束时,由于线程启动、调用传入函数等操作需要时间开销,导致线程还没开始输出,而这时主程序已经运行到cout这一行了,所以就会先输出     before join:  10 

三、Mutex与Atomic

        3.1、mutex(互斥锁)

                用于保护共享数据免受并发访问的冲突,通过阻塞和唤醒线程来管理共享资源的访问。

主要特点:

  1. 互斥访问:确保同一时间只有一个线程可以访问被保护的资源。
  2. 锁的所有权:通过lock()unlock()成员函数来获取和释放锁。通常使用lock_guardunique_lock来自动管理锁的生命周期。
  3. 死锁:不当的使用可能导致死锁,例如,两个线程相互等待对方释放锁。
  4. 上下文切换:当一个线程等待锁时,可能会发生上下文切换,增加系统开销。

优点:

  • 提供同步,确保数据的一致性和线程安全。
  • 适用于需要保护复杂数据结构或执行复杂同步操作的场景。

缺点:

  • 可能导致性能开销,特别是在高争用的情况下。
  • 需要正确管理锁的获取和释放,否则可能引发死锁。

代码:

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

mutex mtx; // 创建互斥锁

void printID(int id) {
    lock_guard<mutex> lock(mtx); // 锁定互斥锁
    cout << "ID: " << id << endl;
}

int main() {
    thread t1(printID, 1);
    thread t2(printID, 2);
    
    t1.join();
    t2.join();
    
    return 0;
}

互斥锁的释放时机:

  • std::lock_guard 对象的生命周期结束时,互斥锁会被自动释放。
  • 这通常发生在以下情况:
    • 作用域结束lock_guard 对象被销毁。
    • 程序流程离开包含 lock_guard 对象的代码块,例子如下
void Function() {
    std::mutex mtx;
    {
        std::lock_guard<std::mutex> lock(mtx);  // 互斥锁在此处被锁定
        // 在这个作用域内,mtx 被锁定
    } // 作用域结束,lock_guard 对象被销毁,互斥锁在此处被释放
    // 此时 mtx 已经解锁,可以再次被锁定
}

使用 lock_guard 的好处:

  • 自动管理:自动获取和释放互斥锁,减少忘记手动释放锁的风险。
  • 异常安全:保证即使在发生异常时互斥锁也能被正确释放,避免死锁
  • 代码简洁:使代码更加简洁易于理解和维护

        3.2、atomic(原子锁)

                C++11引入的用于实现原子操作的模板类。原子操作保证在多线程环境中,单个操作不可分割,不会出现中间状态。

主要特点:

  1. 不可分割的操作:原子操作要么完全执行,要么完全不执行,不会出现中间状态。
  2. 内存顺序:提供不同内存顺序选项,如memory_order_relaxedmemory_order_acquire等。
  3. 不阻塞:原子操作通常不会阻塞线程,而是通过硬件支持来实现同步。
  4. 只支持基本类型:主要支持基本数据类型(如整数类型、指针类型)。

优点:

  • 性能较高,特别是对于简单的数据类型和操作。
  • 不会引起线程阻塞,减少了上下文切换的开销。

缺点:

  • 功能有限,主要适用于简单的数据类型和操作。
  • 需要仔细处理内存顺序,以避免数据竞争和不一致性。

代码:

#include <atomic>
#include <thread>

atomic<int> count(0); // 创建原子变量

void increment() {
    count.fetch_add(1, memory_order_relaxed); // 令 count 的值 +1
}

int main() {
    thread t1(increment);
    thread t2(increment);

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

    cout << "Final count: " << count << endl;

    return 0;
}

四、自旋锁spinlock

自旋锁(Spinlock)

        定义: 一种简单锁机制,当一个线程尝试获取一个已被其他线程持有的锁时,该线程不会立即阻塞(即不会进入睡眠状态),而是在当前位置循环(自旋,占用CPU),直到锁被释放

        用途
                自旋锁适用于锁持有时间且线程持续占用CPU的场景。        
                它们常用于避免线程在等待锁时浪费CPU资源。

使用方式: 自旋锁通常在多核竞争环境中使用。在单核或高竞争环境中,自旋锁可能导致CPU资源浪费,因为线程在等待锁时仍然占用CPU。

如果有pthread库,使用起来很简单,但是一般不导入第三方库,我们需要使用atomic_flag一种轻量级的自旋锁机制,来达到自旋锁功能。

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

class spin_lock {
private:
    atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while (flag.test_and_set(memory_order_acquire)) {
            // 循环直到能够设置flag
        }
    }
    void unlock() {
        flag.clear(memory_order_release);
    }
};

int main() {

    spin_lock lock;
    thread t1([&]() {
        lock.lock();
        cout << "t1 thread" << endl;
        // 临界区代码
        lock.unlock();
        });
    thread t2([&]() {
        lock.lock();
        cout <<"t1 : "<< t1.get_id() << endl;
        // 临界区代码
        lock.unlock();
        });
    cout <<"t2 : "<< t2.get_id() << endl;
    t1.join();
    t2.join();
    return 0;
}

五、条件变量condition_variable

        定义: 一种同步机制,用于实现线程之间的等待和唤醒机制,可以与mutex(互斥锁)结合使用,实现复杂的线程同步和通信

        用途

                条件变量用于线程间基于特定条件的协调,如生产者-消费者问题
                它们可以减少线程在等待条件满足时的CPU资源浪费

使用方式: 条件变量通常与互斥锁(mutex)一起使用。线程在进入等待状态前需要锁定互斥锁,然后在等待条件变量时释放锁,并在被唤醒后重新尝试获取锁。

        主要作用是允许一个或多个线程等待某个条件满足后再执行。在等待期间,线程会被阻塞,不会消耗CPU资源,直到其他线程通过通知(notify)来唤醒它们。

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

condition_variable cv;
mutex mt;
int b = 0;
void out(int id) {
	unique_lock<mutex> lock(mt);
	cv.wait(lock, [id]() {return (b == id); });
	cout << b++ << endl;
	cv.notify_one();
}

int main() {
	vector<thread> threadList;
	thread t;
	for (int i = 0; i < 3; i++) {
		t = thread(out,i);
		threadList.push_back(move(t));
        //threadList.emplace_back(out, i);
	}
	for (auto& tmp : threadList) {
		if (tmp.joinable()) {
			tmp.join();
		}
	}
	return 0;
}

六、join与detach

         (1)join

                join函数会阻塞线程,直到线程函数执行结束,再执行后续语句
                优点:可以保证资源的清理和数据的一致性,因为主线程会在所有子线程结束后再结束
                缺点:如果主线程等待时间过长,可能会导致程序的响应性降低
        (2)detach

                detach将线程分离,使其在后台继续运行而任何线程关联。分离后的线程称为守护线程(daemon thread)
                优点:允许线程在后台运行而不影响主线程的结束,适用于时间运行的任务,如日志记录或资源监控
                ​​​​​​​缺点:如果分离线程访问了线程中的对象,而该对象在主线程结束后被销毁,可能会导致未定义行为或程序崩溃

  • 37
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值