目前多线程方面我主要用两种方式:Qt的QThread和std::thread,都是跨平台,封装了系统的线程库。 暂时还没用到WindowsApi的thread和Linux的pthread。
线程同步方面主要使用互斥锁mutex和条件变量condition_variable,没有用到一定需要用信号量的场景且互斥锁效率不一定比读写锁差,所以所有项目我都只用互斥锁和条件变量基本都能实现需求,当然目前Qt项目中线程需求过于简单,就不讨论Qt中线程问题。
互斥锁:保证临界区代码同时只能被一个线程访问。最常用的同步方式。
有一些注意的地方Winodows下,锁在同一个线程中还没解锁的情况下再次锁 会直接导致崩溃,据说linux下只是阻塞,有待测试。我们要避免在同一个线程中未解锁的情况下锁两次。其次,mutex最好用raii的方式封装,自动解锁。
条件变量:线程同步中 我们往往需要用条件变量阻塞线程,等到满足条件之后,再唤醒线程。同时提供了等待条件变量满足的 wait 系列方法(wait、wait_for、wait_until 方法),发送条件信号使用 notify 方法(notify_one 和 notify_all 方法),使用 std::condition_variable 对象时需要绑定一个 std::unique_lock 或 std::lock_guard 对象。在linux平台下会存在虚假唤醒的情况,我们可以用while而不用if判断条件规避虚假唤醒。
下面贴个简单的例子:
#include <thread>
#include <mutex>
#include <condition_variable>
#include <list>
#include <iostream>
class Task
{
public:
Task(int taskID)
{
this->taskID = taskID;
}
void doTask()
{
std::cout << "handle a task, taskID: " << taskID << ", threadID: " << std::this_thread::get_id() << std::endl;
}
private:
int taskID;
};
std::mutex mymutex;
std::list<Task*> tasks;
std::condition_variable mycv;
void* consumer_thread()
{
Task* pTask = NULL;
while (true)
{
std::unique_lock<std::mutex> guard(mymutex);
while (tasks.empty())
{
//如果获得了互斥锁,但是条件不合适的话,pthread_cond_wait会释放锁,不往下执行。
//当发生变化后,条件合适,pthread_cond_wait将直接获得锁。
mycv.wait(guard);
}
pTask = tasks.front();
tasks.pop_front();
if (pTask == NULL)
continue;
pTask->doTask();
delete pTask;
pTask = NULL;
}
return NULL;
}
void* producer_thread()
{
int taskID = 0;
Task* pTask = NULL;
while (true)
{
pTask = new Task(taskID);
//使用括号减小guard锁的作用范围
{
std::lock_guard<std::mutex> guard(mymutex);
tasks.push_back(pTask);
std::cout << "produce a task, taskID: " << taskID << ", threadID: " << std::this_thread::get_id() << std::endl;
}
//释放信号量,通知消费者线程
mycv.notify_one();
taskID ++;
//休眠1秒
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return NULL;
}
int main()
{
//创建5个消费者线程
std::thread consumer1(consumer_thread);
std::thread consumer2(consumer_thread);
std::thread consumer3(consumer_thread);
std::thread consumer4(consumer_thread);
std::thread consumer5(consumer_thread);
//创建一个生产者线程
std::thread producer(producer_thread);
producer.join();
consumer1.join();
consumer2.join();
consumer3.join();
consumer4.join();
consumer5.join();
return 0;
}
2020.9.8
今天面试问道 mutex再加锁情况下,第二个线程再加锁会发生什么行为,我回答阻塞,然后面试官说会挂起,我心想挂起和阻塞不是一回事情吗,回去然后我在网上翻阅,各种五花八门的解释。贴了个貌似能解释通的比喻,我也不知道这比喻合不合理,之后还得看看操作系统导论,大学课程忘光了。
首先这些术语都是对于线程来说的。对线程的控制就好比你控制了一个雇工为你干活。你对雇工的控制是通过编程来实现的。
挂起线程的意思就是你对主动对雇工说:“你睡觉去吧,用着你的时候我主动去叫你,然后接着干活”。
使线程睡眠的意思就是你主动对雇工说:“你睡觉去吧,某时某刻过来报到,然后接着干活”。
线程阻塞的意思就是,你突然发现,你的雇工不知道在什么时候没经过你允许,自己睡觉呢,但是你不能怪雇工,肯定你 这个雇主没注意,本来你让雇工扫地,结果扫帚被偷了或被邻居家借去了,你又没让雇工继续干别的活,他就只好睡觉了。至于扫帚回来后,雇工会不会知道,会不会继续干活,你不用担心,雇工一旦发现扫帚回来了,他就会自己去干活的。因为雇工受过良好的培训。这个培训机构就是操作系统。
后来我翻了下操作系统导论,书上只是说了线程的 启动 就绪 阻塞 和死亡状态,都没找到挂起这个词。。。我觉得阻塞状态是对的,一种让出cpu时间分片的操作。