多线程学习之二

1)线程的一些常识

1)进程:就是运行起来的可执行程序;
2)线程:一条代码的执行通路;
3)全局变量,指针,引用都可以在线程之间传递;
4)主线程从main函数开始,子线程也得从函数开始,一旦这个函数运行完毕,就代表我们这个线程结束!
5)在线程中,join是有【汇合】等待的意思,延伸为等待,就是主线程要等待子线程汇合后再执行,所以主线程要阻塞!
      join这个函数很重要,决定了程序的执行顺序;
     传统多线程程序是主线程要等待子线程执行完后,自己最后退出;
6)为什么引入detach(),因为一个主线程等待多个子线程执行完再继续执行,编程方法过于保守;
      detach分离后,子线程就会驻留在后台运行,这个子线程相当于被C++运行时库接管,当子线程运行完,运行时库清理该子线程的资源!
      驻留到后台的线程叫【守护线程】;
7)   joinable()判断是否使用join()或者detach();
8)  lambda表达式作为线程参数
9)  在写线程的时候,当处理子线程和主线程的关系的时候,时刻想着join和detach这两个函数,时刻要明确,用detach是否安全!
int main() //上述代码
{
    
    
	auto fun = []() {
    
    cout << "12344444" << endl; };
	std::thread th(fun);
	th.join();
	return 0;
}
9)类对象(仿函数)作为线程参数
class A
{
    
    
public:
	A(int tmp):m_(tmp){
    
     cout << "构造函数" << endl; }
	A(const A& obj):m_(obj.m_)
	{
    
    
		cout << "拷贝构造函数"<< endl;
	}
	~A()
	{
    
    
		cout << "析构函数---" << endl;
	}
	void operator()() //不能带参数!!!否则调用不起来
	{
    
    
		cout << "***********"<< endl;
	}
private:
	int& m_;
};
int main() //上述代码
{
    
    
	int num = 6;
	A a1(num);
	std::thread th(a1);//这里执行了拷贝构造函数
	//th.join();
	th.detach();
	cout << "main thread-------" << endl;
	return 0;
}

上述代码,可能有个疑问,一旦detach(), 那主线程结束了,则对象a1是否存在?
答案:这个对象实际是被【复制】到线程中去的,执行完主线程,a1确实会销毁,但复制的对象依然存在!
上述代码输出结果:
构造函数
拷贝构造函数
***********
析构函数--- //复制对象(线程的对象)
main thread-------
析构函数--- //主线程内a1对象

2)线程的参数使用

1)传递临时对象作为线程参数
//以下myprint函数中,主线程的buf地址和args地址是一个地址,当主线程detach的时候,会造成严重问题!所以绝对不能用指针
//修改为创建线程时,用string创建一个临时对象,在线程函数中,用string接这个临时对象就可以了;
//不用临时对象,则构造函数是在子线程进行的,是不安全的;用了临时对象,构造函数是在主函数执行的,所以是安全的。
//args要用引用来接,否则会再次调用一次拷贝构造函数,浪费!
//终极结论:尽可能用join,万不得已用detach;
void myprint(const int i, const string& args)  //char* args,这样写时错误的!
{
    
    
	cout << i << endl;
	cout << args.c_str() << endl;
	return;
}

int main() //上述代码
{
    
    
	int var = 1;
	int& varref = var;
	char buf[] = "i love china";
	//这里虽然是引用,但也是按值传递的【varref和参数i不是一个地址】,但老师也不建议这么写!!!
	std::thread th(myprint, varref, std::string(buf));  //直接写buf作为字符串参数也错误的,必须用创建临时对象作为参数;
	th.join();
	cout << "main thread------" << endl;
	return 0;
}

2)为什么要非得创建临时对象后,才可以作为线程参数,测试代码如下:
class A
{
    
    
public:
	A(int a) :a_(a) {
    
     cout << "a constructor--" << endl; }
	A(const A& obj) :a_(obj.a_) {
    
     cout << "a copy constructor--" << endl; }
	~A() {
    
     cout << "a deConstructor--" << endl; }
	int a_;
};

void printMsg(int i, const A& arg)
{
    
    
	cout << i << endl;
	cout << arg.a_ << endl;
}

int main()
{
    
    
	std::thread th(printMsg, 12, A(67));
	th.detach();
	cout << "main------thread" << endl;
	return 0;
}


2)传递类对象,智能指针作为线程参数
      用对象作为参数,在创建线程的时候,当子线程结束后,子线程的数据不会影响到主线程,因为都是按值传递的;
     如果真的想把子线程的数据返回给主线程,需要用std::ref()这个函数,才可以,这样子线程的计算数据会返回给主线程;
void printMsg(const A& arg) //这里为什么必须是const呢
{
    
    
	arg.a_ = 56;
	cout << arg.a_ << endl;
}

int main()
{
    
    
	A a1(3);
	std::thread th(printMsg, std::ref(a1)); //std::ref的用法
	th.join();
	cout << "main------thread a1.a_: "<<a1.a_ << endl;
	return 0;
}

//智能指针作为线程参数,注意事项:必须用join等待,用detach一定是错误的,因为ptr地址和arg地址是同一个地址!
void printMsg(std::unique_ptr<int> arg){
    
    }

int main()
{
    
    
	std::unique_ptr<int> ptr(new int(10));
	std::thread my(printMsg, std::move(ptr));
	my.join();
	return 0;
}

3)用vector创建多个线程,用容器管理线程

void func(int i) //线程入口函数
{
    
    
	cout << "thread is create,i is: " << i << endl;
}

