LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
Posted yantuguiguziPGJ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口相关的知识,希望对你有一定的参考价值。
目录
1114. 按序打印
我们提供了一个类:
public class Foo {
public void first() { print("first"); }
public void second() { print("second"); }
public void third() { print("third"); }
}
三个不同的线程 A、B、C 将会共用一个 Foo
实例。
- 一个将会调用
first()
方法 - 一个将会调用
second()
方法 - 还有一个将会调用
third()
方法
请设计修改程序,以确保 second()
方法在 first()
方法之后被执行,third()
方法在 second()
方法之后被执行。
示例 1:
输入: [1,2,3]
输出: "firstsecondthird"
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。
正确的输出是 "firstsecondthird"。
示例 2:
输入: [1,3,2]
输出: "firstsecondthird"
解释:
输入 [1,3,2] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 third() 方法,线程 C 将会调用 second() 方法。
正确的输出是 "firstsecondthird"。
提示:
- 尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。
- 你看到的输入格式主要是为了确保测试的全面性。
解法
作者:zintrulcre
链接:https://leetcode-cn.com/problems/print-in-order/solution/c-hu-chi-suo-tiao-jian-bian-liang-xin-hao-liang-yi/
来源:力扣(LeetCode)
class Foo {
std::atomic<bool> a{ false };
std::atomic<bool> b{ false };
public:
void first(function<void()> printFirst) {
printFirst();
a = true;
}
void second(function<void()> printSecond) {
while (!a)
this_thread::sleep_for(chrono::milliseconds(1));
printSecond();
b = true;
}
void third(function<void()> printThird) {
while (!b)
this_thread::sleep_for(chrono::milliseconds(1));
printThird();
}
};
class Foo {
function<void()> task = []() {};
packaged_task<void()> pt_1{ task }, pt_2{ task };
public:
void first(function<void()> printFirst) {
printFirst();
pt_1();
}
void second(function<void()> printSecond) {
pt_1.get_future().wait();
printSecond();
pt_2();
}
void third(function<void()> printThird) {
pt_2.get_future().wait();
printThird();
}
};
class Foo {
promise<void> pro1, pro2;
public:
void first(function<void()> printFirst) {
printFirst();
pro1.set_value();
}
void second(function<void()> printSecond) {
pro1.get_future().wait();
printSecond();
pro2.set_value();
}
void third(function<void()> printThird) {
pro2.get_future().wait();
printThird();
}
};
#include <semaphore.h>
class Foo {
private:
sem_t sem_1, sem_2;
public:
Foo() {
sem_init(&sem_1, 0, 0), sem_init(&sem_2, 0, 0);
}
void first(function<void()> printFirst) {
printFirst();
sem_post(&sem_1);
}
void second(function<void()> printSecond) {
sem_wait(&sem_1);
printSecond();
sem_post(&sem_2);
}
void third(function<void()> printThird) {
sem_wait(&sem_2);
printThird();
}
};
class Foo {
condition_variable cv;
mutex mtx;
int k = 0;
public:
void first(function<void()> printFirst) {
printFirst();
k = 1;
cv.notify_all(); // 通知其他所有在等待唤醒队列中的线程
}
void second(function<void()> printSecond) {
unique_lock<mutex> lock(mtx); // lock mtx
cv.wait(lock, [this](){ return k == 1; }); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 1 才能继续运行
printSecond();
k = 2;
cv.notify_one(); // 随机通知一个(unspecified)在等待唤醒队列中的线程
}
void third(function<void()> printThird) {
unique_lock<mutex> lock(mtx); // lock mtx
cv.wait(lock, [this](){ return k == 2; }); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 2 才能继续运行
printThird();
}
};
class Foo {
mutex mtx_1, mtx_2;
unique_lock<mutex> lock_1, lock_2;
public:
Foo() : lock_1(mtx_1, try_to_lock), lock_2(mtx_2, try_to_lock) {
}
void first(function<void()> printFirst) {
printFirst();
lock_1.unlock();
}
void second(function<void()> printSecond) {
lock_guard<mutex> guard(mtx_1);
printSecond();
lock_2.unlock();
}
void third(function<void()> printThird) {
lock_guard<mutex> guard(mtx_2);
printThird();
}
};
class Foo {
mutex mtx1, mtx2;
public:
Foo() {
mtx1.lock(), mtx2.lock();
}
void first(function<void()> printFirst) {
printFirst();
mtx1.unlock();
}
void second(function<void()> printSecond) {
mtx1.lock();
printSecond();
mtx1.unlock();
mtx2.unlock();
}
void third(function<void()> printThird) {
mtx2.lock();
printThird();
mtx2.unlock();
}
};
1115. 交替打印FooBar
我们提供一个类:
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}
public void bar() {
for (int i = 0; i < n; i++) {
print("bar");
}
}
}
两个不同的线程将会共用一个 FooBar
实例。其中一个线程将会调用 foo()
方法,另一个线程将会调用 bar()
方法。
请设计修改程序,以确保 "foobar" 被输出 n 次。
示例 1:
输入: n = 1
输出: "foobar"
解释: 这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,"foobar" 将被输出一次。
示例 2:
输入: n = 2
输出: "foobarfoobar"
解释: "foobar" 将被输出两次。
解法
作者:mu-lang-ren
链接:https://leetcode-cn.com/problems/print-foobar-alternately/solution/c-jiao-ti-da-yin-foobar-ti-jie-zheng-li-by-mu-lang/
来源:力扣(LeetCode)
class FooBar {
private:
int n;
public:
FooBar(int n) {
this->n = n;
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
std::unique_lock<std::mutex> lk(Mu);//获取Mu锁
v.wait(lk,[this](){return count == 1;});//看v是否满足条件,(锁和变量)
// printFoo() outputs "foo". Do not change or remove this line.
printFoo();
count++;//变量+1,=2,等于2时,foo会阻塞,bar的condition_variable 满足条件
v.notify_one();//通知并唤醒阻塞在v2里面的线程
}
}
void bar(function<void()> printBar) {
//注释同上
for (int i = 0; i < n; i++) {
std::unique_lock<std::mutex> lk(Mu);
v.wait(lk,[this](){return count == 2;});
// printBar() outputs "bar". Do not change or remove this line.
printBar();
count--;
v.notify_one();
}
}
private:
int count = 1;//条件变量
std::condition_variable v;//条件变量对象
std::mutex Mu;//定义一个锁
};
class FooBar {
private:
int n;
mutex m1,m2;
public:
FooBar(int n) {
this->n = n;
m2.lock();
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
m1.lock();
// printFoo() outputs "foo". Do not change or remove this line.
printFoo();
m2.unlock();
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
m2.lock();
// printBar() outputs "bar". Do not change or remove this line.
printBar();
m1.unlock();
}
}
};
class FooBar {
private:
int n;
atomic<bool> fooed = false;
public:
FooBar(int n) {
this->n = n;
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
while(fooed.load())this_thread::yield();
// printFoo() outputs "foo". Do not change or remove this line.
printFoo();
fooed.store(true);
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
while(!fooed.load())this_thread::yield();
// printBar() outputs "bar". Do not change or remove this line.
printBar();
fooed.store(false);
}
}
};
1116. 打印零与奇偶数
假设有这么一个类:
class ZeroEvenOdd {
public ZeroEvenOdd(int n) { ... } // 构造函数
public void zero(printNumber) { ... } // 仅打印出 0
public void even(printNumber) { ... } // 仅打印出 偶数
public void odd(printNumber) { ... } // 仅打印出 奇数
}
相同的一个 ZeroEvenOdd
类实例将会传递给三个不同的线程:
- 线程 A 将调用
zero()
,它只输出 0 。 - 线程 B 将调用
even()
,它只输出偶数。 - 线程 C 将调用
odd()
,它只输出奇数。
每个线程都有一个 printNumber
方法来输出一个整数。请修改给出的代码以输出整数序列 010203040506
... ,其中序列的长度必须为 2n。
示例 1:
输入:n = 2
输出:"0102"
说明:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 "0102"。
示例 2:
输入:n = 5
输出:"0102030405"
解法
看到这个题我第一反应是三个线程交叉进行
--根据图例那么就需要两个锁,分别是两个bool值 SingleMic为true打印 奇数,DoubleMic为true打印 偶数,都为false 打印0
--打完奇数之后两个锁都为false准备打印0,偶数同理,唯一的冲突点在于打印0之后是打印奇数还是偶数,这里我选择声明一个int(此处curnum)来记录当前该打的数
--通过打印0函数之后让这个int ++之后来判断下一个该打的数是奇数还是偶数,这样就可以打开对应的锁即以下代码(如有不妥望指正)
作者:tu-ma-ma
链接:https://leetcode-cn.com/problems/print-zero-even-odd/solution/c-yuan-zi-cao-zuo-jiao-cha-da-yin-by-tu-vruh9/
来源:力扣(LeetCode)
class ZeroEvenOdd {
private:
int n;
int curnum = 0;//当前应该打印数字
public:
ZeroEvenOdd(int n) {
this->n = n;
}
atomic<bool> SingleMic = false;
atomic<bool> DoubleMic = false;
void zero(function<void(int)> printNumber) {
for(int i = 0; i < n; i++)
{
while(SingleMic || DoubleMic)//当奇和偶其中一个准备打印
this_thread::yield();
printNumber(0);
curnum++;//当前应该打印的数字++,根据当前应该打印奇偶来判断开哪个锁
if(curnum % 2 == 0)
{
SingleMic = false;
DoubleMic = true;
}
else
{
SingleMic = true;
DoubleMic = false;
}
}
}
void even(function<void(int)> printNumber) {//偶数
for(int i = 1; i <= n; i++)
{
if(i % 2 != 0) continue;
while(!DoubleMic)//当偶不准备打印
this_thread::yield();
printNumber(i);
SingleMic = false;
DoubleMic = false;
}
}
void odd(function<void(int)> printNumber) {//奇数
for(int i = 1; i <= n; i++)
{
if(i % 2 == 0) continue;
while(!SingleMic)//当奇不准备打印
this_thread::yield();
printNumber(i);
SingleMic = false;
DoubleMic = false;
}
}
};
作者:c-bee-9F5W7dRrwd
链接:https://leetcode-cn.com/problems/print-zero-even-odd/solution/cchun-hu-chi-suo-chun-xin-hao-liang-liang-chong-fa/
来源:力扣(LeetCode)
class ZeroEvenOdd {
private:
int n;
pthread_mutex_t mutex0;
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
public:
ZeroEvenOdd(int n) {
this->n = n;
pthread_mutex_init(&mutex0, NULL);
pthread_mutex_init(&mutex1, NULL);
pthread_mutex_init(&mutex2, NULL);
//lock
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
}
// printNumber(x) outputs "x", where x is an integer.
void zero(function<void(int)> printNumber) {
for(int i = 1; i <= n; i++) {
pthread_mutex_lock(&mutex0);
printNumber(0);
if(i & 1) pthread_mutex_unlock(&mutex1);
else pthread_mutex_unlock(&mutex2);
}
}
void even(function<void(int)> printNumber) {
for(int i = 2; i <= n; i+=2) {
pthread_mutex_lock(&mutex2);
printNumber(i);
pthread_mutex_unlock(&mutex0);
}
}
void odd(function<void(int)> printNumber) {
for(int i = 1; i <= n; i+=2) {
pthread_mutex_lock(&mutex1);
printNumber(i);
pthread_mutex_unlock(&mutex0);
}
}
};
#include <semaphore.h>
class ZeroEvenOdd {
private:
int n;
sem_t zero_sem;
sem_t odd_sem;
sem_t even_sem;
public:
ZeroEvenOdd(int n) {
this->n = n;
sem_init(&zero_sem, 0, 1); //初始化一个0
sem_init(&odd_sem, 0, 0);
sem_init(&even_sem, 0, 0);
}
// printNumber(x) outputs "x", where x is an integer.
void zero(function<void(int)> printNumber) {
for(int i = 1; i <= n; i++) {
sem_wait(&zero_sem);
printNumber(0);
if(i & 1) sem_post(&odd_sem);
else sem_post(&even_sem);
}
}
void even(function<void(int)> printNumber) {
for(int i = 2; i <= n; i+=2) {
sem_wait(&even_sem);
printNumber(i);
sem_post(&zero_sem);
}
}
void odd(function<void(int)> printNumber) {
for(int i = 1; i <= n; i+=2) {
sem_wait(&odd_sem);
printNumber(i);
sem_post(&zero_sem);
}
}
};
1117. H2O 生成
现在有两种线程,氧 oxygen
和氢 hydrogen
,你的目标是组织这两种线程来产生水分子。
存在一个屏障(barrier)使得每个线程必须等候直到一个完整水分子能够被产生出来。
氢和氧线程会被分别给予 releaseHydrogen
和 releaseOxygen
方法来允许它们突破屏障。
这些线程应该三三成组突破屏障并能立即组合产生一个水分子。
你必须保证产生一个水分子所需线程的结合必须发生在下一个水分子产生之前。
换句话说:
- 如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
- 如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。
书写满足这些限制条件的氢、氧线程同步代码。
示例 1:
输入: "HOH"
输出: "HHO"
解释: "HOH" 和 "OHH" 依然都是有效解。
示例 2:
输入: "OOHHHH"
输出: "HHOHHO"
解释: "HOHHHO", "OHHHHO", "HHOHOH", "HOHHOH", "OHHHOH", "HHOOHH", "HOHOHH" 和 "OHHOHH" 依然都是有效解。
提示:
- 输入字符串的总长将会是 3n, 1 ≤ n ≤ 50;
- 输入字符串中的 “H” 总数将会是 2n 。
- 输入字符串中的 “O” 总数将会是 n 。
解法
作者:ffreturn
链接:https://leetcode-cn.com/problems/building-h2o/solution/cjian-dan-yi-dong-de-condition_variables-gwfp/
来源:力扣(LeetCode)
class H2O {
public:
// 氧气的计数
int cntO;
// 氢气的计数
int cntH;
mutex m;
condition_variable cv;
H2O() {
cntO = 0;
cntH = 0;
}
void hydrogen(function<void()> releaseHydrogen) {
unique_lock<mutex> l(m);
cv.wait(l, [this]()
{
// 氢气最大是2
return this->cntH < 2;
});
// releaseHydrogen() outputs "H". Do not change or remove this line.
releaseHydrogen();
++cntH;
// 已经构成 H2O ,重置计数器
if (cntH + cntO == 3)
{
cntH = 0;
cntO = 0;
}
cv.notify_one();
}
void oxygen(function<void()> releaseOxygen) {
unique_lock<mutex> l(m);
cv.wait(l, [this]()
{
// 氧气最大是1
return this->cntO < 1;
});
// releaseOxygen() outputs "O". Do not change or remove this line.
releaseOxygen();
++cntO;
// 已经构成 H2O ,重置计数器
if (cntH + cntO == 3)
{
cntH = 0;
cntO = 0;
}
cv.notify_one();
}
};
注意题目的要求,重点用加粗显示:
如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。所以当一个氢/氧线程到达时,如果与已经到达的其余元素无法组成 H2O,那么它必须被卡在 releaseHydrogen() / releaseOxygen() 之前。看到挺多代码是用计数的方法,数到两个 H 和一个 O 就把信号量重置,这样是不对的。
作者:zerotrac2
链接:https://leetcode-cn.com/problems/building-h2o/solution/c-shou-xie-xin-hao-liang-man-zu-ti-mu-yao-qiu-de-x/
来源:力扣(LeetCode)
class Semaphore {
private:
int n_;
mutex mu_;
condition_variable cv_;
public:
Semaphore(int n): n_{n} {}
public:
void wait() {
unique_lock<mutex> lock(mu_);
if (!n_) {
cv_.wait(lock, [this]{return n_;});
}
--n_;
}
void signal() {
unique_lock<mutex> lock(mu_);
++n_;
cv_.notify_one();
}
};
class H2O {
private:
Semaphore s_hIn, s_oIn;
Semaphore s_hBarrier, s_oBarrier;
public:
H2O(): s_hIn{2}, s_oIn{1}, s_hBarrier{0}, s_oBarrier{0} {}
void hydrogen(function<void()> releaseHydrogen) {
s_hIn.wait();
s_oBarrier.signal();
s_hBarrier.wait();
releaseHydrogen();
s_hIn.signal();
}
void oxygen(function<void()> releaseOxygen) {
s_oIn.wait();
s_oBarrier.wait();
s_oBarrier.wait();
s_hBarrier.signal();
s_hBarrier.signal();
releaseOxygen();
s_oIn.signal();
}
};
使用原子操作即可完成线程间的同步。
作者:NiceBlueChai
链接:https://leetcode-cn.com/problems/building-h2o/solution/cpp-shi-yong-yuan-zi-cao-zuo-jin-xing-tong-bu-da-y/
来源:力扣(LeetCode)
class H2O {
atomic<int> h2;
public:
H2O() {
h2=0;
}
void hydrogen(function<void()> releaseHydrogen) {
// releaseHydrogen() outputs "H". Do not change or remove this line.
while(h2.load()>1)std::this_thread::yield();
releaseHydrogen();
h2++;
}
void oxygen(function<void()> releaseOxygen) {
// releaseOxygen() outputs "O". Do not change or remove this line.
while(h2.load()!=2)std::this_thread::yield();
releaseOxygen();
h2.store(0);
}
};
1118. 设计有限阻塞队列
题目
leetcode原题:1188. 设计有限阻塞队列
实现一个拥有如下方法的线程安全有限阻塞队列:
-
BoundedBlockingQueue(int capacity) 构造方法初始化队列,其中capacity代表队列长度上限。
-
void enqueue(int element) 在队首增加一个element. 如果队列满,调用线程被阻塞直到队列非满。
-
int dequeue() 返回队尾元素并从队列中将其删除. 如果队列为空,调用线程被阻塞直到队列非空。
-
int size() 返回当前队列元素个数。
你的实现将会被多线程同时访问进行测试。每一个线程要么是一个只调用enqueue方法的生产者线程,要么是一个只调用dequeue方法的消费者线程。size方法将会在每一个测试用例之后进行调用。
请不要使用内置的有限阻塞队列实现,否则面试将不会通过。
示例 1:
输入:
1
1
["BoundedBlockingQueue","enqueue","dequeue","dequeue","enqueue","enqueue","enqueue","enqueue","dequeue"]
[[2],[1],[],[],[0],[2],[3],[4],[]]
输出:
[1,0,2,2]
解释:
生产者线程数目 = 1
消费者线程数目 = 1
BoundedBlockingQueue queue = new BoundedBlockingQueue(2); // 使用capacity = 2初始化队列。
queue.enqueue(1); // 生产者线程将1插入队列。
queue.dequeue(); // 消费者线程调用dequeue并返回1。
queue.dequeue(); // 由于队列为空,消费者线程被阻塞。
queue.enqueue(0); // 生产者线程将0插入队列。消费者线程被解除阻塞同时将0弹出队列并返回。
queue.enqueue(2); // 生产者线程将2插入队列。
queue.enqueue(3); // 生产者线程将3插入队列。
queue.enqueue(4); // 生产者线程由于队列长度已达到上限2而被阻塞。
queue.dequeue(); // 消费者线程将2从队列弹出并返回。生产者线程解除阻塞同时将4插入队列。
queue.size(); // 队列中还有2个元素。size()方法在每组测试用例最后调用。
示例 2:
输入:
3
4
["BoundedBlockingQueue","enqueue","enqueue","enqueue","dequeue","dequeue","dequeue","enqueue"]
[[3],[1],[0],[2],[],[],[],[3]]
输出:
[1,0,2,1]
解释:
生产者线程数目 = 3
消费者线程数目 = 4
BoundedBlockingQueue queue = new BoundedBlockingQueue(3); // 使用capacity = 3初始化队列。
queue.enqueue(1); // 生产者线程P1将1插入队列。
queue.enqueue(0); // 生产者线程P2将0插入队列。
queue.enqueue(2); // 生产者线程P3将2插入队列。
queue.dequeue(); // 消费者线程C1调用dequeue。
queue.dequeue(); // 消费者线程C2调用dequeue。
queue.dequeue(); // 消费者线程C3调用dequeue。
queue.enqueue(3); // 其中一个生产者线程将3插入队列。
queue.size(); // 队列中还有1个元素。
由于生产者/消费者线程的数目可能大于1,我们并不知道线程如何被操作系统调度,即使输入看上去隐含了顺序。因此任意一种输出[1,0,2]或[1,2,0]或[0,1,2]或[0,2,1]或[2,0,1]或[2,1,0]都可被接受。
解法
https://www.cnblogs.com/ging/p/14239631.html
解析
阻塞队列,首先想到要使用ReetrantLock来实现锁,同时需要使用lock来创建两个的等待条件,一个是非空一个是非满,生产者线程等待非满条件入队,消费者线程等待非空条件出队。同时题目需求,入队在队首,出队在队尾,可以选择使用LinkList中的addFirst和removeLast来实现。
这样大框架就出来了。
class BoundedBlockingQueue {
private LinkedList<Integer> innerQueue;
private int capacity;
private ReentrantLock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public BoundedBlockingQueue(int capacity) {
innerQueue = new LinkedList<Integer>();
this.capacity = capacity;
}
public void enqueue(int element) throws InterruptedException {
try{
lock.lockInterruptibly();
while(size() == capacity){
notFull.await();
}
innerQueue.addFirst(element);
notEmpty.signalAll();
}finally{
lock.unlock();
}
}
public int dequeue() throws InterruptedException {
try{
lock.lockInterruptibly();
while(size() == 0){
notEmpty.await();
}
int result = innerQueue.removeLast();
notFull.signalAll();
return result;
}finally{
lock.unlock();
}
}
public int size() {
try{
lock.lock();
return innerQueue.size();
}finally{
lock.unlock();
}
}
}
https://blog.csdn.net/liuyanlin007/article/details/103370648
public class BoundedBlockingQueue {
private LinkedList<Integer> queue=new LinkedList<>();
private ReentrantLock lock=new ReentrantLock();
private Condition empty=lock.newCondition();
private Condition full=lock.newCondition();
//目前队列中多少元素
private Integer size=0;
//队列大小
private Integer cap=null;
//构造函数初始化有界队列大小
public BoundedBlockingQueue(int capacity) {
if(cap==null){
lock.lock();
try{
if(cap==null){
cap=capacity;
}
}finally {
lock.unlock();
}
}
}
//入队方法
public void enqueue(int element) throws InterruptedException {
lock.lock();
try{
//如果队列已满,阻塞元素入队
while(size>=cap){
full.await();
}
queue.offerFirst(element);
size+=1;
//唤醒empty
empty.signalAll();
}finally {
lock.unlock();
}
}
//出队方法
public int dequeue() throws InterruptedException {
lock.lock();
int res=-1;
try {
//队列空,阻塞元素出队
while(size==0){
empty.await();
}
res=queue.pollLast();
size-=1;
//唤醒full
full.signalAll();
} finally {
lock.unlock();
}
return res;
}
public int size() {
return size;
}
}
方法1:两个信号量
主要思路:
(1)使用两个信号量实现同步有序执行;
(2)两个信号量 sem_enqueue 初始化为需要的容量capacity,sem_dequeue初始化为 0,配合实现对 队列的共享;
(3)调用函数 enqueue 时,表示有元素要入队列,则sem_enqueue先调用wait函数,若是队列已满,则此时的sem_enqueue的值已经为0,则阻塞在wait函数,否则,元素入队,然后对信号量sem_dequeue调用post函数,表示队列中有元素存在,可以执行出队操作,若是有线程阻塞到 sem_dequeue 的wait函数,则此时可以接着执行;
(4)调用函数 dequeue时,表示有元素要出队,此时先调用信号量 sem_dequeue,此时若队列中没有元素,既若是为空,则阻塞在这里(注意其初始值为0,既若没有执行过入队函数,则一定阻塞在这里),当有元素压入到队列中时,才可以执行,弹出元素后,要对信号领 sem_enqueue执行post操作,表示队列中腾出了空间,可以接着执行入队操作了,若是有线程阻塞在其对应的wait函数,则此时可以接着执行;方法2:使用互斥量和条件变量
主要思路:
(1)使用两个条件变量来标识队列的满 或 空 的状态,使用互斥量实现对 队列的互斥访问;
(2)注意将两个信号量的wait函数放入到while循环中,避免惊群现象;
————————————————
版权声明:本文为CSDN博主「王培琳」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44171872/article/details/107570938
#include<semaphore.h>
class BoundedBlockingQueue {
private:
queue<int> q;
sem_t sem_enqueue;
sem_t sem_dequeue;
int cap;
public:
BoundedBlockingQueue(int capacity) {
cap=capacity;
//初始化两个信号量,一个用于入对,一个用于出队
sem_init(&sem_enqueue,0,capacity);
sem_init(&sem_dequeue,0,0);
}
void enqueue(int element) {
//入队的信号量若大于0,表示有空间剩余,可以接着入队,否则阻塞
sem_wait(&sem_enqueue);
q.push(element);
//出队信号量加1,表示此时队列中有元素存在,可以执行出队操作
sem_post(&sem_dequeue);
}
int dequeue() {
//出队信号量若大于0,表示队列中有元素存在,可以执行出队操作,否则阻塞
sem_wait(&sem_dequeue);
int tmp=q.front();
q.pop();
//入队信号量加1,表示有空余的空间了,可以执行入队操作
sem_post(&sem_enqueue);
return tmp;
}
int size() {
return q.size();
}
};
#include<pthread.h>
class BoundedBlockingQueue {
private:
pthread_mutex_t mutex;
pthread_cond_t de_cond;
pthread_cond_t en_cond;
int cap;
queue<int> q;
public:
BoundedBlockingQueue(int capacity) {
int cap=capacity;
pthread_cond_init(& de_cond,0);
pthread_cond_init(&en_cond,0);
pthread_mutex_init(& mutex,0);
}
void enqueue(int element) {
pthread_mutex_lock(&mutex);//使用互斥量和条件变量配合,需要先对互斥量加锁
while(q.size()==cap){//注意将wait函数放入到while循环中,避免惊群现象
pthread_cond_wait(&en_cond,&mutex);
}
q.push(element);
pthread_cond_signal(&de_cond);//此时队列中有元素,故可以给入队的信号量发信号,解除阻塞
pthread_mutex_unlock(&mutex);//解锁互斥量
}
int dequeue() {
pthread_mutex_lock(& mutex);
while(q.empty()){//避免惊群现象
pthread_cond_wait(&de_cond,& mutex);
}
int tmp=q.front();
q.pop();
pthread_cond_signal(&en_cond);//此时队列中有空间存在,可以给出队的信号量发信号,解除阻塞
pthread_mutex_unlock(& mutex);//解除信号量
return tmp;
}
int size() {
return q.size();
}
};
1195. 交替打印字符串
编写一个可以从 1 到 n 输出代表这个数字的字符串的程序,但是:
- 如果这个数字可以被 3 整除,输出 "fizz"。
- 如果这个数字可以被 5 整除,输出 "buzz"。
- 如果这个数字可以同时被 3 和 5 整除,输出 "fizzbuzz"。
例如,当 n = 15
,输出: 1, 2, fizz, 4, buzz, fizz, 7, 8, fizz, buzz, 11, fizz, 13, 14, fizzbuzz
。
假设有这么一个类:
class FizzBuzz {
public FizzBuzz(int n) { ... } // constructor
public void fizz(printFizz) { ... } // only output "fizz"
public void buzz(printBuzz) { ... } // only output "buzz"
public void fizzbuzz(printFizzBuzz) { ... } // only output "fizzbuzz"
public void number(printNumber) { ... } // only output the numbers
}
请你实现一个有四个线程的多线程版 FizzBuzz
, 同一个 FizzBuzz
实例会被如下四个线程使用:
- 线程A将调用
fizz()
来判断是否能被 3 整除,如果可以,则输出fizz
。 - 线程B将调用
buzz()
来判断是否能被 5 整除,如果可以,则输出buzz
。 - 线程C将调用
fizzbuzz()
来判断是否同时能被 3 和 5 整除,如果可以,则输出fizzbuzz
。 - 线程D将调用
number()
来实现输出既不能被 3 整除也不能被 5 整除的数字。
提示:
- 本题已经提供了打印字符串的相关方法,如
printFizz()
等,具体方法名请参考答题模板中的注释部分。
解法
1、使用linux barrier 屏障
调用pthread_barrier_wait,计数没到设定的值(这里是4) 线程就会休眠
每调用一次pthread_barrier_wait 计数加1,加到4唤醒所有线程,又重新从0开始计数作者:eric-345
链接:https://leetcode-cn.com/problems/fizz-buzz-multithreaded/solution/1195-jiao-ti-da-yin-zi-fu-chuan-by-eric-tyr4w/
来源:力扣(LeetCode)
#include <pthread.h>
class FizzBuzz {
private:
int n;
pthread_barrier_t b;
public:
FizzBuzz(int n) {
this->n = n;
pthread_barrier_init(&b, NULL, 4); // 4个线程
}
~FizzBuzz() {
pthread_barrier_destroy(&b);
}
// printFizz() outputs "fizz".
void fizz(function<void()> printFizz) {
for(int i = 1; i <= n; ++i) {
// 仅被3整除
if (i % 3 == 0 && i % 5 != 0) {
printFizz();
}
pthread_barrier_wait(&b);
}
}
// printBuzz() outputs "buzz".
void buzz(function<void()> printBuzz) {
for(int i = 1; i <= n; ++i) {
// 仅被5整除
if (i % 3 != 0 && i % 5 == 0) {
printBuzz();
}
pthread_barrier_wait(&b);
}
}
// printFizzBuzz() outputs "fizzbuzz".
void fizzbuzz(function<void()> printFizzBuzz) {
for(int i = 1; i <= n; ++i) {
if(i % 3 == 0 && i % 5 == 0) {
printFizzBuzz();
}
pthread_barrier_wait(&b);
}
}
// printNumber(x) outputs "x", where x is an integer.
void number(function<void(int)> printNumber) {
for(int i = 1; i <= n; ++i) {
if(i % 3 != 0 && i % 5 != 0) {
printNumber(i);
}
pthread_barrier_wait(&b);
}
}
};
mutex + condition_variable 的实现
每个线程都维持自己需要等待的状态,知道num > n的时候才结束
要注意能结束线程,避免死循环作者:ffreturn
链接:https://leetcode-cn.com/problems/fizz-buzz-multithreaded/solution/czhong-gui-zhong-ju-de-mutexcondition_va-lkao/
来源:力扣(LeetCode)
class FizzBuzz {
private:
int n;
int num;
condition_variable cv;
mutex m;
public:
FizzBuzz(int n) {
this->n = n;
num = 1;
}
// printFizz() outputs "fizz".
void fizz(function<void()> printFizz) {
unique_lock<mutex> l(m);
while (num <= n)
{
if ((num % 3 == 0) && (num % 5 != 0))
{
printFizz();
++num;
cv.notify_all();
}
cv.wait(l, [this]{
// 结束条件是 num超过范围 或者是 不能被3和5整除的数字
return (num > n) || ((num % 3 == 0) && (num % 5 != 0));
});
}
}
// printBuzz() outputs "buzz".
void buzz(function<void()> printBuzz) {
unique_lock<mutex> l(m);
while (num <= n)
{
if ((num % 3 != 0) && (num % 5 == 0))
{
printBuzz();
++num;
cv.notify_all();
}
cv.wait(l, [this]{
// 结束条件是 num超过范围 或者是 不能被3和5整除的数字
return (num > n) || ((num % 3 != 0) && (num % 5 == 0));
});
}
}
// printFizzBuzz() outputs "fizzbuzz".
void fizzbuzz(function<void()> printFizzBuzz) {
unique_lock<mutex> l(m);
while (num <= n)
{
if ((num % 3 == 0) && (num % 5 == 0))
{
printFizzBuzz();
++num;
cv.notify_all();
}
cv.wait(l, [this]{
// 结束条件是 num超过范围 或者是 能被3和5整除的数字
return (num > n) || ((num % 3 == 0) && (num % 5 == 0));
});
}
}
// printNumber(x) outputs "x", where x is an integer.
void number(function<void(int)> printNumber) {
unique_lock<mutex> l(m);
while (num <= n)
{
if ((num % 3 != 0) && (num % 5 != 0))
{
printNumber(num);
++num;
// 关键一步,避免死循环
if (num > n)
{
cv.notify_all();
break;
}
if ((num % 3 != 0) && (num % 5 != 0))
{
// 依然满足条件,继续下一个num处理
continue;
}
else
{
// 不满足,则把控制权给其他线程
cv.notify_all();
}
}
cv.wait(l, [this]{
// 结束条件是 num超过范围 或者是 能被3和5整除的数字
return (num > n) || ((num % 3 != 0) && (num % 5 != 0));
});
}
}
};
1226. 哲学家进餐
5 个沉默寡言的哲学家围坐在圆桌前,每人面前一盘意面。叉子放在哲学家之间的桌面上。(5 个哲学家,5 根叉子)
所有的哲学家都只会在思考和进餐两种行为间交替。哲学家只有同时拿到左边和右边的叉子才能吃到面,而同一根叉子在同一时间只能被一个哲学家使用。每个哲学家吃完面后都需要把叉子放回桌面以供其他哲学家吃面。只要条件允许,哲学家可以拿起左边或者右边的叉子,但在没有同时拿到左右叉子时不能进食。
假设面的数量没有限制,哲学家也能随便吃,不需要考虑吃不吃得下。
设计一个进餐规则(并行算法)使得每个哲学家都不会挨饿;也就是说,在没有人知道别人什么时候想吃东西或思考的情况下,每个哲学家都可以在吃饭和思考之间一直交替下去。
问题描述和图片来自维基百科 wikipedia.org
哲学家从 0 到 4 按 顺时针 编号。请实现函数 void wantsToEat(philosopher, pickLeftFork, pickRightFork, eat, putLeftFork, putRightFork)
:
philosopher
哲学家的编号。pickLeftFork
和pickRightFork
表示拿起左边或右边的叉子。eat
表示吃面。putLeftFork
和putRightFork
表示放下左边或右边的叉子。- 由于哲学家不是在吃面就是在想着啥时候吃面,所以思考这个方法没有对应的回调。
给你 5 个线程,每个都代表一个哲学家,请你使用类的同一个对象来模拟这个过程。在最后一次调用结束之前,可能会为同一个哲学家多次调用该函数。
示例:
输入:n = 1
输出:[[4,2,1],[4,1,1],[0,1,1],[2,2,1],[2,1,1],[2,0,3],[2,1,2],[2,2,2],[4,0,3],[4,1,2],[0,2,1],[4,2,2],[3,2,1],[3,1,1],[0,0,3],[0,1,2],[0,2,2],[1,2,1],[1,1,1],[3,0,3],[3,1,2],[3,2,2],[1,0,3],[1,1,2],[1,2,2]]
解释:
n 表示每个哲学家需要进餐的次数。
输出数组描述了叉子的控制和进餐的调用,它的格式如下:
output[i] = [a, b, c] (3个整数)
- a 哲学家编号。
- b 指定叉子:{1 : 左边, 2 : 右边}.
- c 指定行为:{1 : 拿起, 2 : 放下, 3 : 吃面}。
如 [4,2,1] 表示 4 号哲学家拿起了右边的叉子。
提示:
1 <= n <= 60
解法
- 学过操作系统课程的同学都知道这个题目,参考答案
- 总共有三种办法可以避免死锁的发生;
- 限定哲学家就餐数量;
作者:mike-meng
链接:https://leetcode-cn.com/problems/the-dining-philosophers/solution/zhe-xue-jia-jiu-can-wen-ti-by-mike-meng/
来源:力扣(LeetCode)
class Semaphore {
public:
Semaphore(int count = 0) : count_(count) {
}
void Set(int count){
count_ = count;
}
void Signal() {
std::unique_lock<std::mutex> lock(mutex_);
++count_;
cv_.notify_one();
}
void Wait() {
std::unique_lock<std::mutex> lock(mutex_);
while(count_ <= 0){
cv_.wait(lock);
}
--count_;
}
private:
std::mutex mutex_;
std::condition_variable cv_;
int count_;
};
class DiningPhilosophers {
public:
DiningPhilosophers() {
guid.Set(4);
}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork) {
int l = philosopher;
int r = (philosopher+1)%5;
guid.Wait();
lock[l].lock();
lock[r].lock();
pickLeftFork();
pickRightFork();
eat();
putRightFork();
putLeftFork();
lock[r].unlock();
lock[l].unlock();
guid.Signal();
}
private:
std::mutex lock[5];
Semaphore guid;
};
同时抬起左右拿叉子;
class DiningPhilosophers {
public:
DiningPhilosophers() {
}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork) {
int l = philosopher;
int r = (philosopher+1)%5;
guid.lock();
lock[l].lock();
lock[r].lock();
pickLeftFork();
pickRightFork();
guid.unlock();
eat();
putRightFork();
putLeftFork();
lock[l].unlock();
lock[r].unlock();
}
private:
std::mutex lock[5];
std::mutex guid;
};
限定就餐策略;
class DiningPhilosophers {
public:
DiningPhilosophers() {
}
void wantsToEat(int philosopher,
function<void()> pickLeftFork,
function<void()> pickRightFork,
function<void()> eat,
function<void()> putLeftFork,
function<void()> putRightFork) {
int l = philosopher;
int r = (philosopher+1)%5;
if(philosopher%2 == 0){
lock[r].lock();
lock[l].lock();
pickLeftFork();
pickRightFork();
}else{
lock[l].lock();
lock[r].lock();
pickLeftFork();
pickRightFork();
}
eat();
putRightFork();
putLeftFork();
lock[l].unlock();
lock[r].unlock();
}
private:
std::mutex lock[5];
};
1242. 多线程网页爬虫
给你一个初始地址 startUrl 和一个 html 解析器接口 HtmlParser,请你实现一个 多线程的网页爬虫,用于获取与 startUrl 有 相同主机名 的所有链接。
以 任意 顺序返回爬虫获取的路径。
爬虫应该遵循:
从 startUrl 开始
调用 HtmlParser.getUrls(url) 从指定网页路径获得的所有路径。
不要抓取相同的链接两次。
仅浏览与 startUrl 相同主机名 的链接。
如上图所示,主机名是 example.org 。简单起见,你可以假设所有链接都采用 http 协议,并且没有指定 端口号。举个例子,链接 http://leetcode.com/problems 和链接 http://leetcode.com/contest 属于同一个 主机名, 而 http://example.org/test 与 http://example.com/abc 并不属于同一个 主机名。
HtmlParser 的接口定义如下:
interface HtmlParser {
// Return a list of all urls from a webpage of given url.
// This is a blocking call, that means it will do HTTP request and return when this request is finished.
public List<String> getUrls(String url);
}
注意一点,getUrls(String url) 模拟执行一个HTTP的请求。 你可以将它当做一个阻塞式的方法,直到请求结束。 getUrls(String url) 保证会在 15ms 内返回所有的路径。 单线程的方案会超过时间限制,你能用多线程方案做的更好吗?
对于问题所需的功能,下面提供了两个例子。为了方便自定义测试,你可以声明三个变量 urls,edges 和 startUrl。但要注意你只能在代码中访问 startUrl,并不能直接访问 urls 和 edges。
拓展问题:
假设我们要要抓取 10000 个节点和 10 亿个路径。并且在每个节点部署相同的的软件。软件可以发现所有的节点。我们必须尽可能减少机器之间的通讯,并确保每个节点负载均衡。你将如何设计这个网页爬虫?
如果有一个节点发生故障不工作该怎么办?
如何确认爬虫任务已经完成?
示例 1:
输入:
urls = [
"http://news.yahoo.com",
"http://news.yahoo.com/news",
"http://news.yahoo.com/news/topics/",
"http://news.google.com",
"http://news.yahoo.com/us"
]
edges = [[2,0],[2,1],[3,2],[3,1],[0,4]]
startUrl = "http://news.yahoo.com/news/topics/"
输出:[
"http://news.yahoo.com",
"http://news.yahoo.com/news",
"http://news.yahoo.com/news/topics/",
"http://news.yahoo.com/us"
]
示例 2:
输入:
urls = [
"http://news.yahoo.com",
"http://news.yahoo.com/news",
"http://news.yahoo.com/news/topics/",
"http://news.google.com"
]
edges = [[0,2],[2,1],[3,2],[3,1],[3,0]]
startUrl = "http://news.google.com"
输出:["http://news.google.com"]
解释:startUrl 链接与其他页面不共享一个主机名。
提示:
1 <= urls.length <= 1000
1 <= urls[i].length <= 300
startUrl 是 urls 中的一个。
主机名的长度必须为 1 到 63 个字符(包括点 . 在内),只能包含从 “a” 到 “z” 的 ASCII 字母和 “0” 到 “9” 的数字,以及中划线 “-”。
主机名开头和结尾不能是中划线 “-”。
参考资料:https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_hostnames
你可以假设路径都是不重复的。
解法
这道题目是test case 有问题还是写的代码有问题。 为啥不给一下开启的线程数量。 MAX_ALIVE_THREAD_NUM 这个量定小了就会超时,多了就会超出内存限制。
我给的做法是用 CountDownLatch。 起某一数量的线程,把耗时的操作htmlParser.getUrls() 放到一个独立的线程中去进行操作。
这里需要维护一个 线程安全的 queue (也可以去限制queue的大小,e.g boundedblockingqueue) 来去保存需要 CrawlWorker 去 crawl 的request url .
因为这里queue和set 都会有多个线程同时读写。所以 要用线程安全的 queue 和set。ConcurrentLinkedQueue 已经保证了多个线程同时读/写访问的安全性了。
每个阶段开的线程数量取决于queue的size和 MAX_ALIVE_THREAD_NUM 中的最小值。
这道题目的本质就是找到独立互不影响的操作, 开启一个线程去执行。对于这道题目就是对每一个url 爬虫都是一个独立request。 然后从爬出的url选出同一个host以及没出现在结果集的作为新的request放到 queue中。
还有一个比较好的练习可以写一下, Merge k sorted list Multithreaded ,关键也是找到线程独立的操作。
/**
* // This is the HtmlParser's API interface.
* // You should not implement it, or speculate about its implementation
* interface HtmlParser {
* public List<String> getUrls(String url) {}
* }
*/
class Solution {
class CrawlWorker implements Runnable {
private String startUrl;
private CountDownLatch countDownLatch;
private HtmlParser htmlParser;
CrawlWorker(String startUrl, CountDownLatch countDownLatch,HtmlParser htmlParser){
this.startUrl = startUrl;
this.countDownLatch = countDownLatch;
this.htmlParser = htmlParser;
}
@Override
public void run() {
parse();
}
private void parse(){
urlSet.add(startUrl);
List<String> urlList = htmlParser.getUrls(startUrl);
for(String url : urlList){
if(urlSet.contains(url) || !getHost(url).equals(hostName)) continue;
queue.offer(url);
}
this.countDownLatch.countDown();
}
}
private final Set<String> urlSet = ConcurrentHashMap.newKeySet();
private final Queue<String> queue = new ConcurrentLinkedQueue<>();
private String hostName;
private static final Integer MAX_ALIVE_THREAD_NUM = 128;
public List<String> crawl(String startUrl, HtmlParser htmlParser) {
hostName = getHost(startUrl);
queue.offer(startUrl);
while(!queue.isEmpty()){
int curThreadNum = Math.min(MAX_ALIVE_THREAD_NUM, queue.size());
CountDownLatch countDownLatch = new CountDownLatch(curThreadNum);
for(int idx = 0; idx < curThreadNum ;idx++){
String curUrl = queue.poll();
CrawlWorker crawlWorker = new CrawlWorker(curUrl,countDownLatch, htmlParser);
Thread thread = new Thread(crawlWorker);
thread.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return new ArrayList<>(urlSet);
}
private static String getHost(String url){
String host = url.substring(7); // all urls use http protocol
int idx = host.indexOf('/');
if(idx == -1) return host;
return host.substring(0,idx);
}
}
作者:huya-402994
链接:https://leetcode-cn.com/problems/web-crawler-multithreaded/solution/qiu-tao-lun-zhe-dao-xie-ding-e-de-ti-by-huya-40299/
来源:力扣(LeetCode)
1279. 红绿灯路口
题目描述:
这是两条路的交叉路口。第一条路是 A 路,车辆可沿 1 号方向由北向南行驶,也可沿 2 号方向由南向北行驶。第二条路是 B 路,车辆可沿 3 号方向由西向东行驶,也可沿 4 号方向由东向西行驶。
每条路在路口前都有一个红绿灯。红绿灯可以亮起红灯或绿灯。
绿灯表示两个方向的车辆都可通过路口。
红灯表示两个方向的车辆都不可以通过路口,必须等待绿灯亮起。
两条路上的红绿灯不可以同时为绿灯。这意味着,当 A 路上的绿灯亮起时,B 路上的红灯会亮起;当 B 路上的绿灯亮起时,A 路上的红灯会亮起.
开始时,A 路上的绿灯亮起,B 路上的红灯亮起。当一条路上的绿灯亮起时,所有车辆都可以从任意两个方向通过路口,直到另一条路上的绿灯亮起。不同路上的车辆不可以同时通过路口。
给这个路口设计一个没有死锁的红绿灯控制系统。
实现函数 void carArrived(carId, roadId, direction, turnGreen, crossCar) :
carId 为到达车辆的编号。
roadId 为车辆所在道路的编号。
direction 为车辆的行进方向。
turnGreen 是一个函数,调用此函数会使当前道路上的绿灯亮起。
crossCar 是一个函数,调用此函数会允许车辆通过路口。
当你的答案避免了车辆在路口出现死锁,此答案会被认定为正确的。当路口已经亮起绿灯时仍打开绿灯,此答案会被认定为错误的。
示例 1:
输入: cars = [1,3,5,2,4], directions = [2,1,2,4,3], arrivalTimes = [10,20,30,40,50]
输出: [
“Car 1 Has
以上是关于LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口的主要内容,如果未能解决你的问题,请参考以下文章
LeetCode 1188. Design Bounded Blocking Queue