C++11线程池、多线程编程(附源码)
Test1
示例源码展示:
#include<iostream>
#include<thread>
#include<string>
using namespace std;
void printHelloWord(string s)
{
cout << s << endl;
//return;
}
int main()
{
string s;
s = "wegfer";
thread thread1(printHelloWord,s);
//thread1.join();//在线程没有结束的情况下保持主线程一直工作着,不会往下运行导致主线程结束了,线程还没结束。
//thread1.detach();//分离子线程和主线程,主线程结束了子线程还在后台运行,也就是成为孤儿进程
bool isJoinable = thread1.joinable();//返回一个布尔值,表明这个线程是不是可以调用join或者detach功能,一旦线程用过join或者detach功能,返回值就是0
cout << isJoinable << endl;//输出结果1
if (isJoinable)
{
thread1.join();
}
bool isJonable_2 = thread1.joinable();
cout << isJonable_2 << endl;//输出结果0
system("pause");
return 0;
}
函数:
- .join():阻塞线程,当线程没有运行结束的时候主线程一直停着不关闭,防止子线程还没运行完,主线程结束了报错
- .detach():分离子线程和主线程,主线程结束了子线程还在后台运行,也就是成为孤儿进程
- joinable():返回一个布尔值,表明这个线程是不是可以调用join或者detach功能,一旦线程用过join或者detach功能,返回值就是0
Text 02 线程函数中的数据未定义错误
1.传递临时变量问题
void foo(int & x)
{
x = x + 1;
}
void test02()
{
int a = 1;
/*
thread t(foo, a);
t.join();
cout << a << endl;//运行报错,因为t(foo, a)默认情况下传的是值,不是引用,改成ref(a)就可以
*/
thread t(foo, ref(a));
t.join();
cout << a << endl;//输出结果2
}
2.传递指针或引用指向局部变量的问题
#include<iostream>
#include<thread>
#include<string>
using namespace std;
int a = 1;
thread t;
void fool(int &x)
{
x = x + 1;
}
void test()
{
//int a = 1;//会报错,但也不一定,不同环境下有可能也不会,但是这是不安全的情况,所以最好还是放外面
t = thread(fool, ref(a));//a在栈里面,如果test先于foo执行完毕,那么a就释放掉了,线程就报错了。把a放到外面就好了
}
int main()
{
test();
t.join();
system("pause");
return 0;
}
3.传递指针或引用已释放的内存的问题
void fool(int *x)
{
*x = *x + 1;
}
int main()
{
int *ptr = new int(1);
thread t(fool, ptr);//这种代码崩溃不崩溃看运气,而且还会出现可能不崩溃,但是已经不按照预想的情况运行了,所以不要这么写
delete ptr;//这里已经释放了,万一线程还没运行完,他在去这里找,已经不确保正确性了
t.join();
system("pause");
return 0;
}
4.类成员函数作为入口函数,类对象被提前释放
类似于上面的3
解决办法:智能指针
#include<iostream>
#include<thread>
#include<string>
using namespace std;
class A {
public:
void foo()
{
cout << "Hello" << endl;
}
};
int main()
{
shared_ptr<A> a = make_shared<A>();
thread t(&A::foo, a);//当使用 std::thread 来执行成员函数时,调用类的成员函数与调用普通的函数不一样。成员函数需要一个对象实例来调用,因为它是绑定到特定对象的,因此在调用时必须传递一个指向该对象的指针或引用,所以这里A::foo用引用
t.join();
system("pause");
}
5.入口为类的私有成员函数
一下报错,无法运行
class A {
private:
void foo()
{
cout << "Hello" << endl;
}
};
int main()
{
shared_ptr<A> a = make_shared<A>();
thread t(&A::foo, a);//当使用 std::thread 来执行成员函数时,调用类的成员函数与调用普通的函数不一样。成员函数需要一个对象实例来调用,因为它是绑定到特定对象的,因此在调用时必须传递一个指向该对象的指针或引用
t.join();
system("pause");
}
解决办法:友元,或者通过调用类内的公有函数在调用私有函数
class A {
private:
friend void thread_foo();
void foo()
{
cout << "Hello" << endl;
}
};
void thread_foo()
{
shared_ptr<A> a = make_shared<A>();
thread t(&A::foo, a);//当使用 std::thread 来执行成员函数时,调用类的成员函数与调用普通的函数不一样。成员函数需要一个对象实例来调用,因为它是绑定到特定对象的,因此在调用时必须传递一个指向该对象的指针或引用
t.join();
}
int main()
{
thread_foo();
system("pause");
}
test03 互斥量解决多线程数据共享问题
下面这样的代码又安全隐患,有可能是我们想要的20000,也有可能不是。
int a = 0;
void func()
{
for (int i = 0; i < 10000; i++)
{
a = a + 1;
}
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
system("pause");
return 0;
}
解决办法:加锁,互斥锁
int a = 0;
mutex mtx;
void func()
{
for (int i = 0; i < 10000; i++)
{
mtx.lock();//加锁
a = a + 1;
mtx.unlock();//解锁
}
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
system("pause");
return 0;
}
test5 lock_guard与unique_lock
lock_guard: C++标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致数据竞争问题。
特点:构造函数被调用时,该互斥量会被自动锁定
析构函数被调用时,该互斥量会被自动解锁
lock_guard对象不能复制或移动,因此它只能在 局部作用域中使用
void func()
{
for (int i = 0; i < 10000; i++)
{
lock_guard<mutex> lg(mtx);//lock_guard标准库中的类,可以管理锁的自动释放。它会在构造函数中自动尝试锁定 mtx,并在该作用域结束时,自动调用析构函数来释放 mtx 锁。这一轮训话结束就自动解锁了
a = a + 1;
}
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
system("pause");
return 0;
}
unique_lock: C++标准库中的一个互斥封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更灵活的管理,包括延迟加锁、条件变量、超时等。
unique_lock:提供的成员函数:
lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程被阻塞,直到互斥量被成功加锁。
try_lock():尝试对互斥量进行加锁,如果当前互斥量已经被其他线程持有,则函数立刻返回false,否则返回true。
try_lock_for():尝试对互斥量进行加锁,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。
tyr_lock_until():尝试对互斥量进行加锁操作,如果互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加速,或者超过了指定的时间。
unlock():对互斥量进行解锁操作。
void func()
{
for (int i = 0; i < 10000; i++)
{
unique_lock<mutex> lg(mtx);//也会成功加锁
a = a + 1;
}
}
void func()
{
for (int i = 0; i < 10000; i++)
{
unique_lock<mutex> lg(mtx,defer_lock);//成功构造但是不加锁
a = a + 1;
}
}
输出结果:
try_lock_for():
int a = 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)))//等待一段时间,这段时间等不到就直接返回,等到了就运行获取值。
{
std::this_thread::sleep_for(chrono::seconds(3));
a = a + 1;
}//延迟加锁,5秒内没加上就不加了
}
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;
system("pause");
return 0;
}
锁只是不允许获取资源,不是让你加不上锁就直接返回结束你的线程,而是加不上锁的话那就返回false,然后继续后续操作。
int a = 0;
timed_mutex mtx;
void func()
{
for (int i = 0; i < 2; i++)
{
unique_lock<timed_mutex> lg(mtx, defer_lock);//成功构造但是不加锁
lg.try_lock_for(chrono::seconds(2));//延迟加锁,5秒内没加上就不加了
std::this_thread::sleep_for(chrono::seconds(3));
a = a + 1;
}
}
int main()
{
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << a << endl;//输出结果为4
system("pause");
return 0;
}
Test06 call_once与其使用场景
单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全问题。
最常见的是日志类,全局只有一个日志类,日志类可以打印各种信息。
只可以在线程函数中使用call_once
生产者-消费者问题的实现:
#include<iostream>
#include<thread>
#include<string>
#include<mutex>
#include<condition_variable>
#include<queue>
#include<vector>
#include<functional>
using namespace std;
queue<int> g_queue;
condition_variable g_cv;
mutex mtx;
void Producer()
{
for (int i = 0; i < 10; i++)
{
unique_lock<mutex>lock(mtx);
g_queue.push(i);
//队列为空之后加任务
g_cv.notify_one();
cout << "task:"<<i << endl;
}
}
void Consumer()
{
while (1)
{
unique_lock<mutex>lock(mtx);
bool isempty = g_queue.empty();
//g_cv.wait(lock, !isempty);//队列不空的时候消费者继续取
g_cv.wait(lock, []()
{
return !g_queue.empty();
});
int value = g_queue.front();
g_queue.pop();
cout << "value: " << value << endl;
}
}
int main()
{
thread t1(Producer);
thread t2(Consumer);
t1.join();
t2.join();
system("pause");
return 0;
}
运行结果:
**注:**也有可能是生产者生产完了商品消费者才开始消费,这是因为生产者和消费者是独立的两个线程,他们的运行顺序并不总是由我们决定的,操作系统可能会调度他们,有可能是操作系统衡量之后觉得应该让生产者先生产。
出现原因:1.线程调度不确定性:线程的执行顺序有操作系统调度,那次运行可能操作系统觉得应该 让生产者进行生产。
2.生产者的生产速度大于消费者消费速度,可以在生产者那里加一个sleep_for,增加生产者的生产时间(生产者只需要push,而消费者还需要wait)
Test07 生产者与消费者问题
生产者和消费者二者一个创建任务一个取走任务,他们一个操纵任务队列的头部一个操纵任务队列的尾部,任务队列需要互斥访问,1. 因为生产者、消费者对任务队列执行取出或添加的操作后需要更改队列的指针或进行一些内部结构的更新,如果不互斥有可能会使得队列崩溃。2. 防止“写-读”冲突,生产者还未完全把任务送到任务队列,消费者就已经在取这个任务了,就导致读到不完整的数据。3.防止“空队列”或者“满队列”竞争,队列是空还是满需要专门的检测,状态的检测和队列的更新是分开操作,需要分开,如果没有锁,可能队列已经满了,但是还没来得及更新状态,但是生产者取了状态信息认为没有满,然后继续添加导致溢出。同样的情况也在任务队列空的时候。
Test08 C++11跨平台线程池的实现
有一个存储着任务的队列,我们提前创建好的线程池不停的去执行这些任务,线程池存在的意义是因为线程的创建和销毁都是很耗费资源时间的,所以我提前弄好就不用消耗这些,提高效率
问题描述:
线程池的实现(使用了C++11的新特性):
#include<iostream>
#include<thread>
#include<string>
#include<mutex>
#include<condition_variable>
#include<queue>
#include<vector>
#include<functional>
using namespace std;
class ThreadPool {
public:
ThreadPool(int numThreads) :stop(false)
{
for (int i = 0; i < numThreads; i++)
{
threads.emplace_back([this]
{
while (1)
{
unique_lock<mutex>lock(mtx);
condition.wait(lock, [this]
{
return !tasks.empty() || stop;
});
if (stop && tasks.empty())
{
return;
}
function<void()> task(move(tasks.front()));
tasks.pop();
lock.unlock();
task();
}
});
}
}
~ThreadPool() {
{
unique_lock<mutex>lock(mtx);
stop = true;
}
condition.notify_all();
for (auto &t : threads)
{
t.join();
}
}
template<class F,class ... Args>
void enqueue(F && f, Args&&... args)
{
function<void()>task = bind(forward<F>(f), forward<Args>(args)...);
{
unique_lock < mutex>lock(mtx);
tasks.emplace(move(task));
}
condition.notify_one();
}
private:
vector<thread> threads;
queue<function<void()>> tasks;
mutex mtx;
condition_variable condition;
bool stop;
};
int main()
{
ThreadPool pool(4);
for (int i = 0; i < 10; i++)
{
pool.enqueue([i]
{
cout << "task: " << i << "is running " << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "task: " << i << "is done" << endl;
});
}
system("pause");
return 0;
}
运行截图:
注: 八手动解锁关了就能解决打印混乱的问题
参考文献:程序员陈子青-C++11 多线程编程-小白零基础到手撕线程池-哔哩哔哩