网络通信开课作业c++
一、多线程编程-->同步(死锁、活锁)
死锁:指的是在多个进程或线程中,彼此持有对方想要的资源而无法继续执行的情况。在这种情况下,每个进程都在等待其他进程释放资源,导致所有进程都无法继续执行,形成了死循环。简单说,死锁就是互相卡住,无法继续往下执行。
活锁:指的是多个进程或线程在避免死锁的情况下,由于彼此的行为导致无限循环,无法向前推进。简单说,活锁是形成了一个连续的动作,但是无法达到预期结果的情况。
https://www.bilibili.com/video/BV1d841117SH/?spm_id_from=333.337.search-card.all.click&vd_source=865f32e12aef524afb83863069b036aa
1.线程库的基本使用
进程就是运行中的程序;线程就是进程中的进程
一个操作系统中可以有多个进程,同样一个进程中可以有多个线程
以下案例:本来需要通过很长时间才可以干完的事情,通过线程可以提高效率
线程的最大数量取决于cpu的核心数
(1)创建线程
要创建线程,我们需要一个可调用的函数或函数对象,作为线程的入口点。在C++11中,我们可以使用函数指针、函数对象或lambda表达式来实现。
创建线程的基本语法如下:
#include <thread>std::thread t(function_name, args...);
`function_name`是线程入口点的函数或可调用对象
`args...`是传递给函数的参数
案例:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//c++11的线程库
#include<thread>
void printHello() {
cout << "Hello" << endl;
}
int main() {
//1.创建线程
thread thread1(printHello);
return 0;
}
输出但是报错
原因:在单线程的情况下,程序从上往下走,但存在问题就是主程序不会等待线程执行完了之后才继续执行return,而可能出现比如"hello"中的“he”(打了一半),这边主程序就已经终止掉了
(2)主程序等待线程执行完毕join()
运行join之后,主程序不会立即结束,而是检查这个线程有没有执行,如果没有结束主线程也不会结束,这样就解决了上述的问题
(3)传值
我们上面提到过创建线程时候括号除了线程入口点的函数,还可以传入参数,这里我们可以把输出的内容变成我们传入的参数
(4)分离线程detach()
主线程不等待子线程结束,但是主线程结束之后子线程依然可以在后台运行,且程序不报错
主程序未打印任何程序就结束了,但程序不会报错,因为子程序在后台运行了
(5)判断join和detach是否可用 joinable()
会返回一个bool值,来判断这个线程是否可以调用join或者detach,如果不行就不能调用
bool isJoin = thread1.joinable();
if (isJoin) {
thread1.join();
}
2.线程函数中的数据未定义错误
(1)传递临时变量的问题
临时变量在使用过后会立即释放掉内存,再去拿引用就拿不到了
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//c++11的线程库
#include<thread>
void foo(int& x) {
x += 1;
}
int main() {
int a = 1;
thread t(foo, a);
t.join();
cout << a << endl;
return 0;
}
用到std::ref()
当我们用ref函数去修饰a这个局部变量的时候,就已经转换成引用类型的变量了
(2)传递指针或引用指向局部变量的问题
我们上面那个案例就是引用指向局部变量,那为什么没有报错呢?
因为这个程序的局部变量是在main函数当中,在主线程中,即a的地址是会一直存在的
那我们稍微变形一下,将线程的创建安排到一个函数当中
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//c++11的线程库
#include<thread>
thread t;
void foo(int& x) {
x += 1;
}
void test() {
int a = 1;
t = thread(foo, ref(a));
}
int main() {
test();
t.join();
return 0;
}
引发异常(空指针错误)
原因:a是局部变量,只在test的局部有效,是在栈上面。当test执行完毕调用return时,a变量的地址就已经被释放掉了
如果要解决上述问题把a放在全局区,地址一直有效
(3)传递指针或引用指向已释放的内存的问题
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
//c++11的线程库
#include<thread>
thread t;
void foo(int* x) {
cout << *x << endl;
}
int main() {
int* ptr = new int(1);
thread t(foo, ptr);
delete ptr;
t.join();
return 0;
}
和我们预期打印出来的1不一样
原因:首先我们申请了一块内存,里面的数值是1,传递给了foo函数,这个函数取这块地址的时候,立马对这块地址做了一个释放,ptr已经被释放掉了,变成了野指针,地址依然存在但已经不是1了
(4)类成员函数作为入口函数,类对象被提前释放
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <thread>
class MyClass {
public:
void func() {
cout << "Thread " << this_thread::get_id()
<< " started" << endl;
// do some work
cout << "Thread " << this_thread::get_id()
<< " finished" << endl;
}
};
int main() {
MyClass obj;
thread t(&MyClass::func, &obj);
// obj 被提前销毁了,会导致未定义的行为
return 0;
}
只是把int型的对象换成了一个类对象进行演示
obj对象的生命周期仅在main函数内,结束时候被销毁,而线程t在main函数返回之前还没有结束执行
修复:shared_ptr来管理类对象的生命周期,确保在线程执行期间对象不会被销毁。创建线程之前,将类对象的指针封装在一个shared_ptr对象中,将其作为参数传递给线程。这样,在线程执行期间,即使类对象的所有者释放了其所有权,std::shared_ptr 仍然会保持对象的生命周期,直到线程结束。(智能指针)需要结合join函数一起执行,因为shared_ptr只负责对象的生命周期管理,并不负责线程的执行同步。
shared_ptr已经是一个指针类型,所以传值obj的时候不能加上引用
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <thread>
#include<memory>
class MyClass {
public:
void func() {
cout << "Thread " << this_thread::get_id()
<< " started" << endl;
// do some work
cout << "Thread " << this_thread::get_id()
<< " finished" << endl;
}
};
int main() {
shared_ptr<MyClass>obj = make_shared<MyClass>();
thread t(&MyClass::func, obj);
t.join();
return 0;
}
(5)入口函数为类的私有成员函数
友元 是c++中一个特殊的类成员或函数,它不属于类,但可以访问类的私有和保护成员。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <thread>
class MyClass {
private:
friend void myThreadFunc(MyClass* obj);
void privateFunc() {
std::cout << "Thread "
<< std::this_thread::get_id() << " privateFunc" << std::endl;
}
};
void myThreadFunc(MyClass* obj) {
obj->privateFunc();
}
int main() {
MyClass obj;
std::thread thread_1(myThreadFunc, &obj);
thread_1.join();
return 0;
}
3.互斥量解决多线程数据共享问题
在多个线程中共享数据时,需要注意线程安全问题。如果多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,那么就会出现数据竞争问题。数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。
为了避免数据竞争问题,需要使用同步机制来确保多个线程之间对共享数据的访问是安全的。常见的同步机制包括互斥量、条件变量、原子操作等。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <thread>
int shared_data = 0;
void func() {
for (int i = 0; i < 100000; ++i) {
shared_data++;
}
}
int main() {
std::thread t1(func);
std::thread t2(func);
t1.join();
t2.join();
std::cout << "shared_data = " << shared_data << std::endl;
return 0;
}
以上代码,定义了全局变量shared_data,同时创建了两个线程进行累加操作。针对这种共享,需要通过使用互斥量等同步机制来确保多个线程之间对共享数据的访问是安全的。
互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题。提供了两个基本操作:lock()
和 unlock()
。
当一个线程调用 lock()
函数时,如果互斥量当前没有被其他线程占用,则该线程获得该互斥量的所有权,可以对共享资源进行访问。如果互斥量当前已经被其他线程占用,则调用 lock()
函数的线程会被阻塞,直到该互斥量被释放为止。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
int shared_data = 0;
mutex mtx;
void func(int n) {
for (int i = 0; i < 100; ++i) {
mtx.lock();
shared_data++;
cout << "Thread " << n
<< " increment shared_data to " << shared_data <<endl;
mtx.unlock();
}
}
int main() {
thread t1(func, 1);
thread t2(func, 2);
t1.join();
t2.join();
cout << "Final shared_data = " << shared_data << endl;
return 0;
}
4.互斥量死锁
如果有两个线程t1和t2,对两个互斥量mtx1、mtx2进行访问,需要按照下面的顺序来获取互斥量的所有权:
- T1 先获取 mtx1 的所有权,再获取 mtx2 的所有权
- T2 先获取 mtx2 的所有权,再获取 mtx1 的所有权
两个线程同时执行,就会发生死锁问题。若t1获取了mtx1的所有权,就没法获得mtx2的所有权,另一个同理,两个线程互相等待对方释放互斥量,从而导致死锁。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
mutex m1;
mutex m2;
void func_1() {
for (int i = 0; i < 50; i++) {
m2.lock();
m1.lock();
m1.unlock();
m2.unlock();
}
}
void func_2() {
for (int i = 0; i < 50; i++) {
m1.lock();
m2.lock();
m2.unlock();
m1.unlock();
}
}
int main() {
thread t1(func_1);
thread t2(func_2);
t1.join();
t2.join();
cout << "over" << endl;
return 0;
}
可以观察到程序已经卡死了
解决措施:确保所有线程都以相同的顺序锁定互斥锁
5.lock_guard 与 std::unique_lock
lock_guard
是一个c++标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一种资源而导致的数据竞争问题
-
当构造函数被调用时,该互斥量会被自动锁定。
-
当析构函数被调用时,该互斥量会被自动解锁。
-
std::lock_guard
对象不能复制或移动,因此它只能在局部作用域中使用。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
int shared_data = 0;
mutex mtx;
void func() {
for (int i = 0; i < 10000; i++) {
lock_guard<mutex> lg(mtx);
shared_data++;
}
}
int main() {
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << shared_data << endl;
return 0;
}
unique_lock
是c++标准库中提供的一种互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。
主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。
提供了以下几个成员函数:
-
lock()
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。 -
try_lock()
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回false
,否则返回true
。 -
try_lock_for(const std::chrono::duration<Rep, Period>& rel_time)
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。 -
try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time)
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。 -
unlock()
:对互斥量进行解锁操作。
除了上述成员函数外,std::unique_lock
还提供了以下几个构造函数:
-
unique_lock() noexcept = default
:默认构造函数,创建一个未关联任何互斥量的std::unique_lock
对象。 -
explicit unique_lock(mutex_type& m)
:构造函数,使用给定的互斥量m
进行初始化,并对该互斥量进行加锁操作。 -
unique_lock(mutex_type& m, defer_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,但不对该互斥量进行加锁操作。 -
unique_lock(mutex_type& m, try_to_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的std::unique_lock
对象不与任何互斥量关联。 -
unique_lock(mutex_type& m, adopt_lock_t) noexcept
:构造函数,使用给定的互斥量m
进行初始化,并假设该互斥量已经被当前线程成功加锁。
使用defer_lock需要手动进行加锁(实现延迟加锁的方案),try_lock_for尝试对互斥量进行加锁,设置时间如果超出时间就直接执行下面的程序,放弃(这里我休眠十秒可能导致线程只会执行一次) 同时mutex互斥锁也不支持延迟加锁,所以我们需要把传递的锁变成一个时间锁
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
int shared_data = 0;
timed_mutex mtx;
void func() {
for (int i = 0; i < 2; i++) {
unique_lock<timed_mutex> lg(mtx,defer_lock);
if (lg.try_lock_for(chrono::seconds(2))) {
this_thread::sleep_for(chrono::seconds(1));
shared_data++;
}
}
}
int main() {
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << shared_data << endl;
return 0;
}
6.std::call_once与其使用场景
单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。 常见的有日志类
饿汉模式的特点是类加载时就立即初始化,创建单例对象。这种方式简单,可以保证线程安全,因为实例在类加载时就已经创建好了,所以不存在多线程环境下的同步问题。但是,它的缺点是不管这个单例对象是否会被使用,都会在类加载时就创建,可能会导致不必要的内存浪费。例如,如果系统中有大量的单例对象,即使它们并未被使用,也会占用内存资源。
懒汉模式的特点是只有在需要的时候才会创建单例对象,这样可以节约资源,因为如果单例对象不被使用,就不会创建,从而避免了内存浪费。但是,懒汉模式需要处理多线程环境下的同步问题,以确保在多线程访问时,单例对象不会被创建多次。这通常通过加锁(synchronized)或其他同步机制来实现。
以下是单例设计模式的示例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
#include<string>
class Log {
public:
Log() {};
//码确保了Log类的对象不能通过拷贝构造或拷贝赋值来创建或修改,
//为了避免资源管理上的问题
Log(const Log& log) = delete;
Log& operator=(const Log& log) = delete;
static Log& GetInstance() {
//返回一个log的对象且对象静态
//全局唯一一个log
//懒汉模式 有需要才
static Log *log = nullptr;
if (!log) log = new Log;
return *log;
}
void PrintLog(string msg) {
cout << msg << endl;
}
};
int main() {
Log::GetInstance().PrintLog("error");
return 0;
}
现在是单线程,假设在多线程的情况下,可能被new两次
void print_error() {
Log::GetInstance().PrintLog("error");
}
int main() {
thread t1(print_error);
thread t2(print_error);
t1.join();
t2.join();
return 0;
}
使用 std::call_once
和 std::once_flag
来保证 Log
实例只被创建一次
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <thread>
#include <mutex>
#include<string>
class Log {
public:
Log() {};
//码确保了Log类的对象不能通过拷贝构造或拷贝赋值来创建或修改,
//为了避免资源管理上的问题
Log(const Log& log) = delete;
Log& operator=(const Log& log) = delete;
static Log& GetInstance() {
// 使用std::once_flag来保证初始化只执行一次
static std::once_flag flag;
// 静态局部变量,保证只创建一个Log实例
static Log *log = nullptr;
call_once(flag, []() {
log = new Log();
});
return *log;
}
void PrintLog(string msg) {
cout << msg << endl;
}
};
void print_error() {
Log::GetInstance().PrintLog("error");
}
int main() {
thread t1(print_error);
thread t2(print_error);
t1.join();
t2.join();
return 0;
}
7.condition_variable 与其使用场景
可以实现线程的等待和通知机制,从而在多线程环境中实现同步操作。
-
创建一个
std::condition_variable
对象。 -
创建一个互斥锁
std::mutex
对象,用来保护共享资源的访问。 -
在需要等待条件变量的地方
使用
std::unique_lock<std::mutex>
对象锁定互斥锁并调用
std::condition_variable::wait()
、std::condition_variable::wait_for()
或std::condition_variable::wait_until()
函数等待条件变量。 -
在其他线程中需要通知等待的线程时,调用
std::condition_variable::notify_one()
或std::condition_variable::notify_all()
函数通知等待的线程。
生产者和消费者模型
让消费者线程等待生产者线程生产数据后再进行消费,避免了数据丢失或者数据不一致的问题
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex g_mutex;
std::condition_variable g_cv;
std::queue<int> g_queue;
void Producer() {
for (int i = 0; i < 10; i++) {
{
std::unique_lock<std::mutex> lock(g_mutex);
g_queue.push(i);
std::cout << "Producer: produced " << i << std::endl;
}
g_cv.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
void Consumer() {
while (true) {
std::unique_lock<std::mutex> lock(g_mutex);
g_cv.wait(lock, []() { return !g_queue.empty(); });
int value = g_queue.front();
g_queue.pop();
std::cout << "Consumer: consumed " << value << std::endl;
}
}
int main() {
std::thread producer_thread(Producer);
std::thread consumer_thread(Consumer);
producer_thread.join();
consumer_thread.join();
return 0;
}
8.实现线程池
线程不支持拷贝所以只能用emplace_back,而不是pull_back
&&右值引用 一个&是左值引用(在函数模版中加两个&就是万能引用)
forward完美转发 原来是什么引用,传参后还是什么引用
有点难,不是很理解
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
//可以存储、调用函数或可调用对象
#include <functional>
#include <queue>
class ThreadPool {
public:
ThreadPool(int numThreads) : stop(false) {
for (int i = 0; i < numThreads; ++i) {
threads.emplace_back([this] {
while (true) {
std::unique_lock<std::mutex> lock(mutex);
condition.wait(lock, [this] { return stop || !tasks.empty(); });
if (stop && tasks.empty()) {
return;
}
//将 tasks.front() 返回的任务移动到一个新的临时 std::function 对象中。
//std::move 用于将对象的所有权转移给另一个对象.
//这里它将任务从队列中移动出来,而不是复制。
//目的是将任务从线程池的共享队列中取出,以便在没有锁的情况下执行。
//一旦任务被移动出队列,它就不再属于队列,因此其他线程不会再尝试执行它。
std::function<void()> task(std::move(tasks.front()));
tasks.pop();
lock.unlock();
task();
}
});
}
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(mutex);
//表示不再接受新任务
stop = true;
}
condition.notify_all();
//调用每个线程的join方法,等待所有线程完成他们的任务
for (std::thread& thread : threads) {
thread.join();
}
}
template<typename F, typename... Args>
void enqueue(F&& f, Args&&... args) {
//函数的参数和函数绑定在一起之后
//无论这个函数有多少个参数都不需要传参了,所以前面是void()
std::function<void()> task(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
{
std::unique_lock<std::mutex> lock(mutex);
tasks.emplace(std::move(task));
}
//唤醒一个等待的线程来执行新添加的任务
condition.notify_one();
}
private:
std::vector<std::thread> threads;
std::queue<std::function<void()>> tasks;
std::mutex mutex;
std::condition_variable condition;
bool stop;
};
int main() {
ThreadPool pool(4);
//添加8个任务到线程池中
for (int i = 0; i < 8; ++i) {
pool.enqueue([i] {
std::cout << "Task " << i << " is running in thread " << std::this_thread::get_id() << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Task " << i << " is done" << std::endl;
});
}
return 0;
}
二、内存映射机制
内存映射是将文件的一部分映射到进程的地址空间中,使得文件内容可以直接通过指针读写,就像操作内存一样。适合处理大型文件,因为不需要将整个文件读入内存中,而是按需加载。