【C++11】多线程+IO流


目录

一、C++11线程库

1、每个线程都有独立的栈空间

2、加锁的位置

3、CSA操作

4、C++的类模板atomic(原子操作)

5、lock_guard(RAII风格的锁)/unique_lock(可随时释放锁)

6、条件变量(用于互相通知)

二、IO流

1、C语言的输入输出缓冲区

2、C++流的概念

3、流提取

4、C++文件IO流

4.1二进制读写

4.2文本读写(写法比较简单)

5、使用stringstream序列化和反序列化(转字符串)

5.1使用ostringstream和istringstream


一、C++11线程库

        C++11的线程库在windows下能用,在Linux环境下也能用,是因为其内部的条件编译。

        对于C++线程库,必须分配了线程关联函数,线程才被创建启动。

1、每个线程都有独立的栈空间

        两个线程都可以调用Func1函数,在调用函数时,两个线程会各自创建自己的栈空间,所以回调函数的栈区变量从属于各个线程,线程间互不影响。

2、加锁的位置

        如图,Func2的执行效率更高。

        加锁时总是想着把锁放到临界区的极限边界,这是不对的。这里我们要保护的代码是++val,像Func1把锁就放在++val的前后,最短化临界区,卡在了极限的距离。但是AB线程每进行一次++操作,就要判断一次锁,会产生高频次的线程切换,影响效率。但是Func2这种加锁方式,一个线程跑完循环再放另一个线程进去,极大地减少了加锁带来的线程切换。(当然需要根据实际代码分析哈,如果这里的++val是其他执行时间较长的代码,Func2的写法就不合适了,因为会导致另一个线程饥饿)

3、CSA操作

        CAS(Compare and Swap)操作是一种常见的并发编程技术,用于保证多个线程访问同一共享资源时的数据一致性。它可以在不使用锁的情况下实现对共享变量的原子更新操作。

        CAS操作通常由三个参数组成:内存地址V、预期值A和新值B。当多个线程同时尝试更新V时,只有其中一个线程能够成功执行CAS操作,即当且仅当V的当前值等于A时,才会将其更新为B。如果V的当前值不等于A,则说明其他线程已经修改了V,那么当前线程会放弃更新操作,并重新尝试。

        CAS操作通常用于实现无锁算法,在高并发场景中可以提高程序的并发性能。但是,CAS操作也存在一些问题,例如ABA问题和自旋次数过多等,需要开发者在实际应用中注意避免和解决。

4、C++的类模板atomic(原子操作)

        atomic可以使线程并行。底层的本质就是CAS操作,写入时会去比对之前保存的值,如果不一样,说明已经有线程进行了修改,那么该线程将舍弃本次计算结果,重新计算、比对。

5、lock_guard(RAII风格的锁)/unique_lock(可随时释放锁)

保证在抛出异常、出了作用域时正确解锁互斥对象。

6、条件变量(用于互相通知)

        条件变量本身并不是线程安全的。

        一个线程在调用wait函数后,会被阻塞挂起,同时释放自己手上的锁。当另一个线程调用notify_one函数后,将重新唤醒该线程,该线程自动获得当初释放的那把锁。所以,这也是wait函数传入的形参必须是unique_lock的原因。使用条件变量控制偶数先打印的两种代码:

//写法一:
int mian()
{
	condition_variable cv;
	mutex mtx;
	int i = 0;
	bool flag = true;
	//线程1打印奇数,flag=false;
	thread t1([&cv, &mtx, &i,&flag] {
		while (i < 100)
		{
			unique_lock<mutex> ulock(mtx);//1、线程1拿到锁mtx
			while (flag == true)//2、判断成立
			{
				cv.wait(ulock);//3、线程1被wait阻塞并释放锁
			}
			cout << "t1" << this_thread::get_id << "->" << i << endl;//8、线程1执行任务
			++i;
			flag = true;
            cv.notify_one();//9、唤醒线程2
		}
		});
	//线程2打印偶数,flag=true;
	thread t2([&cv, &mtx, &i, &flag] {
		while (i <= 100)
		{
			unique_lock<mutex> ulock(mtx);//4、线程2拿到线程1刚释放的锁
			while (flag == false)//5、判断不成立
			{
				cv.wait(ulock);
			}
			cout << "t2" << this_thread::get_id << "->" << i << endl;//6、执行任务
			++i;
			flag = false;
			cv.notify_one();//7、唤醒线程1
		}
		});
	t1.join();
	t2.join();
	return 0;
}

//写法二
int main()
{
	condition_variable cv;
	mutex mtx;
	int i = 0;
	//线程1打印奇数
	thread t1([&cv, &mtx, &i] {
		while (i < 100)
		{
			unique_lock<mutex> ulock(mtx);
			cv.wait(ulock, [&i] {return i % 2; });
			cout << "t1" << this_thread::get_id << "->" << i << endl;
			++i;
			cv.notify_one();
		}
		});
	//线程2打印偶数
	thread t2([&cv, &mtx, &i] {
		while (i <= 100)
		{
			unique_lock<mutex> ulock(mtx);
			cv.wait(ulock, [&i] {return i % 2 != 1; });
			cout << "t2" << this_thread::get_id << "->" << i << endl;
			++i;
			cv.notify_one();
		}
		});
	t1.join();
	t2.join();
	return 0;
}

        如果这里不使用条件变量,而在两个线程函数中仅使用if判断奇偶,也能达到效果,因为同一时间只有一个线程能进入if判断中,直到完成++i才能放另一个线程进if判断。在此之前,另一个线程会在if判断之外疯狂轮询,加大了CPU的负担。

