10个线程依次打印0-100

本文共1413字。
Copyright: 知识共享署名 非商业性使用 相同方式共享 4.0 国际许可协议 | CC BY-NC-SA 4.0

系统线程调度策略有三种,默认为SCHED_OTHER

1
2
3
4
5
6
/*
* POSIX scheduling policies
*/
#define SCHED_OTHER 1
#define SCHED_FIFO 4
#define SCHED_RR 2

方法一

线程竞争互斥锁。如果某个线程抢到锁且轮到此线程输出,则输出;否则直接释放锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <thread>

int number = 0;
int notPrinted = 0;
std::mutex mutex_number;

constexpr int MAX_NUM = 100;

void printRemainder(const int remainder) {
int policy;
sched_param sch{};
pthread_getschedparam(pthread_self(), &policy, &sch);
printf("sched policy %d, thread priority %d\n", policy, sch.sched_priority);

while (true) {
mutex_number.lock();
if (number > MAX_NUM) {
mutex_number.unlock();
break;
}
if (number % 10 == remainder) {
std::cout << "mythread_" << remainder << ": " << number << std::endl; // 输出
number++;
} else {
notPrinted++;
}

mutex_number.unlock();
}
std::cout << "mythread_" << remainder << " finish" << std::endl; // mythread_1完成
}

int main() {
std::cout << "Create and Start!" << std::endl;

std::vector<std::thread *> threads;
threads.reserve(9);

sched_param sch{};
for (int i = 0; i < 10; ++i) {
auto *thread = new std::thread(printRemainder, i);
pthread_setschedparam(thread->native_handle(), SCHED_OTHER, &sch);
threads.emplace_back(thread);
}

// 调用每个thread的join
std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));

std::cout << "Not printed: " << notPrinted << std::endl;
std::cout << "Finish and Exit!" << std::endl;
return 0;
}

可以看到最终输出了Not printed: 144525,也就是说有144525次线程调度并未输出数值,因为系统调度到的线程不是接下来需要输出数字的线程。

这种方法浪费了太多的系统资源。

如果在线程释放锁之后主动放弃CPU(sched_yield();),Not printed会降低至900左右。(Why?)

方法二

因为线程调度是由操作系统完成的,所以方法一不能很好的完成线程0打印0之后唤醒线程1去打印1这种操作。我们需要尝试修改操作系统的线程调度策略来完成线程的顺序调度。

我们将调度策略设置为SCHED_FIFO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>
#include <thread>

int number = 0;
int notPrinted = 0;
std::mutex mutex_number;

constexpr int MAX_NUM = 100;

void printRemainder(int const remainder) {
int policy;
sched_param sch{};
pthread_getschedparam(pthread_self(), &policy, &sch);
printf("sched policy %d, thread priority %d\n", policy, sch.sched_priority);

while (true) {
mutex_number.lock();
if (number > MAX_NUM) {
mutex_number.unlock();
break;
}
if (number % 10 == remainder) {
std::cout << "mythread_" << remainder << ": " << number << std::endl; // 输出
number++;
} else {
notPrinted++;
}

mutex_number.unlock();
// 主动放弃CPU
sched_yield();
}
std::cout << "mythread_" << remainder << " finish" << std::endl; // mythread_1完成
}

int main() {
std::cout << "Create and Start!" << std::endl;

std::vector<std::thread *> threads;
threads.reserve(9);

sched_param sch{};
for (int i = 0; i < 10; ++i) {
auto *thread = new std::thread(printRemainder, i);
pthread_setschedparam(thread->native_handle(), SCHED_FIFO, &sch);
threads.emplace_back(thread);
}

// 调用每个thread的join
std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));

std::cout << "Not printed: " << notPrinted << std::endl;
std::cout << "Finish and Exit!" << std::endl;
return 0;
}

可以看到最终输出了Not printed: 1872,相比于方法一的144525已经有很大的优化。这1872次调度到了错误的线程,是因为线程在创建完成后立刻开始进入调度队列,被系统调度时开始执行。当我们创建了2个线程时,系统不停地调度这两个线程,但实际上真正需要输出数字的线程3还未被创建,所以线程创建初期时会存在调度了错误的线程的情况。

方法三

针对方法二再进行优化,当线程创建时,并不真正开始执行,而是所有线程创建完成后统一开始执行。只需在线程创建前做一下mutex_number.lock();,线程全部创建完成后执行mutex_number.unlock();来解锁,让所有线程同时开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <thread>

int number = 0;
int notPrinted = 0;
std::mutex mutex_number;

constexpr int MAX_NUM = 100;

void printRemainder(int const remainder) {
int policy;
sched_param sch{};
pthread_getschedparam(pthread_self(), &policy, &sch);
printf("sched policy %d, thread priority %d\n", policy, sch.sched_priority);

while (true) {
mutex_number.lock();
if (number > MAX_NUM) {
mutex_number.unlock();
break;
}
if (number % 10 == remainder) {
std::cout << "mythread_" << remainder << ": " << number << std::endl; // 输出
number++;
} else {
notPrinted++;
}

mutex_number.unlock();
// 主动放弃CPU
sched_yield();
}
std::cout << "mythread_" << remainder << " finish" << std::endl; // mythread_1完成
}
// 我希望thread创建之后不要立刻运行,而是等10个线程创建完成后统一开始执行
int main() {
std::cout << "Create and Start!" << std::endl;

std::vector<std::thread *> threads;
threads.reserve(9);

sched_param sch{};
mutex_number.lock();
for (int i = 0; i < 10; ++i) {
auto *thread = new std::thread(printRemainder, i);
pthread_setschedparam(thread->native_handle(), SCHED_FIFO, &sch);
threads.emplace_back(thread);
}
mutex_number.unlock();

// 调用每个thread的join
std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));

std::cout << "Not printed: " << notPrinted << std::endl;
std::cout << "Finish and Exit!" << std::endl;
return 0;
}

这种方式下依然输出Not printed: 1214,检查发现线程调度并不是严格按照0、1、2、3…、9的顺序调度的,而是比较平均地调度了每个线程。我们使用10个线程输出0-100,每次调度到正确的线程的可能性是10%,那么正确调度100次所需要的平均调度总次数为1000次。在多次运行时,也发现Not printed平均在900左右。

总结

SCHED_FIFO调度策略并不是我们想的那样能够严格按照顺序来执行这10个线程,并且sched_yield();也很大程度上影响了调度的效率,具体原因还需深究。

不主动释放CPU Not printed 主动释放CPU Not printed
方法一 144525 1899
方法二 59165 1872
方法三 66781 1895