Studying-多线程学习Part1-线程库的基本使用、线程函数中的数据未定义错误、互斥量解决多线程数据共享问题

news/2024/10/8 12:31:12 标签: 多线程, thread

来源:多线程编程


线程库的基本使用

两个概念:

  • 进程是运行中的程序
  • 线程是进程中的进程

串行运行:一次只能取得一个任务并执行这一个任务

并行运行:可以同时通过多进程/多线程的方式取得多个任务,并以多进程或多线程的方式同时执行这些任务。

线程的最大数量取决于cpu的核心数。

thread的函数原型:传入一个函数名就可以运行

1.创建线程:

使用thread函数,但是如下程序会报错,原因在于线程还在运行的时候,主程序可能就已经结束了。 

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


void printHelloWorld() {
	cout << "Hello World" << endl;
	return;
}

int main() {
	//1.创建线程
	thread thread1(printHelloWorld);
	return 0;
}

2.等待线程完成:

为了解决上述问题,我们可以使用join()函数,来让主线程等待线程执行完毕。

PS:join()函数是阻塞的,程序会一直停留在join()处,直到线程运行完毕

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


void printHelloWorld() {
	cout << "Hello World" << endl;
	return;
}

int main() {
	//1.创建线程
	thread thread1(printHelloWorld);
	//主程序等待线程执行完毕join()
	thread1.join();

	return 0;
}

3.传入参数:

如果函数带有参数,我们也可以在thread的后面增加参数列表。 

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


void printHelloWorld(string msg, string msg2) {
	cout << msg << endl;
	cout << msg2 << endl;
	return;
}

int main() {
	//1.创建线程
	thread thread1(printHelloWorld, "Hello Thread", "Hello World");
	//主程序等待线程执行完毕join()
	thread1.join();

	return 0;
}

4.分离线程:

我们有时候也希望主程序不需要等待线程完成,而是让线程它在后台运行,这时候我们可以使用到detach()函数分离线程。(下述情况什么都不会打印,来不及打印)

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


void printHelloWorld(string msg, string msg2) {
	cout << msg << endl;
	cout << msg2 << endl;
	return;
}

int main() {
	//1.创建线程
	thread thread1(printHelloWorld, "Hello Thread", "Hello World");
	//主程序等待线程执行完毕join()
	thread1.detach();

	return 0;
}

5. joinable():

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

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


void printHelloWorld(string msg, string msg2) {
	cout << msg << endl;
	cout << msg2 << endl;
	return;
}

int main() {
	//1.创建线程
	thread thread1(printHelloWorld, "Hello Thread", "Hello World");
	//主程序等待线程执行完毕join()
	bool isJoin = thread1.joinable();

	if (isJoin) {
		thread1.join();
	}

	return 0;
}

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

1.传递临时变量的问题:

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

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

int main() {

	thread t(foo, 1);
	t.join();
	return 0;
}

在上述例子中,我们将临时变量1作为参数传递给了foo, 这样会导致在线程函数执行时,临时变量`1`已经销毁,从而导致未定义行为。

解决方案是将变量复制到一个持久的对象中,然后将该对象传递给线程。例如,我们可以将`1`复制到一个`int`类型的变量中,然后将该变量的引用传递给线程。

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

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

int main() {
	int a = 1;
	//ref函数,将a转换为自身的引用,在线程函数中需要使用
	thread t(foo, ref(a));
	t.join();
	cout << a << endl;
	return 0;
}

2. 传递指针或引用指向局部变量的问题: 

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


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

void test() {
	int a = 1;
	t = thread(foo, ref(a));
	return;
}

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

如果传入线程中的参数是局部变量,则线程在进行的时候,变量就已经被销毁了,无法得到结果。解决办法就是让a变量变为全局变量。或者join()放在thread()函数后面。关键就在于要注意变量的生命周期。

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

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

void foo(int* x) {
    cout << *x << endl; // 访问已经被释放的内存
}
int main() {
    int* ptr = new int(1);
    thread t(foo, ptr); // 传递已经释放的内存
    delete ptr;
    t.join();
    return 0;
}

