如何使用 C++11 编写 Linux 多线程程序(3)线程其它方法和特点及线程调度
- UID
- 1066743
|
如何使用 C++11 编写 Linux 多线程程序(3)线程其它方法和特点及线程调度
thread 类是一个特殊的类,它不能被拷贝,只能被转移或者互换,这是符合线程的语义的,不要忘记这里所说的线程是直接被操作系统调度的。线程的转移使用 move 函数,示例如下:
清单 6.例子 thread_move.cc1
2
3
4
5
6
7
8
9
10
11
12
| void threadMove(void){
int a = 1;
thread t( [](int* pa){
for(;;){
*pa = (*pa * 33) % 0x7fffffff;
if ( ( (*pa) >> 30) & 1) break;
}
}, &a);
thread t2 = move(t); // 改为 t2 = t 将不能编译。
t2.join();
cout << "a=" << a << endl;
}
|
在这个例子中,如果将 t2.join() 改为 t.join() 将会导致整个进程被结束,因为忘记了调用 t2 也就是被转移的线程的 join() 方法,从而导致整个进程被结束,而 t 则因为已经被转移,其 id 已被置空。
线程实例互换使用 swap 函数,示例如下:
清单 7.例子 thread_swap.cc1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| void threadSwap(void){
int a = 1;
thread t( [](int* pa){
for(;;){
*pa = (*pa * 33) % 0x7fffffff;
if ( ( (*pa) >> 30) & 1) break;
}
}, &a);
thread t2;
cout << "before swap: t=" << t.get_id()
<< ", t2=" << t2.get_id() << endl;
swap(t, t2);
cout << "after swap : t=" << t.get_id()
<< ", t2=" << t2.get_id() << endl;
t2.join();
cout << "a=" << a << endl;
}
|
互换和转移很类似,但是互换仅仅进行实例(以 id 作标识)的互换,而转移则在进行实例标识的互换之前,还进行了转移目的实例(如下例的t2)的清理,如果 t2 是可聚合的(joinable() 方法返回 true),则调用 std::terminate(),这会导致整个进程退出,比如下面这个例子:
清单 8.例子 thread_move_term.cc1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| void threadMoveTerm(void){
int a = 1;
thread t( [](int* pa){
for(;;){
*pa = (*pa * 33) % 0x7fffffff;
if ( ( (*pa) >> 30) & 1) break;
}
}, &a);
thread t2( [](){
int i = 0;
for(;;)i++;
} );
t2 = move(t); // 将会导致 std::terminate()
cout << "should not reach here" << endl;
t2.join();
}
|
所以,在进行线程实例转移的时候,要注意判断目的实例的 id 是否为空值(即 id())。
如果我们继承了 thread 类,则还需要禁止拷贝构造函数、拷贝赋值函数以及赋值操作符重载函数等,另外,thread 类的析构函数并不是虚析构函数。示例如下:
清单 9.例子 thread_inherit.cc1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class MyThread : public thread{
public:
MyThread() noexcept : thread(){};
template<typename Callable, typename... Args>
explicit
MyThread(Callable&& func, Args&&... args) :
thread( std::forward<Callable>(func),
std::forward<Args>(args)...){
}
~MyThread() { thread::~thread(); }
// disable copy constructors
MyThread( MyThread& ) = delete;
MyThread( const MyThread& ) = delete;
MyThread& operator=(const MyThread&) = delete;
};
|
因为 thread 类的析构函数不是虚析构函数,在上例中,需要避免出现下面这种情况:
MyThread* tc = new MyThread(...);
...
thread* tp = tc;
...
delete tp;
这种情况会导致 MyThread 的析构函数没有被调用。
线程的调度我们可以调用 this_thread::yield() 将当前调用者线程切换到重新等待调度,但是不能对非调用者线程进行调度切换,也不能让非调用者线程休眠(这是操作系统调度器干的活)。
清单 10.例子 thread_yield.cc1
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
| void threadYield(void){
unsigned int procs = thread::hardware_concurrency(), // 获取物理线程数目
i = 0;
thread* ta = new thread( [](){
struct timeval t1, t2;
gettimeofday(&t1, NULL);
for(int i = 0, m = 13; i < COUNT; i++, m *= 17){
this_thread::yield();
}
gettimeofday(&t2, NULL);
print_time(t1, t2, " with yield");
} );
thread** tb = new thread*[ procs ];
for( i = 0; i < procs; i++){
tb = new thread( [](){
struct timeval t1, t2;
gettimeofday(&t1, NULL);
for(int i = 0, m = 13; i < COUNT; i++, m *= 17){
do_nothing();
}
gettimeofday(&t2, NULL);
print_time(t1, t2, "without yield");
});
}
ta->join();
delete ta;
for( i = 0; i < procs; i++){
tb->join();
delete tb;
};
delete tb;
}
|
ta 线程因为需要经常切换去重新等待调度,它运行的时间要比 tb 要多,比如在作者的机器上运行得到如下结果:
1
2
3
4
5
6
7
8
9
| $time ./a.out
without yield elapse 0.050199s
without yield elapse 0.051042s
without yield elapse 0.05139s
without yield elapse 0.048782s
with yield elapse 1.63366s
real 0m1.643s
user 0m1.175s
sys 0m0.611s
|
ta 线程即使扣除系统调用运行时间 0.611s 之后,它的运行时间也远大于没有进行切换的线程。
C++11 没有提供调整线程的调度策略或者优先级的能力,如果需要,只能通过调用相关的 pthread 函数来进行,需要的时候,可以通过调用 thread 类实例的 native_handle() 方法或者操作系统 API pthread_self() 来获得 pthread 线程 id,作为 pthread 函数的参数。 |
|
|
|
|
|