目录
1. 线程池
1.1 前期代码
thread.hpp
1.2 加上锁的代码
lockGuard.hpp
1.3 加上任务的代码
1.4 加上日志的代码
log.hpp
Task.hpp
2. 单例模式的线程安全
2.1 线程池的懒汉模式
threadPool.hpp
testMain.cc
3. STL和智能指针的线程安全
4. 笔试题
答案及解析
本篇完。
多线程部分的知识差不多学完了,现在创建一个线程池来检验一下学习成果。
1. 线程池
线程池是一种线程使用模式。
线程过多会带来调度开销,进而影响缓存局部性和整体性能。
线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,这避免了在处理短时间任务时创建与销毁线程的代价。
线程池不仅能够保证内核的充分利用,还能防止过分调度。
可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
每个线程在创建的时候会进行一系列系统调用,所以线程创建是有系统开销的。如果每需要一个线程再去创建,就会导致系统的性能下降等问题。
所以线程池就是维护着一些已经创建但是处于阻塞等待状态的线程,当有任务需要处理时,线程被唤醒并且执行对应的任务。
此时就避免了新建线程的系统开销,并且提高了响应效率。
1.1 前期代码
线程池示例:
① 创建固定数量线程池,循环从任务队列中获取任务对象,
② 获取到任务对象后,执行任务对象中的任务接口。
线程池这里的实现直接放一部分代码了,跟着注释看吧:(复制粘贴到VSCode里好看点)
Makefile
thread_pool:testMain.cc
g++ -o $@ $^ -std=c++11 -lpthread
clean:
rm -f thread_pool
thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *); // 定义函数指针->返回值是void*,函数名是fun_t,参数是void*->直接用fun_t
class ThreadData // 线程数据
{
public:
void *_args; // 真实参数
std::string _name; // 名字
};
class Thread // 封装的线程
{
public:
Thread(int num, fun_t callback, void *args)
: _func(callback) // 回调函数
{
char nameBuffer[64];
snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num); // 格式化到nameBuffer
_name = nameBuffer;
_tdata._args = args; // 线程构造时把参数和名字带给线程数据
_tdata._name = _name;
}
void start() // 启动线程
{
pthread_create(&_tid, nullptr, _func, (void*)&_tdata); // 传入线程数据
}
void join() // join自己
{
pthread_join(_tid, nullptr);
}
std::string name() // 返回线程名
{
return _name;
}
~Thread() // 析构什么也不做
{}
protected:
std::string _name; // 线程名字
pthread_t _tid; // 线程tid
fun_t _func; // 线程要执行的函数
ThreadData _tdata; // 线程数据
};
threadPool.hpp
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
const int g_thread_num = 3;
// 线程池->有一批线程,一批任务,有任务push有任务pop,本质是: 生产消费模型
template<class T>
class ThreadPool
{
public:
ThreadPool(int thread_num = g_thread_num)
:_num(thread_num)
{
for(int i = 1; i <= _num; ++i)
{
_threads.push_back(new Thread(i, routine, nullptr));
}
}
void run() // 1. 线程池的整体启动
{
for (auto &iter : _threads)
{
iter->start();
std::cout << iter->name() << " 启动成功" << std::endl;
}
}
static void *routine(void *args) // 每个线程启动后做的工作
{ // 类的成员函数有this指针 -> 两个参数 -> 类型不匹配 -> 所以加static
ThreadData *td = (ThreadData *)args;
while (true)
{
std::cout << "我是一个线程, 我的名字: " << td->_name << std::endl;
sleep(1);
}
}
void pushTask(const T &task) // 2. 任务到来时 -> push进线程池 -> 处理任务
{}
void joins()
{
for (auto &iter : _threads)
{
iter->join();
}
}
~ThreadPool()
{
for (auto &iter : _threads)
{
// iter->join();
delete iter;
}
}
protected:
std::vector<Thread *> _threads; // 保存一堆线程的容器
int _num; // 线程的数量
std::queue<T> _task_queue; // 任务队列
};
testMain.cc
#include "threadPool.hpp"
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
int main()
{
ThreadPool<int> *tp = new ThreadPool<int>();
tp->run();
// while(true) // 主线程向线程池push任务->现在没有,暂时写个joins接口
// {}
tp->joins();
return 0;
}
编译运行:
此时就发现了四个线程运行了,有一个是主线程。
1.2 加上锁的代码
把我们以前写的锁拷贝过来:
lockGuard.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t* mtx)
:_pmtx(mtx)
{}
void lock()
{
pthread_mutex_lock(_pmtx);
std::cout << "进行加锁成功" << std::endl;
}
void unlock()
{
pthread_mutex_unlock(_pmtx);
std::cout << "进行解锁成功" << std::endl;
}
~Mutex()
{}
protected:
pthread_mutex_t* _pmtx;
};
class lockGuard // RAII风格的加锁方式
{
public:
lockGuard(pthread_mutex_t* mtx) // 因为不是全局的锁,所以传进来,初始化
:_mtx(mtx)
{
_mtx.lock();
}
~lockGuard()
{
_mtx.unlock();
}
protected:
Mutex _mtx;
};
线程池里定义一把锁和条件变量,然后在线程池的构造和析构处理一下:
任务直接push:
重点看下面第三行注释:
这里构造函数是可以传this指针的,因为除了初始化列表,代码块里已经申请了对象的空间,为了更稳妥可以把传this指针的工作放到最后:
1.3 加上任务的代码
在阻塞队列里写过Task.hpp,拷贝过来改一下operator():
#pragma once
#include <iostream>
#include <functional>
typedef std::function<int(int, int)> func_t;
class Task
{
public:
Task()
{}
Task(int x, int y, func_t func)
: _x(x)
, _y(y)
, _func(func)
{}
void operator ()(const std::string &name)
{
std::cout << "线程 " << name << " 处理完成, 结果是: "
<< _x << " + " << _y << " = " << _func(_x, _y) << std::endl;
}
public: // 不想写get接口就直接弄公有了
int _x;
int _y;
// int type; 任务类型,这里不弄了
func_t _func;
};
routine是静态的,所以给它加几个接口
pthread_mutex_t *getMutex()
{
return &lock;
}
bool isEmpty()
{
return _task_queue.empty();
}
void waitCond() // 特定的条件变量下等待
{
pthread_cond_wait(&cond, &lock);
}
T getTask()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
static void *routine(void *args) // 每个线程启动后做的工作
{ // 类的成员函数有this指针 -> 两个参数 -> 类型不匹配 -> 所以加static
// 消费过程 -> 访问_task_queue -> 静态访问不了 -> 构造函数传this指针
ThreadData *td = (ThreadData *)args;
ThreadPool<T> *tp = (ThreadPool<T> *)td->_args;
while (true)
{
T task;
{
lockGuard lockguard(tp->getMutex()); // 出花括号自动调用析构,花括号里的接口全是加锁的
while (tp->isEmpty()) // 空就等待
{
tp->waitCond();
}
// 任务队列不为空,读取任务
task = tp->getTask(); // 是共享的-> 将任务从共享,拿到自己的私有空间
}
task(td->_name); // 告诉哪一个线程去处理这个任务就行了
}
}
void pushTask(const T &task) // 2. 任务到来时 -> push进线程池 -> 处理任务
{
lockGuard lockguard(&lock); // 加锁,执行完这个函数自动解锁
_task_queue.push(task); // 生产一个任务
pthread_cond_signal(&cond); // 唤醒一个线程
}
testMain.cc
#include "threadPool.hpp"
#include "Task.hpp"
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
ThreadPool<Task> *tp = new ThreadPool<Task>();
tp->run();
// while(true) // 主线程向线程池push任务->现在没有,暂时写个joins接口
// {}
// tp->joins();
while(true)
{
//生产的过程,制作任务的时候,要花时间
int x = rand() % 100 + 1;
usleep(7721);
int y = rand() % 30 + 1;
Task t(x, y, [](int x, int y)->int{return x + y;});
std::cout << "制作任务完成: " << x << " + " << y << " = ?" << std::endl;
// 推送任务到线程池中
tp->pushTask(t);
sleep(1);
}
return 0;
}
编译运行:
把打印加锁解锁成功代码注释掉再编译运行:
写到这其实就差不多了,但下面封装一下日志的使用。
1.4 加上日志的代码
日志平常用处不大,但是工作时是要求写日志的,所以这里来演示一下。
log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...) // 可变参数
{
#ifndef DEBUG_SHOW
if(level== DEBUG)
{
return;
}
#endif
char stdBuffer[1024]; // 标准日志部分
time_t timestamp = time(nullptr); // 获取时间戳
// struct tm *localtime = localtime(×tamp); // 转化麻烦就不写了
snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; // 自定义日志部分
va_list args; // 提取可变参数的 -> #include <cstdarg> 了解一下就行
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof(logBuffer), format, args);
va_end(args); // 相当于ap=nullptr
printf("%s%s\n", stdBuffer, logBuffer);
// FILE *fp = fopen(LOGFILE, "a"); // 追加到文件,这里写好了就不演示了
// fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
// fclose(fp);
}
Task.hpp
#pragma once
#include <iostream>
#include <functional>
#include "log.hpp"
typedef std::function<int(int, int)> func_t;
class Task
{
public:
Task()
{}
Task(int x, int y, func_t func)
: _x(x)
, _y(y)
, _func(func)
{}
void operator ()(const std::string &name)
{
// std::cout << "线程 " << name << " 处理完成, 结果是: "
// << _x << " + " << _y << " = " << _func(_x, _y) << std::endl;
logMessage(WARNING, "%s处理完成: %d + %d = %d | %s | %d",
name.c_str(), _x, _y, _func(_x, _y), __FILE__, __LINE__); // 预处理符号->当前源文件和行号
}
public: // 不想写get接口就直接弄公有了
int _x;
int _y;
// int type; 任务类型,这里不弄了
func_t _func;
};
testMain.cc
#include "threadPool.hpp"
#include "Task.hpp"
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
ThreadPool<Task> *tp = new ThreadPool<Task>();
tp->run();
// while(true) // 主线程向线程池push任务->现在没有,暂时写个joins接口
// {}
// tp->joins();
while(true)
{
//生产的过程,制作任务的时候,要花时间
int x = rand() % 100 + 1;
usleep(7721);
int y = rand() % 30 + 1;
Task t(x, y, [](int x, int y)->int{return x + y;});
// std::cout << "制作任务完成: " << x << " + " << y << " = ?" << std::endl;
logMessage(DEBUG, "制作任务完成: %d + %d = ?", x, y);
// 推送任务到线程池中
tp->pushTask(t);
sleep(1);
}
return 0;
}
threadPool.hpp,thread.hpp,lockGuard.hpp和上面一样,编译运行:
此时DEBUG等级的消息就没打印出来,在Makefile加上 -D DEBUG_SHOW:
thread_pool:testMain.cc
g++ -o $@ $^ -std=c++11 -lpthread -D DEBUG_SHOW
clean:
rm -f thread_pool
编译运行:
此时就打印出来了,在-D DEBUG_SHOW前加#(注释Makefile里的代码)
这就是日志和条件编译的应用。
- 根据上面代码可以看到,线程池是根本不知道推送到任务队列中的任务是什么。所维护的线程同样也不知道。
- 具体是什么任务是由任务的推送方决定的,线程池只负责从任务队列中获取并处理任务。
线程池的全部内容就先到这里了。
2. 单例模式的线程安全
单例模式C++已经介绍过了:
从C语言到C++_37(特殊类设计和C++类型转换)单例模式_GR_C的博客-CSDN博客
再举个洗碗的例子:
饿汉方式:吃完饭,立刻洗碗。
因为下一顿吃的时候可以立刻拿着碗就能吃饭。
懒汉方式:吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗。
懒汉方式最核心的思想是 "延时加载",从而能够优化服务器的启动速度。
2.1 线程池的懒汉模式
懒汉模式讲究的就是一个延时加载,既然操作系统在很多方面都采用这种方式,说明这种方式非常重要,同样这里将线程池再改造成懒汉模式的单例对象:
静态成员变量必须在类外进行定义初始化:
- 单例对象指针变量的定义初始化:
ThreadPool<T>*中的ThreadPool<T>虽然还没有实例化,但是并不妨碍给ThreadPool<T>*这个指针赋值为空,就像void*虽然不知道void是什么类型,但是却可以给这个指针赋值。
- 静态互斥锁的初始化:
单例对象只有一个,所以也只需要一个互斥锁来维护线程安全, 所以同样放在静态区上。 std::mutex表示互斥锁是标准库中的互斥锁类型,ThreadPool<T>表示是在先线程池这个作用域中。
public:
static ThreadPool<T> *getThreadPool(int num = g_thread_num) // 多线程使用单例的过程
{
// 可以有效减少未来必定要进行加锁检测的问题
// 拦截大量的在已经创建好单例的时候,剩余线程请求单例的而直接访问锁的行为
// 如果这里不加if,未来任何一个线程想获取单例,都必须调用getThreadPool接口
// 一定会存在大量的申请和释放锁的行为,这个是无用且浪费资源的
if (nullptr == thread_ptr)
{
lockGuard lockguard(&mutex);
// pthread_mutex_lock(&mutex);
if (nullptr == thread_ptr)
{
thread_ptr = new ThreadPool<T>(num);
}
// pthread_mutex_unlock(&mutex);
}
return thread_ptr;
}
- 在第一次使用单例对象的时候再在堆区new一个单例对象出来。
- 为了维护单例对象的线程安全,所以在判断单例对象是否存在的时候,需要加锁。
- 为了提高效率,单例对象被创建后就不再申请锁去判断,采样双检查加锁的方式。
其他内容,像构造函数私有,拷贝构造以及赋值运算符重载等处理和饿汉模式一样。
此时threadPool.hpp就是这样的:
threadPool.hpp
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "thread.hpp"
#include "lockGuard.hpp"
const int g_thread_num = 3;
// 线程池->有一批线程,一批任务,有任务push有任务pop,本质是: 生产消费模型
template<class T>
class ThreadPool
{
private:
ThreadPool(int thread_num = g_thread_num)
:_num(thread_num)
{
pthread_mutex_init(&lock, nullptr);
pthread_cond_init(&cond, nullptr);
for(int i = 1; i <= _num; ++i)
{
_threads.push_back(new Thread(i, routine, this));
}
}
ThreadPool(const ThreadPool<T> &other) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &other) = delete;
public:
static ThreadPool<T> *getThreadPool(int num = g_thread_num) // 多线程使用单例的过程
{
// 可以有效减少未来必定要进行加锁检测的问题
// 拦截大量的在已经创建好单例的时候,剩余线程请求单例的而直接访问锁的行为
// 如果这里不加if,未来任何一个线程想获取单例,都必须调用getThreadPool接口
// 一定会存在大量的申请和释放锁的行为,这个是无用且浪费资源的
if (nullptr == thread_ptr)
{
lockGuard lockguard(&mutex);
// pthread_mutex_lock(&mutex);
if (nullptr == thread_ptr)
{
thread_ptr = new ThreadPool<T>(num);
}
// pthread_mutex_unlock(&mutex);
}
return thread_ptr;
}
void run() // 1. 线程池的整体启动
{
for (auto &iter : _threads)
{
iter->start();
std::cout << iter->name() << " 启动成功" << std::endl;
}
}
pthread_mutex_t *getMutex()
{
return &lock;
}
bool isEmpty()
{
return _task_queue.empty();
}
void waitCond() // 特定的条件变量下等待
{
pthread_cond_wait(&cond, &lock);
}
T getTask()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
static void *routine(void *args) // 每个线程启动后做的工作
{ // 类的成员函数有this指针 -> 两个参数 -> 类型不匹配 -> 所以加static
// 消费过程 -> 访问_task_queue -> 静态访问不了 -> 构造函数传this指针
ThreadData *td = (ThreadData *)args;
ThreadPool<T> *tp = (ThreadPool<T> *)td->_args;
while (true)
{
T task;
{
lockGuard lockguard(tp->getMutex()); // 出花括号自动调用析构,花括号里的接口全是加锁的
while (tp->isEmpty()) // 空就等待
{
tp->waitCond();
}
// 任务队列不为空,读取任务
task = tp->getTask(); // 是共享的-> 将任务从共享,拿到自己的私有空间
}
task(td->_name); // 告诉哪一个线程去处理这个任务就行了
}
}
void pushTask(const T &task) // 2. 任务到来时 -> push进线程池 -> 处理任务
{
lockGuard lockguard(&lock); // 加锁,执行完这个函数自动解锁
_task_queue.push(task); // 生产一个任务
pthread_cond_signal(&cond); // 唤醒一个线程
}
// void joins()
// {
// for (auto &iter : _threads)
// {
// iter->join();
// }
// }
~ThreadPool()
{
for (auto &iter : _threads)
{
// iter->join();
delete iter;
}
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
protected:
std::vector<Thread *> _threads; // 保存一堆线程的容器
int _num; // 线程的数量
std::queue<T> _task_queue; // 任务队列
pthread_mutex_t lock;
pthread_cond_t cond;
static ThreadPool<T> *thread_ptr; // 懒汉模式的单例对象指针
static pthread_mutex_t mutex; // 单例对象的锁
};
template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr; // 定义初始化为空
template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER; // 定义锁
testMain.cc
#include "threadPool.hpp"
#include "Task.hpp"
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
// ThreadPool<Task> *tp = new ThreadPool<Task>();
// tp->run(); // 下面是单例模式
ThreadPool<Task>::getThreadPool()->run();
// while(true) // 主线程向线程池push任务->现在没有,暂时写个joins接口
// {}
// tp->joins();
while(true)
{
//生产的过程,制作任务的时候,要花时间
int x = rand() % 100 + 1;
usleep(7721);
int y = rand() % 30 + 1;
Task t(x, y, [](int x, int y)->int{return x + y;});
// std::cout << "制作任务完成: " << x << " + " << y << " = ?" << std::endl;
logMessage(DEBUG, "制作任务完成: %d + %d = ?", x, y);
// 推送任务到线程池中
// tp->pushTask(t); // 下面是单例模式
ThreadPool<Task>::getThreadPool()->pushTask(t);
sleep(1);
}
return 0;
}
其它的头文件和上面的一样,编译运行:
从运行结果上看,和之前的一样。
3. STL和智能指针的线程安全
上面已经讲了单例模式的线程安全,
STL中的容器是否是线程安全的?
STL中的容器不是线程安全的。原因: STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。而且对于不同的容器, 加锁方式的不同,性能可能也不同(例如 hash 表的锁表和锁桶)因此 STL 默认不是线程安全, 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。
对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。(但是如果unique_ptr和其它STL容器一起使用就涉及线程安全问题了)对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。
STL和智能指针的线程安全就先简单讲一下,后面在C++专栏里再改改代码。
4. 笔试题
1. 下列关于银行家算法的叙述中,正确的是()
A.银行家算法可以预防死锁
B.当系统处于安全状态时, 系统中一定无死锁进程
C.当系统处于不安全状态时, 系统中一定会出现死锁进程
D.银行家算法破坏了死锁必要条件中的“ 请求和保持” 条件
2. 下面哪些是死锁发生的必要条件?[多选]
A.互斥条件
B.请求和保持
C.不可剥夺
D.循环等待
3. 在操作系统中,下列有关死锁的说法正确的是()[多选]
A.采用“按序分配”策略可以尽可能的破坏产生死锁的环路等待条件
B.产生死锁的现象是每个进程等待某一个不能得到且不可释放的资源
C.在资源动态分配过程中,防止系统进入安全状态,可避免发生死锁
D.银行家算法是最有代表性的死锁解除算法
4. 关于死锁的说法正确的有?[多选]
A.竞争可剥夺资源会产生死锁
B.竞争临时资源有可能会产生死锁
C.在发生死锁时,必然存在一个进程—资源的环形链
D.如果进程在一次性申请其所需的全部资源成功后才运行,就不会发生死锁
5. 死锁的处理都有哪些方法?[多选]
A.鸵鸟策略
B.预防策略
C.避免策略
D.检测与解除死锁
6. 线程池都有什么作用?[多选]
A.降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗
B.提高线程的可管理性:线程池可以统一管理、分配、调优和监控
C.程序性能更优:创建的线程越多性能越高
D.降低程序的耦合程度: 提高程序的运行效率
7. 以下线程池的关键参数有哪些?[多选]
A.线程池中线程最大数量
B.线程安全的阻塞队列
C.线程池中线程的存活时间
D.线程池中阻塞队列的最大节点数量
8. 请简述线程池的作用与实现原理
9. 无锁化编程有哪些常见方法?[多选]
A.针对计数器,可以使用原子加
B.只有一个生产者和一个消费者,那么就可以做到免锁访问环形缓冲区(Ring Buffer)
C.RCU(Read-Copy-Update),新旧副本切换机制,对于旧副本可以采用延迟释放的做法
D.CAS(Compare-and-Swap),如无锁栈,无锁队列等待
10. 并发编程中通常会遇到三个问题 原子性问题,可见性问题,有序性问题,java/C/C++中volatile关键字可以保证并发编程中的
A.原子性, 可见性
B.可见性,有序性
C.原子性,有序性
D.原子性, 可见性,有序性
11. 下列操作中,需要执行加锁的操作是()[多选]
A.x++;
B.x=y;
C.++x;
D.x=1;
12. CAS(CompareAndSwap),是用来实现lock-free编程的重要手段之一,多数处理器都支持这一原子操作,其用伪代码描述如下,
template bool CAS(T*addr,T expected,T value)
{
if(*addr==expected){
*addr=value;
return true;
}
return false;
}
int count=0;
void count_atomic_inc(int*addr)
{
int oldval=0;
int newval=0;
do{
oldval=*addr;
newval=______+1;
}until CAS(_______,________,_________)
}
请完成下面填空,实现全局计数器的原子递增操作.()
A.newval,addr,*oldval, oldval
B.oldval,addr,oldval,newval
C.oldval,addr,oldval,*newval
D.oldval,addr,newval,oldval
13. 如何理解原语的原子性,在单机环境下如何实现原语的原子性,实现时应注意哪些问题?
答案及解析
1. B
银行家算法的思想在于将系统运行分为两种状态:安全/非安全,有可能出现风险的都属于非安全
A 银行家算法是避免出现死锁的一种算法(并非预防的方法)
C 处于不安全状态只是表示有风险,不代表一定发生
D 银行家算法的思想是为了避免出现“环路等待”条件
2. ABCD
死锁产生的四个必要条件;互斥条件,不可剥夺条件,请求与保持条件,环路等待条件
3. BCD
死锁产生的必要条件:互斥,不可剥夺,请求与保持,环路等待
A 破坏了不可剥夺条件,因此不会产生死锁
B 这里的临时资源指的是(硬件中断,信号,消息...等),通常顺序不定,因此有可能会产生死锁
C 环形链也即是环路等待,这是死锁的必要条件
D 资源一次性分配,也就不存在请求与保持的情况以及环路等待情况了
4. ABCD
A 鸵鸟策略 对可能出现的问题采取无视态度,前提是出现概率很低
B 预防策略 破坏死锁产生的必要条件
C 避免策略 银行家算法,分配资源前进行风险判断,避免风险的发生
D 检测与解除死锁 分配资源时不采取措施,但是必须提供死锁的检测与解除手段
5. C
A:正确:构造了一个空的vector,里面放置的是string类型的对象
B:正确:构造了一个空的动态二维数组
C:错误,svvec是二维的,vector套vector,不能直接使用"hello"构造
6. ABD
多线程程序的运行效率, 是一个正态分布的结果, 线程数量从1开始增加, 随着线程数量的增加, 程序的运行效率逐渐变高, 直到线程数量达到一个临界值, 当在增加线程数量时, 程序的运行效率会减小(主要是由于频繁线程切换影响线程运行效率)
因此C选项错误。
A正确,线程池中更多是对已经创建的线程循环利用,因此节省了新的线程的创建与销毁的时间成本
B正确,线程池是一个模块化的处理思想,具有统一管理,资源分配,调整优化,监控的优点
D正确,线程池模块与任务的产生分离,可以动态的根据性能及任务数量调整线程的数量,提高程序的运行效率
7. ABCD
A 防止资源耗尽,或线程过多性能降低
B 用于任务排队缓冲
C 长时间空闲则退出线程节省资源
D 防止任务过多,资源耗尽
8. 简述线程池的作用与实现原理(简答题)
线程池可以避免大量线程频繁创建或销毁所带来的时间成本,也可以避免在峰值压力下,系统资源耗尽的风险;并且可以统一对线程池中的线程进行管理,调度监控。
线程池通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现,线程池中的线程可以从阻塞队列中获取任务进行任务处理,当线程都处于繁忙状态时可以将任务加入阻塞队列中,等到其它的线程空闲后进行处理。
9. ABCD
A.针对计数器,可以使用原子加
B.只有一个生产者和一个消费者,那么就可以做到免锁访问环形缓冲区(Ring Buffer)
C.RCU(Read-Copy-Update),新旧副本切换机制,对于旧副本可以采用延迟释放的做法
D.CAS(Compare-and-Swap),如无锁栈,无锁队列等待
10. B
原子性:一个操作不会被打断,要么一次完成,要么不做。
可见性:一个资源被修改后,是否对其他线程是立即可见的(一个变量的修改存在一个过程,将数据从内存加载的cpu寄存器,进行运算,完毕后交还内存,但是这个过程在代码优化中可能会被编译器优化,将数据放入寄存器,则后续运算只从寄存器取数据,就节省了从内存获取数据的时间)
有序性:简单理解,程序按照写代码的先后顺序执行,就是有序的。(编译器有时候会为了提高程序效率进行代码优化,进行指令重排,来提高效率,而有序性就是禁止指令重排)
而volatile关键字的作用是,防止编译器过度优化,因此具备可见性与有序性功能(能保证部分原子性但不能保证原子性)
11. ABC
D 常量的直接赋值是一个原子操作
ABC选项中涉及到了数据的运算,则涉及从内存加载数据到寄存器,在寄存器中运算,将寄存器中数据交还内存的过程
因此需要加锁保护的操作中,正确选项为:ABC
12. B
CAS(Compare-and-Swap):一种比较后数据若无改变则交换数据的一种无锁操作(乐观锁)
这个题里边注意各个参数的是否使用指针即可
CAS比较与交换的伪代码可以表示为:
do{
备份旧数据;
基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))
正确选项 为B选项,可以将B选项的数值代入理解CAS锁的原理思想
13. 如何理解原语的原子性,在单机环境下如何实现原语的原子性,实现时应注意哪些问题?
原子性原语double CAS (DCAS) 运行子两个随机排序内存单元上。若当前值与预期值一致,可改变这两个内存单元的值。
所谓原语的原子性操作是指一个操作中的所有动作,要么成功完成,要么全不做。也就是说,原语操作是一个不可分割的整体。为了保证原语操作的正确性,必须保证原语具有原子性。在单机环境下,操作的原子性一般是通过关闭中断来实现的。由于中断是计算机与外设通信的重要手段,关闭中断会对系统产生很大的影响,所以在实现时一定要避免原语操作花费时间过长,绝对不允许原语中出现死循环。
(有几题博客没有讲到的内容就给大家去了解了解)
本篇完。
下一部分就是网络和Linux网络的内容了。不过先完成一些C++留下来的多线程的内容。
下一篇:从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)。下下篇:网络和Linux网络_((网络基础①)网络概念+协议概念。