文章目录
多线程前导
1.产生死锁的原因:
*系统资源不足
*进程运行推进的顺序不合适
*资源分配不当
2.产生死锁的四个必要条件
*互斥条件:一个资源每次只能被一个进程使用
*请求与保持条件:一个进程因请求资源而阻塞时,对已经获得的资源保持不放
*不可剥夺条件:若干进程之间形成了一种头尾相接的循环等待资源的关系
3.死锁的解除与预防
*破坏请求条件
*破坏请保持条件
*破坏不可剥夺条件
*破坏环路等待条件
多线程
c++11提供了语言层面上的多线程,包含在头文件<thread.h>中,它解决了跨平台的问题。
提供了管理线程,保护共享数据,线程同步操作,原子操作等类,c++11新标准中引入了五个头文件来支持多线程编程
多进程并发
由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。
但是这也造就了多进程并发的两个缺点:
(1.在进程间通信,无论是使用信号,套接字,还是文件,管道等方式,其使用要么比较复杂,要么就是速度较慢
(2.运行多个线程的开销很大,操作系统要分配很多的资源来对这些进程进程管理
多线程并发
多线程并发指的是在同一个进程中执行多个线程
优点
线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在,
也就是说,同一个进程的多个线程 共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。
缺点
由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作 以保证对共享数据段的操作是以预想的操作顺序进行的,
并且要极力的避免死锁
1.thread
1.join方式,等待启动的线程完成,才会继续往下执行
#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
while(1)
{
cout<<"子线程1"<<endl;
}
}
void thread_2(int x)
{
while(1)
{
cout<<"子线程2"<<endl;
}
}
int main()
{
thread first ( thread_1); // 开启线程,调用:thread_1()
thread second (thread_2,100); // 开启线程,调用:thread_2(100)
first.join(); // pauses until first finishes 这个操作完了之后才能destroyed
second.join(); // pauses until second finishes//join完了之后,才能往下执行。
while(1)
{
std::cout << "主线程\n";
}
return 0;
}
1.2 detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束
#include <iostream>
#include <thread>
using namespace std;
void thread_1()
{
while(1)
{
cout<<"子线程1111"<<endl;
}
}
void thread_2(int x)
{
while(1)
{
cout<<"子线程2222"<<endl;
}
}
int main()
{
thread first ( thread_1); // 开启线程,调用:thread_1()
thread second (thread_2,100); // 开启线程,调用:thread_2(100)
first.detach();
second.detach();
for(int i = 0; i < 10; i++)
{
std::cout << "主线程\n";
}
return 0;
}
2.this_thread
this_thread是一个类,它有四个功能函数,具体如下:
this_thread::get_id//获取进程id
this_thread::yield()//放弃线程执行,回到就绪状态
this_thread::sleep_for(std::chrono::second(1))//暂停一秒
this_thread::sleep_until//xx时间之后执行
3.mutex
mutex头文件主要声明了与互斥量(mutex)相关的类。mutex提供了4种互斥类型,如下表所示。
mutex:最基本的mutex类
recursive_mutex:递归mutex类
time_mutex:定时mutex类
recursive_timed_mutex:递归定时mutex类
4.lock 与 unlock
lock():资源上锁
unlock():解锁资源
trylock():查看是否上锁,
(1.未上锁返回FALSE,并锁住
(2.其他线程已上锁,返回TRUE
(3.同一个线程已经对它上锁,将会产生死锁
同一个mutex变量上锁之后,一个时间段内,只允许一个线程访问它。例如
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void print_block (int n, char c)
{
mtx.lock();
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
mtx.unlock();
}
int main ()
{
std::thread th1 (print_block,50,'*');//线程1:打印*
std::thread th2 (print_block,50,'$');//线程2:打印$
th1.join();
th2.join();
return 0;
}
如果是不同mutex变量,因为不涉及到同一资源的竞争,所以下列代码运行可能会出现交替打印的情况,或者另一个线程可以修改共同的全局变量!!!:
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex
std::mutex mtx_1; // mutex for critical section
std::mutex mtx_2; // mutex for critical section
int test_num = 1;
void print_block_1 (int n, char c) {
mtx_1.lock();
for (int i=0; i<n; ++i)
{
test_num = 1;
std::cout<<test_num<<std::endl;
}
std::cout << '\n';
mtx_1.unlock();
}
void print_block_2 (int n, char c) {
mtx_2.lock();
test_num = 2;
for (int i=0; i<n; ++i)
{
test_num = 2;
std::cout<<test_num<<std::endl;
}
mtx_2.unlock();
}
int main ()
{
std::thread th1 (print_block_1,10000,'*');
std::thread th2 (print_block_2,10000,'$');
th1.join();
th2.join();
return 0;
}
5.lock_guard
创建lock_guard对象时,它将尝试获取提供它的互斥锁的所有权,当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量
lock_guard的特点:
(1.创建即加锁,作用域结束自动析构并解锁,无需手工解锁
(2.不能中途解锁,必须等作用域结束才解锁
(3.不能复制
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex g_i_mutex; // protects g_i,用来保护g_i
void safe_increment()
{
const std::lock_guard<std::mutex> lock(g_i_mutex);
++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
// g_i_mutex自动解锁
}
int main()
{
std::cout << "main id: " <<std::this_thread::get_id()<<std::endl;
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
说明;
(1.该程序的功能为,每经过一个线程,g_i加1;
(2.因为涉及到的共同资源g_i,所以需要一个共同的mutex:g_i_mutex
(3.main线程的id为1,所以下次的线程id依次加1
6.unique_lock
简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,
同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。
unique_lock的特点:
(1.创建时可以不锁定(通过指定第二个参数为std::defer_lock,而在需要时再锁定
(2.可以随时加锁解锁
(3.作用域规则同lock_guard,析构时自动释放
(4.不可复制,不可移动
(5.条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
#include <mutex>
#include <thread>
#include <iostream>
struct Box {
explicit Box(int num) : num_things{num} {}
int num_things;
std::mutex m;
};
void transfer(Box &from, Box &to, int num)
{
// defer_lock表示暂时unlock,默认自动加锁
std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
//两个同时加锁
std::lock(lock1, lock2);//或者使用lock1.lock()
from.num_things -= num;
to.num_things += num;
//作用域结束自动解锁,也可以使用lock1.unlock()手动解锁
}
int main()
{
Box acc1(100);
Box acc2(50);
std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
t1.join();
t2.join();
std::cout << "acc1 num_things: " << acc1.num_things << std::endl;
std::cout << "acc2 num_things: " << acc2.num_things << std::endl;
}
说明:
(1.该函数的作用是,从一个结构体中的变量减去一个num,加载到另一个结构体的变量中去
(2.std::mutex m 在结构体中,mutex不是共享的,但是只需要一把锁也能锁住,因为引用传递后,同一把锁传给了两个函数
(3.cout需要在join后面进行,要不然cout的结果不一定是最终算出来的结果
(4.ref用于包装按引用传递的值
(5.cref用于包装按const传递的值
7.condition_variable
condition_variable的头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any.
condition_variable必须结合unique_lock使用,condition_variable_any可以使用任何的锁,下面以condition_variable为例进行介绍
condition_variable变量可以阻塞(wait,wait_for,wait_until)调用的线程直到使用(notify_one或notigy)_all)通知恢复为止。
condition_variable是一个类,这个类既有构造函数也有析构函数,使用时要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能
类型 | 说明 |
---|---|
condition_variable | 构建对象 |
析构 | 删除 |
wait | wait until notified |
wait_for | Wait for timeout or until notified |
wait_until | Wait until notified or time point |
notify_one | 解锁一个线程,如果有多个,则未知哪个线程执行 |
notify_all | 解锁所有线程 |
cv_status | 这是一个类,表示variable 的状态,如下所示 |
enum class cv_status { no_timeout, timeout }
7.1.wait
当前线程调用 wait() 后将被阻塞
在线程被阻塞时,该函数会自动调用 unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait()函数也是自动调用 lock(),使得对象的状态和 wait 函数被调用时相同。
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::yield
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
int cargo = 0;
bool shipment_available() {return cargo!=0;}
void consume (int n) {
for (int i=0; i<n; ++i) {
std::unique_lock<std::mutex> lck(mtx);//自动上锁
//第二个参数为false才阻塞(wait),阻塞完即unlock,给其它线程资源
cv.wait(lck,shipment_available);
// consume:
std::cout << cargo << '\n';
cargo=0;
}
}
int main ()
{
std::thread consumer_thread (consume,10);
for (int i=0; i<10; ++i) {
//每次cargo每次为0才运行。
while (shipment_available()) std::this_thread::yield();
std::unique_lock<std::mutex> lck(mtx);
cargo = i+1;
cv.notify_one();
}
consumer_thread.join();,
return 0;
}
说明:
(1.主线程中的while,每次在cargo=0才运行
(2.每次cargo被置为0,会通知子线程unblock(非阻塞),也就是子线程可以继续往下执行
(3.子线程中cargo被置为0后,wait又一次启动等待,也就是说shipment_available为FALSE,则等待。
7.2.wait_for
与std::condition_variable::wait() 类似,不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。 而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。
#include <iostream> // std::cout
#include <thread> // std::thread
#include <chrono> // std::chrono::seconds
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable, std::cv_status
std::condition_variable cv;
int value;
void read_value() {
std::cin >> value;
cv.notify_one();
}
int main ()
{
std::cout << "Please, enter an integer (I'll be printing dots): \n";
std::thread th (read_value);
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
while (cv.wait_for(lck,std::chrono::seconds(1))==std::cv_status::timeout) {
std::cout << '.' << std::endl;
}
std::cout << "You entered: " << value << '\n';
th.join();
return 0;
}
说明:
通知或者超时都会解锁,所以主线程会一直打印。
事例中只要过去一秒,就会不断的打印。