提前把ptr进行删除,会导致传入的是已经释放内存了的空间,结果变得不确定。这也是要注意变量的生命周期和作用范围的问题。

4. 类成员函数作为入口函数,类对象被提前释放

和上一个问题的原因类似,在创建线程之后,如果类对象已经被销毁,这会导致在线程执行时无法访问对象,可能会导致程序崩溃或者产生未定义的行为。

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

class A {
public:
	void foo() {
		cout << "Hello" << endl;
	}
};

int main() {
	A a;
	thread t(&A::foo, &a);
	/*
	进行一系列操作,可能会导致a被释放
	*/
	t.join();
	return 0;
}

为了解决上述问题,我们需要使用指针来保证地址有效,但如果用普通指针,我们需要自行进行指针的删除和释放。因此我们可以采用智能指针shared_ptr来管理类对象的生命周期,确保在线程执行期间对象不会被销毁。具体来说,可以在创建线程之前,将类对象的指针封装在shared_ptr 对象中,并将其作为参数传递给线程。这样,在线程执行期间,即使类对象的所有者释放了其所有权。

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

class A {
public:
	void foo() {
		cout << "Hello" << endl;
	}
};

int main(){
	shared_ptr<A> a = make_shared<A>();
	thread t(&A::foo, a);
	/*
	进行一系列操作,可能会导致a被释放
	*/
	t.join();
	return 0;
}

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

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

class A {
private:
	void foo() {
		cout << "Hello" << endl;
	}
};

int main() {
	shared_ptr<A> a = make_shared<A>();
	thread t(&A::foo, a); //报错不能调用foo函数
	/*
	进行一系列操作,可能会导致a被释放
	*/
	t.join();
	return 0;
}

如果函数是私有成员函数,那么是无法调用的。需要使用到友元函数, 并在函数中调用foo()函数。

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

class A {
private:
	friend void thread_foo();
	void foo() {
		cout << "Hello" << endl;
	}
};

void thread_foo() {
	shared_ptr<A> a = make_shared<A>();
	thread t(&A::foo, a); //报错不能调用foo函数
	/*
	进行一系列操作,可能会导致a被释放
	*/
	t.join();
	return;
}

int main() {
	thread_foo();
	return 0;
}

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

数据共享问题分析

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

数据竞争问题简单的可以理解为,t1和t2在取数据的时候,可能会取到同样的a的值,也就是t2不会等待t1完成对a的所有加1操作之后,才去取a,这样就会导致a无法正确的加200000次。

#include <iostream>
#include <thread>
using namespace std;
int share_data = 0;
void fun() {
	for (int i = 0; i < 100000; ++i) {
		share_data += 1;
	}
}

int main() {
	thread t1(fun);
	thread t2(fun);
	t1.join();
	t2.join();
	cout << share_data << endl;
	return 0;
}

为了解决这个问题,我们需要保证当一个线程去拿a的值的时候,其他的线程不能去拿a,保证共享数据的安全。 为了避免数据竞争问题,需要使用同步机制来确保多个线程之间对共享数据的访问是安全的。常见的同步机制包括互斥量、条件变量、原子操作等。

互斥锁 

互斥量(mutex)是一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题。互斥量提供了两个基本操作:lock() 和 unlock()。当一个线程调用 lock() 函数时,如果互斥量当前没有被其他线程占用,则该线程获得该互斥量的所有权,可以对共享资源进行访问。如果互斥量当前已经被其他线程占用,则调用 lock() 函数的线程会被阻塞,直到该互斥量被释放为止。

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

int share_data = 0;
mutex mtx;
void fun() {
	for (int i = 0; i < 1000000; ++i) {
		mtx.lock(); //加锁操作
		share_data += 1;
		mtx.unlock(); //解锁操作
	}
}

int main() {
	thread t1(fun);
	thread t2(fun);
	t1.join();
	t2.join();
	cout << share_data << endl;
	return 0;
}

