1.多线程概念
程序:程序就是指令和数据的结合,是静态,如果不运行的话,啥都做不了。 进程:进程就是程序的执行过程,即运行中的程序,是动态的,此时占用了操纵系统的资源。 线程:线程是进程中的实际运行单位,也是操作系统进行调度运算的最小单位。 每个进程至少有一个线程,当然可能不止一个,那就是多线程。 多线程指的就是一个进程中同时有多个线程在执行。
2.thread类
C++11开始提供专门的线程类thread,可以让我们很方便地编写多线程程序,并且可以跨平台运行。需要包含头文件#include 在之前,要想做多线程编程,只能调用操作系统的多线程接口,最大的问题是不能跨平台。
thread的相关函数: 构造函数: 1.thread();无参构造,默认构造函数,构造的是一个空线程对象,即这个线程没有关联绑定任何任务,任务即函数。 2.thread t(func);这个线程对象绑定了一个函数func,但它不会立刻执行,而需要我们手动让它执行,手动执行的方式有两种:阻塞执行join 和 分离执行detach
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <vector>
#include <condition_variable>
using namespace std;
//线程绑定的函数
void fun1(int n)
{
for (int i = 0; i < 5; i++)
{
cout << "fun1执行,参数:" << n << endl;
this_thread::sleep_for(chrono::milliseconds(500));//当前线程休眠500ms
}
}
void fun2(int& n)
{
for (int i = 0; i < 5; i++)
{
cout << "fun2执行,参数:" << n << endl;
n++;
this_thread::sleep_for(chrono::milliseconds(500));//当前线程休眠500ms
}
}
void test01()
{
int n = 0;
thread t1;//无参构造,没有绑定函数,不会执行
cout << "t1=" << t1.get_id() << endl;//0
thread t2(fun1, n);//t2线程马上开始执行
cout << "t2=" << t2.get_id() << endl;//这里可以看到t2的id
thread t3(fun2, ref(n));//fun2需要引用传参,用ref函数来实现
cout << "t3=" << t3.get_id() << endl;//t3也有id
thread t4(move(t3));//移动构造,使用move函数,t3转移给t4了
cout << "t3=" << t3.get_id() << endl;//0
cout << "t4=" << t4.get_id() << endl;//t4的id就是原来t3的id
//为了保证线程正常执行,使用阻塞或者分离的方式
t2.join();
t4.join();
cout << "t2和t4已经执行完毕,n=" << n << endl;
cout << "t2=" << t2.get_id() << endl;//t2的id=0,证明已经结束了
cout << "t4=" << t4.get_id() << endl;//t4的id=0,证明已经结束了
}
阻塞执行:使用join()函数来完成阻塞执行,阻塞就是在某处停下来,指的是当前线程必须执行完毕后,才能继续往下执行。比如说,我们在main函数中构造了这个线程t,main函数有自己线程,它是父线程,t是子线程,子线程的控制权是属于父线程的,父线程可以杀死它。如果我们希望子线程 t 可以顺利地执行完毕,即它绑定的任务函数func返回结果了,然后再往下执行main函数线程的代码,那就需要阻塞的方式来执行。不然如果main函数的线程先执行完毕了,就会杀掉子线程,这样任务就没办法保证完成。 分离执行:使用detach()来实现,即让子线程和父线程分离开,主线程就不会取得对子线程的控制权,它们各自执行完毕即可。这种方式也可以叫做守护线程。如果我们给线程绑定了任务函数,但是又不选择阻塞或分离的方式去执行,我们的程序最终会抛出异常。
3.thread t(func,args1,args2...);这个线程关联了func函数,同时给func函数提供了参数args1, args2...这种情况下,线程会马上执行,但我们仍然需要使用阻塞或者分离的方式,保证任务可以执行完毕。 4.拷贝构造是被禁用的,意味着线程是无法拷贝的。 5.移动构造:thread t(thread&& other);即转移线程对象的所有权,转移所有权之后,原来的线程就会消失,新线程会接着做原来的任务。
其他函数: 1.get_id():获取线程id 2.joinable():返回bool值,用来判断一个线程是否已经join或者detach了 3.join():阻塞执行 4.detach():分离执行 5.关于休眠的函数:让某个线程先休息一会。使当前线程休眠一定时间:this_thread::sleep_for(chrono::milliseconds(100));休眠100ms
void test04()
{
thread t(fun3);
cout << "joinable:" << t.joinable() << endl;//1
t.join();
cout << "joinable:" << t.joinable() << endl;//0
}
3.线程互斥
//3.线程互斥
int num = 0;
mutex num_lock;//定义一个互斥锁对象
atomic_int sum = 0;//这是原子整型,不加锁也可以在多线程程序下安全运行
void fun5()//不加锁,操作num变量,此时的操作在多线程下是不安全的
{
for (int i = 0; i < 5; i++)
{
cout << this_thread::get_id() << ":" << ++num << endl;
}
}
void test05()
{
thread t1(fun5);
thread t2(fun5);
thread t3(fun5);
t1.join();
t2.join();
t3.join();
}
小结
1.先创建的线程不一定跑得快,线程的运行是有偶然性的,没有规律的。
2.线程任务函数返回后,线程将终止,id变为0
3.如果主线程退出,子线程没有阻塞或分离,会引发异常,此时线程也被强行中止
4.阻塞执行可以保证子线程执行完毕后,主线程才可以执行。
5.分离执行,主线程和子线程互不影响,都可以执行完毕。
3.线程互斥
线程锁和原子类
多线程程序运行的时候,需要考虑线程安全问题,即我们需要考虑在多线程运行下的某些代码是否存在一种竞争条件?如果存在这种竞争条件,就认为这些代码不是线程安全的,这种存在竞争条件的代码可以叫做临界区代码或者关键代码段。什么是竞争条件呢?即为了保证逻辑的正确性或者数据的可靠性,我们需要在执行某一段代码前,让某个线程独占一些资源,这样才可以保证程序的正确性。 线程互斥即不允许多个线程同时操作关键代码段,只能有一个线程独占资源。这是通过加锁来实现的,可以使用头文件中的锁,可以通过mutex锁对象的lock方法加锁,再通过unlock方法解锁。这样其他线程才能继续拿锁来独占资源去执行。
另外一个保证多线程资源可靠性的技术,是原子操作类,即一些特殊设计的数据类型,这些数据类型可以从二进制层面保证数据的可靠性,这些数据类型可以在 不加锁的情况下,直接运行在多线程程序中,可以确保数据的可靠性。它用到的技术叫做CAS技术,CAS是比较和交换的缩写。需要包含头文件
//3.线程互斥
int num = 0;
mutex num_lock;//定义一个互斥锁对象
atomic_int sum = 0;//这是原子整型,不加锁也可以在多线程程序下安全运行
void fun5()//不加锁,操作num变量,此时的操作在多线程下是不安全的
{
for (int i = 0; i < 5; i++)
{
cout << this_thread::get_id() << ":" << ++num << endl;
}
}
void test05()
{
thread t1(fun5);
thread t2(fun5);
thread t3(fun5);
t1.join();
t2.join();
t3.join();
}
void fun6()//加锁,保证资源独占,此时操作是安全的
{
num_lock.lock();//这里加锁了
for (int i = 0; i < 5; i++)
{
cout << this_thread::get_id() << ":" << ++num << endl;
}
num_lock.unlock();//用完之后,解锁,让其他线程拿锁
}
void test06()
{
thread t1(fun6);
thread t2(fun6);
thread t3(fun6);
t1.join();
t2.join();
t3.join();
}
线程锁和原子类的区别
//模拟一个大量操作,演示一下普通int和原子整型的区别
void fun_int()//操作int类型,不加锁,多线程不安全
{
for (int i = 0; i < 10000; i++)
{
num++;
}
}
void test07()
{
//准备100个线程同时执行,每个线程执行1万次,最终的结果应该是100万
vector<thread> vec;
vec.reserve(100);
for (int i = 0; i < 100; i++)
{
vec.push_back(thread(fun_int));
}
for (int i = 0; i < 100; i++)
{
vec[i].join();
}
cout << "num=" << num << endl;//此处结果应该是100万。但是可能不是100万,因为出错了。
}
void test08()
{
//准备100个线程同时执行,每个线程执行1万次,最终的结果应该是100万
vector<thread> vec;
vec.reserve(100);
for (int i = 0; i < 100; i++)
{
vec.push_back(thread(fun_atomic));
}
for (int i = 0; i < 100; i++)
{
vec[i].join();
}
cout << "sum=" << sum << endl;//此处结果应该是100万。
}
4.线程同步
除了线程互斥之外,有时候我们还需要让某个线程等待另外一个线程的执行结果才能继续往下执行,这就是线程同步。这样它们之间就需要一种同步通信机制。这个通信是通过一个条件变量来实现的,在头文件中。线程同步中也要配合线程的互斥。我们通过生产者-消费者模型来演示线程同步机制。一个线程扮演生产者,一个线程扮演消费者。生产者生成一个产品后,就等着消费者去消费,消费者消费完这个产品之后,就需要通知生产者去生产,此时消费者等待,等到生产者生产出来产品后,再通知消费者去消费。
//线程同步:生产者消费者模型
mutex g_mtx;//互斥锁
condition_variable g_convar;//条件变量
vector<int> g_vector;//存放产品的容器,这个容器作为生产者和消费者共享的资源,所以对容器的操作需要加锁
//生产者函数
void producter()
{
//一共只生产10个产品就结束
for (int i = 0; i < 10; i++)
{
//第一步就是加锁,加锁之后再去操作容器
//注意:这里需要用到一个智能锁技术,类似于智能指针的技术,它可以帮我们自动加锁、自动解锁。
unique_lock<mutex> lock(g_mtx);
//接下来写生产逻辑,容器中如果有产品,就不需要生产,只需要等待,否则才需要生产
while (!g_vector.empty())
{
//使用条件变量来控制等待的过程
g_convar.wait(lock);//wait函数完成三个动作:1.先释放锁 2.阻塞再这里,直到消费者发来通知 3.再去拿锁,进行生产
}
//跳出循环后,代表容器中已经没有产品了,就该生产了
g_vector.push_back(i);
cout << "生产者生成了产品:" << i << endl;
g_convar.notify_all();//通知消费者来消费
this_thread::sleep_for(chrono::milliseconds(500));
}
}
//消费者函数
void consumer()
{
for (int i = 0; i < 10; i++)
{
unique_lock<mutex> lock(g_mtx);
//作为消费者,如果容器中没有产品,就需要等待,有产品才去消费
while (g_vector.empty())
{
g_convar.wait(lock);
}
//跳出循环后,代表有产品了,可以去消费了
cout << "消费者消费了产品:" << g_vector.back() << endl;
g_vector.pop_back();//消费了产品
g_convar.notify_all();//通知生产者去生产
this_thread::sleep_for(chrono::milliseconds(500));
}
}
void test09()
{
thread t_p(producter);//生产者线程
thread t_c(consumer);//消费者线程
t_p.join();
t_c.join();
}
总结
多线程适合需要并发执行多个任务的场景,比如以下: 1)web服务器开发,多线程可以处理多个客户端的并发连接请求。提高服务器的处理能力。 2)涉及到大量并行计算的任务,比如说图形处理,AI的算法处理,主要强调多核心的并行处理能力。 3)游戏开发:游戏中用到了很多多线程程序。 4)爬虫开发:爬虫是使用程序专门从网络上收集数据处理数据的。 5)工业领域:为了保证数据采集、传输、处理的高效,也会用到大量的多线程。
作业
/*1.线程模拟售票,简单模拟一下火车票的售票系统:
有100张从北京到西安的火车票,在5个窗口同时出售(每个窗口是一个子线程),
要保证票的数量始终是对的(不能卖超,票的数量要对的上)。
备注:使用线程互斥*/
mutex mutx;//互斥锁
int ticket = 100;//100张票
//售票函数
void sell()
{
while (ticket>0)//这个判断不可靠,因为此时没有锁,ticket同时被五个线程操作。
{
mutx.lock();//卖票之前加锁
//加锁之后,再进行一次判断,这次判断才是可靠的
if (ticket>0)
{
cout << "售票窗口" << this_thread::get_id() << ":";
ticket -= 1;
cout << "售出1张票,还剩" << ticket << "张票" << endl;
}
else
{
cout << "售票窗口" << this_thread::get_id() << ":票已售完" << endl;
}
mutx.unlock();//解锁,其他线程拿锁
this_thread::sleep_for(chrono::milliseconds(100));
}
}
void test10()
{
thread arr[5];
for (int i = 0; i < 5; i++)
{
arr[i] = thread(sell);
}
for (int i = 0; i < 5; i++)
{
arr[i].join();
}
}
/*2、编写一个程序,开启三个线程,这三个线程按照顺序依次报数:1 2 3,
一共报数3轮后结束,最后结果是1 2 3 1 2 3 1 2 3
备注:使用线程同步。*/
//思路,报数之前如何能知道改报几?需要有一个变量来记录
mutex mtx;//互斥锁
condition_variable cv;//条件变量
int ready = 1;//记录变量,表示该报几
void func1()//报1的函数
{
unique_lock<mutex> ul(mtx);
for (int i = 0; i < 3; i++)//共报三轮
{
while (ready!=1)
{
cv.wait(ul);
}
//开始报数
cout << "1" << " ";
ready = 2;//再将变量设置为下一个需要报的数
cv.notify_all();//通知其他线程
this_thread::sleep_for(chrono::milliseconds(500));
}
}
void func2()//报2的函数
{
unique_lock<mutex> ul(mtx);
for (int i = 0; i < 3; i++)//共报三轮
{
while (ready != 2)
{
cv.wait(ul);
}
//开始报数
cout << "2" << " ";
ready = 3;//再将变量设置为下一个需要报的数
cv.notify_all();//通知其他线程
this_thread::sleep_for(chrono::milliseconds(500));
}
}
void func3()//报3的函数
{
unique_lock<mutex> ul(mtx);
for (int i = 0; i < 3; i++)//共报三轮
{
while (ready != 3)
{
cv.wait(ul);
}
//开始报数
cout << "3" << " ";
ready = 1;//再将变量设置为下一个需要报的数
cv.notify_all();//通知其他线程
this_thread::sleep_for(chrono::milliseconds(500));
}
}
void test11()
{
thread t1(func1);
thread t2(func2);
thread t3(func3);
t1.join();
t2.join();
t3.join();
}