jsp做的婚恋网站,在谷歌上做英文网站,有没有哪个网站怎么做动漫新闻的,dede怎么换网站页面1 多线程编程简介
说到多线程编程#xff0c;就不得不提并行和并发#xff0c;多线程是实现并发和并行的一种手段。 并行是指两个或多个独立的操作同时进行。 并发是指一个时间段内执行多个操作。
在单核时代#xff0c;多个线程是并发的#xff0c;在一个时间段内轮流…1 多线程编程简介
说到多线程编程就不得不提并行和并发多线程是实现并发和并行的一种手段。 并行是指两个或多个独立的操作同时进行。 并发是指一个时间段内执行多个操作。
在单核时代多个线程是并发的在一个时间段内轮流执行在多核时代多个线程可以实现真正的并行在多核上真正独立的并行执行。例如现在常见的4核4线程可以并行4个线程4核8线程则使用了超线程技术把一个物理核模拟为2个逻辑核心可以并行8个线程。
并发编程的方法
通常要实现并发有两种方法多进程并发和多线程并发。
多进程并发:
使用多进程并发是将一个应用程序划分为多个独立的进程每个进程只有一个线程这些独立的进程间可以互相通信共同完成任务。
由于操作系统对进程提供了大量的保护机制以避免一个进程修改了另一个进程的数据使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点
在进程件的通信无论是使用信号、套接字还是文件、管道等方式其使用要么比较复杂要么就是速度较慢或者两者兼而有之。运行多个线程的开销很大操作系统要分配很多的资源来对这些进程进行管理。
由于多个进程并发完成同一个任务时不可避免的是操作同一个数据和进程间的相互通信上述的两个缺点也就决定了多进程的并发不是一个好的选择。
多线程并发:
多线程并发指的是在同一个进程中执行多个线程。
有操作系统相关知识的应该知道线程是轻量级的进程每个线程可以独立的运行不同的指令序列但是线程不独立的拥有资源依赖于创建它的进程而存在。也就是说同一进程中的多个线程共享相同的地址空间可以访问进程中的大部分数据指针和引用可以在线程间进行传递。这样同一进程内的多个线程能够很方便的进行数据共享以及通信也就比进程更适用于并发操作。
由于缺少操作系统提供的保护机制在多线程共享数据及通信时就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的并且要极力的避免**死锁(deadlock)**。 2 std::thread简介
C11之前window和linux平台分别有各自的多线程标准使用C编写的多线程往往是依赖于特定平台的。
Window平台提供用于多线程创建和管理的win32 apiLinux下则有POSIX多线程标准Threads或Pthreads库提供的API可以在类Unix上运行
在C11新标准中可以简单通过使用thread库来管理多线程。thread库可以看做对不同平台多线程API的一层包装因此使用新标准提供的线程库编写的程序是跨平台的。
一个简单的多线程实现
C11的标准库中提供了多线程库使用时需要#include thread头文件该头文件主要包含了对线程的管理类std::thread以及其他管理线程相关的类。下面是使用C多线程库的简单示例
#include iostream
#include threadusing namespace std;void output(int i)
{cout i endl;
}int main()
{for (uint8_t i 0; i 4; i){thread t(output, i);t.detach(); }getchar();return 0;
}
在一个for循环内创建4个线程分别输出数字0、1、2、3并且在每个数字的末尾输出换行符。语句thread t(output, i)创建一个线程t该线程运行output第二个参数i是传递给output的参数。t在创建完成后自动启动t.detach表示该线程在后台允许无需等待该线程完成继续执行后面的语句。这段代码的功能是很简单的如果是顺序执行的话其结果很容易预测得到
0
1
2
3但是在并行多线程下其执行的结果就多种多样了比如下面就是代码一次运行的结果
012
3这就涉及到多线程编程最核心的问题资源竞争。
假设CPU有4核可以同时执行4个线程但是控制台却只有一个同时只能有一个线程拥有这个唯一的控制台将数字输出。将上面代码创建的四个线程进行编号t0,t1,t2,t3分别输出的数字0,1,2,3。参照上图的执行结果控制台的拥有权的转移如下
t0拥有控制台输出了数字0但是其没有来的及输出换行符控制的拥有权却转移到了t10t1完成自己的输出t1线程完成 1\n控制台拥有权转移给t0输出换行符 \nt2拥有控制台完成输出 2\nt3拥有控制台完成输出 3\n)
由于控制台是系统资源这里控制台拥有权的管理是操作系统完成的。但是假如是多个线程共享进程空间的数据这就需要自己写代码控制每个线程何时能够拥有共享数据进行操作。
共享数据的管理以及线程间的通信是多线程编程的两大核心。
线程管理
每个应用程序至少有一个进程而每个进程至少有一个主线程除了主线程外在一个进程中还可以创建多个线程。每个线程都需要一个入口函数入口函数返回退出该线程也会退出主线程就是以main函数作为入口函数的线程。
在C 11的线程库中将线程的管理放在了类std::thread中使用std::thread可以创建、启动一个线程并可以将线程挂起、结束等操作。
启动一个线程
C 11的线程库启动一个线程是非常简单的只需要创建一个std::thread对象就会启动一个线程并使用该std::thread对象来管理该线程。
do_task();
std::thread(do_task);
这里创建std::thread传入的函数实际上其构造函数需要的是可调用callable类型只要是有函数调用类型的实例都是可以的。所以除了传递函数外还可以使用 lambda表达式 使用lambda表达式启动线程输出数字 for (int i 0; i 4; i)
{thread t([i]{cout i endl;});t.detach();
} 重载了()运算符的类的实例 使用重载了()运算符的类实现多线程数字输出 class Task
{
public:void operator()(int i){cout i endl;}
};int main()
{for (uint8_t i 0; i 4; i){Task task;thread t(task, i);t.detach(); }
}
把函数对象传入std::thread的构造函数时要注意一个C的语法解析错误C’s most vexing parse。向std::thread的构造函数中传入的是一个临时变量而不是命名变量就会出现语法解析错误。如下代码
std::thread t(Task());
这里相当于声明了一个函数t其返回类型为thread而不是启动了一个新的线程。可以使用新的初始化语法避免这种情况
std::thread t{Task()};
当线程启动后一定要在和线程相关联的thread销毁前确定以何种方式等待线程执行结束。
C11有两种方式来等待线程结束
detach方式启动的线程自主在后台运行当前的代码继续往下执行不等待新线程结束。前面代码所使用的就是这种方式。 调用detach表示thread对象和其表示的线程完全分离分离之后的线程是不在受约束和管制会单独执行直到执行完毕释放资源可以看做是一个daemon线程分离之后thread对象不再表示任何线程分离之后joinable() false即使还在执行join方式等待启动的线程完成才会继续往下执行。假如前面的代码使用这种方式其输出就会0,1,2,3因为每次都是前一个线程输出完成了才会进行下一个循环启动下一个新线程。 只有处于活动状态线程才能调用join可以通过joinable()函数检查;joinable() true表示当前线程是活动线程才可以调用join函数默认构造函数创建的对象是joinable() false;join只能被调用一次之后joinable就会变为false表示线程执行完毕调用 ternimate()的线程必须是 joinable() false;如果线程不调用join()函数即使执行完毕也是一个活动线程即joinable() true依然可以调用join()函数
无论在何种情形一定要在thread销毁前调用t.join或者t.detach来决定线程以何种方式运行。
当使用join方式时会阻塞当前代码等待线程完成退出后才会继续向下执行
而使用detach方式则不会对当前代码造成影响当前代码继续向下执行创建的新线程同时并发执行这时候需要特别注意创建的新线程对当前作用域的变量的使用创建新线程的作用域结束后有可能线程仍然在执行这时局部变量随着作用域的完成都已销毁如果线程继续使用局部变量的引用或者指针会出现意想不到的错误并且这种错误很难排查。例如
auto fn [](const int *a)
{for (int i 0; i 10; i){cout *a endl;}
};[fn]
{int a 1010;thread t(fn, a);t.detach();
}();
在lambda表达式中使用fn启动了一个新的线程在装个新的线程中使用了局部变量a的指针并且将该线程的运行方式设置为detach。这样在lamb表达式执行结束后变量a被销毁但是在后台运行的线程仍然在使用已销毁变量a的指针这样就可能会导致不正确的结果出现。
所以在以detach的方式执行线程时要将线程访问的局部数据复制到线程的空间使用值传递一定要确保线程没有使用局部变量的引用或者指针除非你能肯定该线程会在局部作用域结束前执行结束。
当然使用join方式的话就不会出现这种问题它会在作用域结束前完成退出。
异常情况下等待线程完成
当决定以detach方式让线程在后台运行时可以在创建thread的实例后立即调用detach这样线程就会后thread的实例分离即使出现了异常thread的实例被销毁仍然能保证线程在后台运行。
但线程以join方式运行时需要在主线程的合适位置调用join方法如果调用join前出现了异常thread被销毁线程就会被异常所终结。为了避免异常将线程终结或者由于某些原因例如线程访问了局部变量就要保证线程一定要在函数退出前完成就要保证要在函数退出前调用join
void func()
{thread t([]{cout hello C 11 endl;});try{do_something_else();}catch (...){t.join();throw;}t.join();
}
上面代码能够保证在正常或者异常的情况下都会调用join方法这样线程一定会在函数func退出前完成。但是使用这种方法不但代码冗长而且会出现一些作用域的问题并不是一个很好的解决方法。
一种比较好的方法是资源获取即初始化RAII,Resource Acquisition Is Initialization)该方法提供一个类在析构函数中调用join。
class thread_guard
{thread t;
public :explicit thread_guard(thread _t) :t(_t){}~thread_guard(){if (t.joinable())t.join();}thread_guard(const thread_guard) delete;thread_guard operator(const thread_guard) delete;
};void func(){thread t([]{cout Hello thread endl ;});thread_guard g(t);
}
无论是何种情况当函数退出时局部变量g调用其析构函数销毁从而能够保证join一定会被调用。
向线程传递参数
向线程调用的函数传递参数也是很简单的只需要在构造thread的实例时依次传入即可。例如
void func(int *a,int n){}int buffer[10];
thread t(func,buffer,10);
t.join();
需要注意的是默认的会将传递的参数以拷贝的方式复制到线程空间即使参数的类型是引用。例如
void func(int a,const string str);
thread t(func,3,hello);
func的第二个参数是string 而传入的是一个字符串字面量。该字面量以const char*类型传入线程空间后在**线程的空间内转换为string**。
如果在线程中使用引用来更新对象时就需要注意了。默认的是将对象拷贝到线程空间其引用的是拷贝的线程空间的对象而不是初始希望改变的对象。如下
class _tagNode
{
public:int a;int b;
};void func(_tagNode node)
{node.a 10;node.b 20;
}void f()
{_tagNode node;thread t(func, node);t.join();cout node.a endl ;cout node.b endl ;
}
在线程内将对象的字段a和b设置为新的值但是在线程调用结束后这两个字段的值并不会改变。这样由于引用的实际上是局部变量node的一个拷贝而不是node本身。在将对象传入线程的时候调用std::ref将node的引用传入线程而不是一个拷贝。例如thread t(func,std::ref(node));
也可以使用类的成员函数作为线程函数示例如下
class _tagNode{public:void do_some_work(int a);
};
_tagNode node;thread t(_tagNode::do_some_work, node,20);
上面创建的线程会调用node.do_some_work(20)第三个参数为成员函数的第一个参数以此类推。
转移线程的所有权
thread是可移动的(movable)的但不可复制(copyable)。可以通过move来改变线程的所有权灵活的决定线程在什么时候join或者detach。
thread t1(f1);
thread t3(move(t1));
将线程从t1转移给t3,这时候t1就不再拥有线程的所有权调用t1.join或t1.detach会出现异常要使用t3来管理线程。这也就意味着thread可以作为函数的返回类型或者作为参数传递给函数能够更为方便的管理线程。
线程的标识类型为std::thread::id有两种方式获得到线程的id。
通过thread的实例调用get_id()直接获取在当前线程上调用this_thread::get_id()获取 3 互斥锁std::mutex
std::mutex 是C11 中最基本的互斥量std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁。
std::mutex 成员函数
构造函数 std::mutex不允许拷贝构造也不允许 move 拷贝最初产生的 mutex 对象是处于 unlocked 状态的。 lock() 1、如果该互斥量当前没有被锁住则调用线程将该互斥量锁住直到调用 unlock之前该线程一直拥有该锁。 2、 如果当前互斥量被其他线程锁住则当前的调用线程被阻塞住。 3、 如果当前互斥量被当前调用线程锁住则会产生死锁(deadlock)。 unlock() 解锁释放对互斥量的所有权。 try_lock() 尝试锁住互斥量如果互斥量被其他线程占有则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况 1、 如果当前互斥量没有被其他线程占有则该线程锁住互斥量直到该线程调用 unlock 释放互斥量。 2、 如果当前互斥量被其他线程锁住则当前调用线程返回 false而并不会被阻塞掉。 3、 如果当前互斥量被当前调用线程锁住则会产生死锁(deadlock)。这一条存疑实测单线程内先lock再trylock不会死锁会有1ms以内的停顿trylock返回false std::lock_guard
lock_guard是一个互斥量包装程序它提供了一种方便的RAIIResource acquisition is initialization 风格的机制来在作用域块的持续时间内拥有一个互斥量。 创建lock_guard对象时它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时lock_guard析构并释放互斥量。 1、创建即加锁作用域结束自动析构并解锁无需手工解锁 2、不能中途解锁必须等作用域结束才解锁 3、不能复制 std::unique_lock
unique_lock是lock_guard的升级版它允许延迟锁定限时深度锁定递归锁定锁定所有权的转移以及与条件变量一起使用。 1、创建时可以不锁定通过指定第二个参数为std::defer_lock而在需要时再锁定 2、可以随时加锁解锁 3、作用域规则同 lock_grard析构时自动释放锁 4、不可复制可移动 5、条件变量需要该类型的锁作为参数此时必须使用unique_lock lock_guard 和unique_lock 并不管理 std::mutex 对象的生命周期在使用 lock_guard 的过程中如果 std::mutex 的对象被释放了那么在 lock_guard 析构的时候进行解锁就会出现空指针错误。 std::mutex实测代码
#include mutex
#include unistd.h
std::mutex g_mutex;
pthread_t Tid[10];
void* threadfunc0(void*)
{printf(threadfunc0\n);std::unique_lockstd::mutex locker(g_mutex);
// g_mutex.unlock();/*如果执行,则立即打印threadfunc1,after lock,不会等待5秒*/sleep(5);return nullptr;
}void* threadfunc1(void*)
{printf(threadfunc1\n);std::unique_lockstd::mutex locker(g_mutex);if(g_mutex.try_lock() true)//不会死锁,返回false,有1ms内的停顿{printf(try_lock\n);g_mutex.unlock();}printf(threadfunc1,after lock\n);return nullptr;
}MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui-setupUi(this);setbuf(stdout,nullptr);pthread_create(Tid[0],nullptr,threadfunc0,nullptr);usleep(100);pthread_create(Tid[1],nullptr,threadfunc1,nullptr);
} 打印:
threadfunc0 threadfunc1 等待5秒 threadfunc1,after lock 4 std::condition_variable
4.1 介绍
头文件 #include condition_variable
std::condition_variable 是 C11 提供的一种线程同步原语它可以用于在线程之间等待和通知事件的发生。
通常情况下线程需要等待某个条件变量被满足后才能继续执行这个等待的过程通常使用 std::unique_lock 和std::condition_variable 一起使用。 4.2 std::condition_variable 类提供了两个主要的方法
wait(lock)等待条件变量的通知如果条件变量没有被满足线程将被阻塞并释放关联的互斥锁 lock。当条件变量被满足时线程会重新获取互斥锁并继续执行。notify_one() 或 notify_all()通知等待在条件变量上的线程条件已经被满足等待的线程将被唤醒继续执行。
使用 std::condition_variable 的一般流程如下 :
首先定义一个互斥量std::mutex和一个条件变量std::condition_variable用来保证线程安全和线程之间的同步。
std::mutex _queueMutex;
std::condition_variable _queueCond;
在往任务队列中添加任务时需要先获取互斥锁并检查队列是否已满。如果已满则使用条件变量等待有空闲位置。
// 添加任务
void addTask(Task task) {// 获取互斥锁std::unique_lockstd::mutex lock(_queueMutex);// 如果队列已满则等待有空闲位置_queueCond.wait(lock, [this](){ return _taskQueue.size() _maxQueueSize; });// 将任务添加到队列中_taskQueue.push_back(std::move(task));
}
在从任务队列中取出任务时也需要获取互斥锁并检查队列是否为空。如果为空则使用条件变量等待有任务可取。
// 取出任务
Task takeTask() {// 获取互斥锁std::unique_lockstd::mutex lock(_queueMutex);// 如果队列为空则等待有任务可取_queueCond.wait(lock, [this](){ return !_taskQueue.empty(); });// 从队列中取出一个任务Task task std::move(_taskQueue.front());_taskQueue.pop_front();return task;
} 通过使用条件变量往任务队列中添加任务的线程和从任务队列中取出任务的线程可以有效地同步避免了死锁和竞争条件的问题。同时也避免了线程空转减少了资源的浪费。