清单 5.例子 thread_kill.cc1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | static void on_signal_term( int sig){
cout << "on SIGTERM:" << this_thread::get_id() << endl;
pthread_exit(NULL);
}
void threadPosixKill( void ){
signal (SIGTERM, on_signal_term);
thread * t = new thread ( [](){
while ( true ){
++counter;
}
});
pthread_t tid = t->native_handle();
cout << "tid=" << tid << endl;
this_thread::sleep_for( chrono::seconds(1) );
pthread_kill(tid, SIGTERM);
t->join();
delete t;
cout << "thread destroyed." << endl;
}
|
上述例子还可以用来给某个线程发送其它信号,具体的 pthread_exit 函数调用的约定依赖于具体的操作系统的实现,所以,这个方法是依赖于具体的操作系统的,而且,因为在 C++11 里面没有这方面的具体约定,用这种方式也是依赖于 C++编译器的具体实现的。
线程类 std::thread 的其它方法和特点 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.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);
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 (); }
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[i] = 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[i]->join();
delete tb[i];
};
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 函数的参数。
线程间的数据交互和数据争用 (Data Racing) 同一个进程内的多个线程之间多是免不了要有数据互相来往的,队列和共享数据是实现多个线程之间的数据交互的常用方式,封装好的队列使用起来相对来说不容易出错一些,而共享数据则是最基本的也是较容易出错的,因为它会产生数据争用的情况,即有超过一个线程试图同时抢占某个资源,比如对某块内存进行读写等,如下例所示: 清单 11.例子 thread_data_race.cc1 2 3 4 5 6 7 8 9 10 11 12 13 14 | static void
inc( int *p ){
for ( int i = 0; i < COUNT; i++){
(*p)++;
}
}
void threadDataRacing( void ){
int a = 0;
thread ta( inc, &a);
thread tb( inc, &a);
ta.join();
tb.join();
cout << "a=" << a << endl;
}
|
这是简化了的极端情况,我们可以一眼看出来这是两个线程在同时对&a 这个内存地址进行写操作,但是在实际工作中,在代码的海洋中发现它并不一定容易。从表面看,两个线程执行完之后,最后的 a 值应该是 COUNT * 2,但是实际上并非如此,因为简单如 (*p)++这样的操作并不是一个原子动作,要解决这个问题,对于简单的基本类型数据如字符、整型、指针等,C++提供了原子模版类 atomic,而对于复杂的对象,则提供了最常用的锁机制,比如互斥类 mutex,门锁 lock_guard,唯一锁 unique_lock,条件变量 condition_variable 等。 现在我们使用原子模版类 atomic 改造上述例子得到预期结果:
|