本质上,互斥锁并没有对share_data加锁,而是在每次运行+= 1操作之前,都会判断mtx互斥量是否上锁,如果上锁了则阻塞等待,直到另一边解锁,以此来实现对shared_data变量的访问是安全的。 

线程安全定义:

如果多线程程序每一次的运行结果和单线程运行的结果始终是一样的,那么你的线程就是安全的。也就是把多线程改为单线程之后,运行的结果也不会发生改变。(本质上多线程就是把单线程的任务分割为多个任务,能够安全的分别进行,以此来增加程序的运行效率)


http://www.niftyadmin.cn/n/5694126.html

相关文章

黑马JavaWeb开发跟学(十二)SpringBootWeb案例

黑马JavaWeb开发跟学十二.SpringBootWeb案例 案例-登录认证1. 登录功能1.1 需求1.2 接口文档1.3 思路分析1.4 功能开发1.5 测试 2. 登录校验2.1 问题分析2.2 会话技术2.2.1 会话技术介绍2.2.2 会话跟踪方案2.2.2.1 方案一 - Cookie2.2.2.2 方案二 - Session2.2.2.3 方案三 - 令…

【分布式微服务云原生】Redis持久化策略:RDB vs AOF

Redis持久化策略&#xff1a;RDB vs AOF 摘要 本文深入探讨了Redis的两种主要持久化策略&#xff1a;RDB和AOF。我们将分析它们的工作原理、优缺点&#xff0c;并探讨如何在不同的应用场景中选择最合适的持久化策略。此外&#xff0c;文章还将提供Java代码示例和流程图&#…

828华为云征文 | 华为云Flexus X实例在混合云环境中的应用与实践

目录 前言 1. 混合云环境的优势与挑战 1.1 混合云的优势 1.2 混合云的挑战 2. Flexus X实例的配置与集成 2.1 Flexus X实例简介 2.2 Flexus X实例的混合云部署 2.3 配置步骤与措施 3. 数据迁移与同步策略 3.1 数据迁移方案 3.2 数据同步措施 4. 安全性与合规性管理…

性能剖析利器-Conan|得物技术

作者 / 得物技术 - 仁慈的狮子 目录 一、背景 1. 局限性 2. 向前一步 二、原理剖析 1. 系统架构 2. 工作模式 3. reporter 三、稳定性验证 四、案例分析 五、写在最后 一、背景 线上问题的定位与优化是程序员进阶的必经之路&#xff0c;常见的问题定位手段有日志排查、分布式链…

操作系统 | 学习笔记 | 王道 | 3.2 虚拟内存管理

3.2 虚拟内存管理 3.2.1 虚拟内存的基本概念 传统存储管理方式的特征 传统存储管理方式 连续分配 单一连续分配固定分区分配动态分区分配 非连续分配 基本分页存储管理基本分段存储管理基本段页式存储管理 特征&#xff1a; 一次性&#xff1a; 作业必须一次性全部装入内存后…

策略模式和模板模式的区别

目录 一、实现方式 策略模式 模板模式 二、使用场景 三、优点 四、举例 一、实现方式 策略模式 定义策略接口 Strategy创建具体策略类 OperationAdd、OperationSubtract、OperationMultiply创建一个上下文类 Context&#xff0c;包含一个策略对象的引用&#xff0c;并通…

基于连续小波变换(CWT)批量生成一维信号的时频图 最终生成30张时频图。生成的图像可用于后续的深度学习分类或其他处理。附详细的说明文档。

Matlab基于连续小波变换&#xff08;CWT&#xff09;&#xff0c;将一维信号批量生成时频图的源代码。此示例中&#xff0c;原始信号data是30*1280的格式&#xff0c;一共30条信号&#xff0c;信号长度为1280。最终生成30张时频图。生成的图像可用于后续的深度学习分类或其他处…

毕业设计项目 深度学习安全帽佩戴检测(源码+论文)

文章目录 0 前言1 项目运行效果2 设计概要3 最后 0 前言 &#x1f525;这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点&#xff0c;往往达不到毕业答辩的要求&#xff0c;这两年不断有学弟学妹告诉学长自己做的项目系统达不到老师…