目录
一、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。