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

来源:多线程编程


线程库的基本使用

两个概念:

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

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

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

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

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

1.创建线程:

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

cpp 复制代码
#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()处,直到线程运行完毕

cpp 复制代码
#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的后面增加参数列表。

cpp 复制代码
#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()函数分离线程。(下述情况什么都不会打印,来不及打印)

cpp 复制代码
#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异常。

cpp 复制代码
#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.传递临时变量的问题:

cpp 复制代码
#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`类型的变量中,然后将该变量的引用传递给线程。

cpp 复制代码
#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. 传递指针或引用指向局部变量的问题:

cpp 复制代码
#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. 传递指针或引用指向已释放的内存的问题:

cpp 复制代码
#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. 类成员函数作为入口函数,类对象被提前释放

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

cpp 复制代码
#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 对象中,并将其作为参数传递给线程。这样,在线程执行期间,即使类对象的所有者释放了其所有权。

cpp 复制代码
#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.入口函数为类的私有成员函数

cpp 复制代码
#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()函数。

cpp 复制代码
#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次。

cpp 复制代码
#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() 函数的线程会被阻塞,直到该互斥量被释放为止。

cpp 复制代码
#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变量的访问是安全的。

线程安全定义:

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

相关推荐
程序员一点3 天前
Python并发编程(1)——Python并发编程的几种实现方式
python·多线程·并发编程·多进程
这题怎么做?!?9 天前
【Linux】多线程:线程池的创建、日志类、RAII互斥锁、单例模式:饿汉方式与懒汉方式
linux·c语言·c++·单例模式·线程池·多线程·日志
写hello world都有bug10 天前
谈谈Redisson分布式锁的底层实现原理
redis·多线程·分布式锁
hn_tzy11 天前
C++11中引入的thread
开发语言·c++·多线程·条件变量·thread·互斥锁·同步
无理 Java14 天前
【技术解析】消息中间件MQ:从原理到RabbitMQ实战(深入浅出)
java·分布式·后端·rabbitmq·多线程·mq·消息中间件
刘大猫.14 天前
Arthas thread(查看当前JVM的线程堆栈信息)
jvm·thread·命令·arthas·查看当前jvm的线程堆栈信息·thread命令
亿牛云爬虫专家18 天前
优化数据的抓取规则:减少无效请求
python·数据采集·多线程·爬虫代理·数据抓取·代理ip·房价
亿牛云爬虫专家19 天前
如何通过subprocess在数据采集中执行外部命令 —以微博为例
爬虫·python·数据采集·多线程·代理ip·subprocess·微博