thread t1([&i] {
    while (i < 100000)
    {
        if (i % 2)
        {
            cout << this_thread::get_id() << "->" << i << endl;
            ++i;
            //this_thread::sleep_for(std::chrono::microseconds(500));
        }
    }
    });
    
thread t2([&i] {
    while (i <= 100000)//这里写<会有线程安全问题
    {
        if (i % 2 == 0)
        {
            cout << this_thread::get_id() << "->" << i << endl;
            ++i;
            //this_thread::sleep_for(std::chrono::microseconds(500));
        }
    }
});

二、IO流

1、C语言的输入输出缓冲区

        1、可以屏蔽掉低级I/O的实现2、可以使用这部分的内容实现“行”读取的行为。

2、C++流的概念

        C++的流有数据流动的意思。为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能。它的特性是:有序连续、具有方向性

3、流提取

        流提取是一个阻塞操作,以空格或者换行作为一段读取的结束:

        cin>>str是std::string的operator>>所支持的,它的返回值是istream。

        为什么返回值istream可以作为while循环的逻辑判断呢?这是因为ios这个父类重载了operator bool,让其支持了逻辑判断。

        从C++11开始,可以使用explicit关键字来显式声明operator bool()函数,完成istream到bool类型的转变,以避免隐式类型转换带来的问题。(这意味着其内部显式地将istream对象转换为bool类型的值,而不能进行隐式类型转换。可以避免编译器自作主张进行隐式类型转换,例如编译器将一个对象错误地转换为bool类型的值,而导致程序出现错误。)

        那么问题来了,为什么void*和bool可以被重载?其实不是他俩能被重载,而是自定义类型可以通过类内重载指定类型完成隐式类型转换。(看起来很秀,但本质上是隐式类型转换,悄悄的改变类型,上面说了,你重载了类型之后,永远猜不到它会在哪个不该转换的地方发生转换)

4、C++文件IO流

        C++根据文件内容的数据格式分为二进制文件和文本文件。采用文件流对象操作文件的一般步骤:

        1. 定义一个文件流对象

ifstream ififile(只输入用)

ofstream ofifile(只输出用)

fstream iofifile(既输入又输出用)

        2. 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系

        3. 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写

        4. 关闭文件

4.1二进制读写

// 二进制读写
struct ServerInfo
{
	char _address[32];//这里不要给string
	//string _address;
	int _port;
};

struct ConfigManager
{
public:
	ConfigManager(const char* filename)
		:_filename(filename)
	{}

	void WriteBin(const ServerInfo& info)
	{
		//ofstream ofs(_filename, ofstream::out | ofstream::binary);
		ofstream ofs(_filename, ios_base::out | ios_base::binary);
		ofs.write((char*)&info, sizeof(info));
	}

	void ReadBin(ServerInfo& info)
	{
		ifstream ifs(_filename, ios_base::in | ios_base::binary);
		ifs.read((char*)&info, sizeof(info));
	}

private:
	string _filename; // 配置文件
};

int main()
{
	ConfigManager cm("test.txt");
	ServerInfo winfo = { "192.0.0.111111111111111111", 80};
	cm.WriteBin(winfo);

	ServerInfo rinfo;
	cm.ReadBin(rinfo);

	cout << rinfo._address << endl;
	cout << rinfo._port << endl;

	return 0;
}

        二进制读写,不要对string对象进行读写操作。

4.2文本读写(写法比较简单)

class Date
{
	friend ostream& operator << (ostream& out, const Date& d);
	friend istream& operator >> (istream& in, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	operator bool()
	{
		// 这里是随意写的,假设输入_year为0,则结束
		if (_year == 0)
			return false;
		else
			return true;
	}
private:
	int _year;
	int _month;
	int _day;
};
istream& operator >> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}
ostream& operator << (ostream& out, const Date& d)
{
	out << d._year << " " << d._month << " " << d._day;
	return out;
}

struct ServerInfo
{
	//char _address[32];
	string _address;
	int _port;
	Date _date;
};
class ConfigMange
{
public:
	ConfigMange(const char* filename)
		:_filename(filename)
	{}
	void WriteText(const ServerInfo& info)
	{
		ofstream ofs(_filename);
		//写的时候必须给空格或换行
		ofs << info._address << endl;
		ofs << info._port << endl;
		//ofstream是ostream的子类,子类对象可以调用继承于父类的流插入和流提取
		ofs << info._date << endl;//只要重载自定义类型的流插入和流提取就能这么写
	}
	void ReadText(ServerInfo& info)
	{
		ifstream ifs(_filename);
		ifs >> info._address;
		ifs >> info._port;
		ifs >> info._date;
	}
private:
	string _filename;
};
int main()
{
	ConfigMange cm("filename.txt");
	ServerInfo winfo = { "111111111111111111111",8080 ,{2022,1,1} };
	cm.WriteText(winfo);

	ServerInfo rinfo;
	cm.ReadText(rinfo);
	cout << rinfo._address << endl;
	cout << rinfo._port << endl;
	cout << rinfo._date << endl;
	return 0;
}

        1、使用ofstream进行写入的时候,每一个变量写完必须给空格或换行,标定每个变量的读取结束,否则读取时会读取出错。

        2、自定义类型也可以使用流插入和流提取的写法是因为ofstream和ifstream是ostream的子类,子类对象可以调用继承于父类的流插入和流提取。(前提是自定义类型重载了流插入和流提取)

5、使用stringstream序列化和反序列化(转字符串)

5.1使用ostringstream和istringstream

不过stringstream兼具ostringstream和istringstream的功能,一般都用stringstream。

猜你喜欢

转载自blog.csdn.net/gfdxx/article/details/130451066