int main()
{
    
    
	std::vector<std::thread> vth;
	for (int i = 0; i < 10; i++)
	{
    
    
		vth.push_back(std::thread(func, i)); //线程临时对象
	}
	for (auto iter = vth.begin(); iter != vth.end(); iter++)
	{
    
    
		iter->join();
	}
	cout << "main---------thread over" << endl;
	return 0;
}

4)创建多个线程,数据共享分析,共享数据的保护案例代码
4.1)直接使用互斥量

2)数据共享分析
2.1)只读数据:是安全稳定的,不需要特别处理手段!
2.2)又读又写:需要特殊处理,	最简单处理,能读时不能写,能写时不能读;
class A
{
    
    
public:
	//把收到的消息(玩家命令)插入到容器中的线程
	void inQueue()//成员函数入口
	{
    
    
		for (int i = 0; i < 10000; i++)
		{
    
    
			cout << "insert element " << i << endl;
			rlist.push_back(i);
		}
	}
	//从容器取出线程
	void outQueue()
	{
    
    
		for (int i = 0; i < 10000; i++)
		{
    
    
			if (!rlist.empty())
			{
    
    
				int cmd = rlist.front();//返回第一个元素,但不检查第一个元素是否存在
				rlist.pop_front();//移除第一个元素,但不返回
			}
			else
			{
    
    
				cout << "outQueue thread handle--but empty" << i << endl;
			}
		}
	}
private:
	std::list<int> rlist;//共享容器,代码玩家发过来的命令
};

int main()
{
    
    
	A obj;
	std::thread thOut(&A::outQueue, &obj);
	std::thread thin(&A::inQueue, &obj);
	thOut.join();
	thin.join();

	return 0;
}
上述代码运行时会出现错误,主要是两个线程(读线程和写线程)同时操作共享数据,造成的,如果解决,需要互斥量mutex;

4.2)std::mutex与std::lock_guard的配合使用

mtx.lock(); //如果锁成功,则返回;不成功,则一直等待
      std::lock_guard是类模板,如果使用了lock_guard, 就不能在使用mtx.lock和mtx.unlock; std::lock_guard<std::mutex> lock(mtx);
class A
{
    
    
public:
	//把收到的消息(玩家命令)插入到容器中的线程
	void inQueue()//成员函数入口
	{
    
    
		for (int i = 0; i < 10000; i++)
		{
    
    
			cout << "insert element " << i << endl;
			std::lock_guard<std::mutex> lock(mtx);
			//mtx.lock(); //如果锁成功,则返回;不成功,则一直等待
			rlist.push_back(i);
			//mtx.unlock();
		}
	}
	//从容器取出线程
	void outQueue()
	{
    
    
		for (int i = 0; i < 10000; i++)
		{
    
    
			if (!rlist.empty())
			{
    
    
				//mtx.lock();
				std::lock_guard<std::mutex> lock(mtx);
				//构造函数里执行了mtx.lock;
				int cmd = rlist.front();//返回第一个元素,但不检查第一个元素是否存在
				rlist.pop_front();//移除第一个元素,但不返回
				//mtx.unlock();
				//出作用域的时候,析构函数执行了mtx.unlock;
			}
			else
			{
    
    
				cout << "outQueue thread handle--but empty" << i << endl;
			}
		}
	}
private:
	std::list<int> rlist;//共享容器,代码玩家发过来的命令
	std::mutex mtx;
};

int main()
{
    
    
	A obj;
	std::thread thOut(&A::outQueue, &obj);
	std::thread thin(&A::inQueue, &obj);
	thOut.join();
	thin.join();
	cout << "main thread is over--------" << endl;

	return 0;
}

4.3)unique_lock代替lock_guard的使用

unique_lock取代lock_guard
  6.1)缺省值时,unique_lock和lock_guard是完全一样的用法;std::unique_lock<std::mutex> unique_guard1(mtx1);
        std::lock_guard<std::mutex> guard1(mtx1);
  6.2)unique_lock的第二参数;std::adopt_lock,std::defe_lock,std::try_to_lock等起到标记作用!!!注意没有括号,不是成员函数
      adopt_lock使用前提是提前加锁,defer_lock是不能提前加锁!调用unique_guard1.lock()手动加锁。
      unique_lock中,std::try_to_lock是尝试加锁,用owns_lock()这个成员函数做判断!
      unique_lock的成员函数:lock()unlock();   try_lock();(注意:try_lock()也要和defer_lock()一起使用);
      try_lock()函数用法和std::try_to_lock参数用法类似,只不过一个用成员函数实现,一个用参数实现。
      release(),返回mutex对象指针,并释放所有权。也就是说unique_lock和mutex不再有任何关系,如果原有对象mutex处于加锁状态,你有责任接管并解锁;
      相关的代码如下:
	std::unique_lock<std::mutex> uniqueGuard(mtx1);
	std::mutex* mtxptr = uniqueGuard.release();
	mtxptr->unlock();
6.3) unique_lock所有权的传递:
    通常情况下,unique_lock和mutex是配合实现的,类似于lock_gurard和mutex一样。
    std::unique_lock<std::mutex> uniqueGuard(mtx1);
    std::unique_lock<std::mutex> uniqueGuard2(std::move(uniqueGuard)); //把uniqueGuard的所有权转移到uniqueGuard2中,uniqueGuard为空;
    总结:数据保护说白了,就是何时lock,何时unlock,都有哪些使用方法;

猜你喜欢

转载自blog.csdn.net/qq_30143193/article/details/132553882