Board logo

标题: 如何使用 C++11 编写 Linux 多线程程序(3)线程其它方法和特点及线程调度 [打印本页]

作者: look_w    时间: 2018-4-22 13:40     标题: 如何使用 C++11 编写 Linux 多线程程序(3)线程其它方法和特点及线程调度

thread 类是一个特殊的类,它不能被拷贝,只能被转移或者互换,这是符合线程的语义的,不要忘记这里所说的线程是直接被操作系统调度的。线程的转移使用 move 函数,示例如下:
清单 6.例子 thread_move.cc
1
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.cc
1
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.cc
1
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.cc
1
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.cc
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
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 函数的参数。




欢迎光临 电子技术论坛_中国专业的电子工程师学习交流社区-中电网技术论坛 (http://bbs.eccn.com/) Powered by Discuz! 7.0.0