显示标签为“Programming”的博文。显示所有博文
显示标签为“Programming”的博文。显示所有博文

2010年10月25日星期一

关于多线程中操作的原子性

看到百度07年的一个招聘试题:
以下多线程对 int 型变量x的操作,哪几个需要进行同步:
A. x=y; B. x++; C. ++x; D. x=1;

看到这个题目,觉得还是没有什么把握,到网上搜到一个篇非常详细的文章,很忍不住要转过来,原文地址是:http://www.parallellabs.com/2010/04/15/atomic-operation-in-multithreaded-application/,希望作者不要介意,看到这个作者写博文相当认真,他的博客都是关于并行计算的,地址在这里。此博文详细的介绍了原子性操作的机制,详细的分析解答了此题目。

以下是转载,转载来自parallellabs.com

------------------------------------------------------------------------------

 

多线程程序中操作的原子性


0. 背景


 

原子操作就是不可再分的操作。在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制,同时也是一些常见的多线程Bug的源头。本文主要讨论了三个问题:1. 多线程程序中对变量的读写操作是否是原子的?2. 多线程程序中对Bit field(位域)的读写操作是否是线程安全的?3. 程序员该如何使用原子操作?

1. 多线程环境下对变量的读写操作是否是原子的?


我们先从一道很热门的百度笔试题讲起。很多人讲不清楚其背后的原理,下面我们就来对它进行一下剖析(其实这个题目有点歧义,后面我们会讲到):
以下多线程对int型变量x的操作,哪几个需要进行同步:( )
A. x=y; B. x++; C. ++x; D. x=1;

要彻底理解这个问题,我们首先需要从硬件讲起。以常见的X86 CPU来说,根据Intel的参考手册,它基于以下三种机制保证了多核中加锁的原子操作(8.1节):
(1)Guaranteed atomic operations (注:8.1.1节有详细介绍)
(2)Bus locking, using the LOCK# signal and the LOCK instruction prefix
(3)Cache coherency protocols that ensure that atomic operations can be carried out on cached data structures (cache lock); this mechanism is present in the Pentium 4, Intel Xeon, and P6 family processors

这三个机制相互独立,相辅相承。简单的理解起来就是
(1)一些基本的内存读写操作是本身已经被硬件提供了原子性保证(例如读写单个字节的操作);
(2)一些需要保证原子性但是没有被第(1)条机制提供支持的操作(例如read-modify-write)可以通过使用”LOCK#”来锁定总线,从而保证操作的原子性
(3)因为很多内存数据是已经存放在L1/L2 cache中了,对这些数据的原子操作只需要与本地的cache打交道,而不需要与总线打交道,所以CPU就提供了cache coherency机制来保证其它的那些也cache了这些数据的processor能读到最新的值(关于cache coherency可以参加我的一篇博文)。

那么CPU对哪些(1)中的基本的操作提供了原子性支持呢?根据Intel手册8.1.1节的介绍:

从Intel486 processor开始,以下的基本内存操作是原子的:
• Reading or writing a byte(一个字节的读写)
• Reading or writing a word aligned on a 16-bit boundary(对齐到16位边界的字的读写)
• Reading or writing a doubleword aligned on a 32-bit boundary(对齐到32位边界的双字的读写)

从Pentium processor开始,除了之前支持的原子操作外又新增了以下原子操作:
• Reading or writing a quadword aligned on a 64-bit boundary(对齐到64位边界的四字的读写)
• 16-bit accesses to uncached memory locations that fit within a 32-bit data bus(未缓存且在32位数据总线范围之内的内存地址的访问)

从P6 family processors开始,除了之前支持的原子操作又新增了以下原子操作:
• Unaligned 16-, 32-, and 64-bit accesses to cached memory that fit within a cache line(对单个cache line中缓存地址的未对齐的16/32/64位访问)

那么哪些操作是非原子的呢?
Accesses to cacheable memory that are split across bus widths, cache lines, and
page boundaries are not guaranteed to be atomic by the Intel Core 2 Duo, Intel®
Atom™, Intel Core Duo, Pentium M, Pentium 4, Intel Xeon, P6 family, Pentium, and
Intel486 processors.(说点简单点,那些被总线带宽、cache line以及page大小给分隔开了的内存地址的访问不是原子的,你如果想保证这些操作是原子的,你就得求助于机制(2),对总线发出相应的控制信号才行)。

需要注意的是尽管从P6 family开始对一些非对齐的读写操作已经提供了原子性保障,但是非对齐访问是非常影响性能的,需要尽量避免。当然了,对于一般的程序员来说不需要太担心这个,因为大部分编译器会自动帮你完成内存对齐。

回到最开始那个笔试题。我们先反汇编一下看看它们到底执行了什么操作:










01x = y;










02mov eax,dword ptr [y]










03mov dword ptr [x],eax










04










05x++;










06mov eax,dword ptr [x]










07add eax,1










08mov dword ptr [x],eax










09










10++x;










11mov eax,dword ptr [x]










12add eax,1










13mov dword ptr [x],eax










14










15x = 1;










16mov dword ptr [x],1




(1)很显然,x=1是原子操作。
因为x是int类型,32位CPU上int占32位,在X86上由硬件直接提供了原子性支持。实际上不管有多少个线程同时执行类似x=1这样的赋值语句,x的值最终还是被赋的值(而不会出现例如某个线程只更新了x的低16位然后被阻塞,另一个线程紧接着又更新了x的低24位然后又被阻塞,从而出现x的值被损坏了的情况)。

(2)再来看x++和++x。
其实类似x++, x+=2, ++x这样的操作在多线程环境下是需要同步的。因为X86会按三条指令的形式来处理这种语句:从内存中读x的值到寄存器中,对寄存器加1,再把新值写回x所处的内存地址(见上面的反汇编代码)。

例如有两个线程,它们按照如下顺序执行(注意读x和写回x是原子操作,两个线程不能同时执行):

time    Thread 1         Thread 2
0      load eax, x
1                            load eax, x
2      add eax, 1        add eax, 1
3      store x, eax
4                            store x, eax

我们会发现最终x的值会是1而不是2,因为Thread 1的结果被覆盖掉了。这种情况下我们就需要对x++这样的操作加锁(例如Pthread中的mutex)以保证同步,或者使用一些提供了atomic operations的库(例如Windows API中的atomic库,Linux内核中的atomic.h,Java concurrent库中的Atomic Integer,C++0x中即将支持的atomic_int等等,这些库会利用CPU提供的硬件机制做一层封装,提供一些保证了原子性的API)。

(3)最后来看看x=y。
在X86上它包含两个操作:读取y至寄存器,再把该值写入x。读y的值这个操作本身是原子的,把值写入x也是原子的,但是两者合起来是不是原子操作呢?我个人认为x=y不是原子操作,因为它不是不可再分的操作。但是它需要不需要同步呢?其实问题的关键在于程序的上下文。

例如有两个线程,线程1要执行{y = 1; x = y;},线程2要执行{y = 2; y = 3;},假设它们按如下时间顺序执行:

time    Thread 1        Thread 2
0        store y, 1
1                            store y, 2
2        load eax, y
3                            store y, 3
4        store x, eax

那么最终线程1中x的值为2,而不是它原本想要的1。我们需要加上相应的同步语句确保y = 2不会在线程1的两条语句之间发生。y = 3那条语句尽管在load y和store x之间执行,但是却不影响x=y这条语句本身的语义。所以你可以说x=y需要同步,也可以说x=y不需要同步,看你怎么理解题意了。x=1是否需要同步也是一样的道理,虽然它本身是原子操作,但是如果有另一个线程要读x=1之后的值,那肯定也需要同步,否则另一个线程读到的就是x的旧值而不是1了。

2. 对Bit field(位域)的读写操作是否是线程安全的?


Bit field常用来高效的存储有限位数的变量,多用于内核/底层开发中。一般来说,对同一个结构体内的不同bit成员的多线程访问是无法保证线程安全的。

例如Wikipedia中的如下例子:










01struct foo {










02 int flag : 1;










03 int counter : 15;










04};










05










06struct foo my_foo;










07










08/* ... */










09










10/* in thread 1 */










11










12pthread_mutex_lock(&my_mutex_for_flag);










13my_foo.flag = !my_foo.flag;










14pthread_mutex_unlock(&my_mutex_for_flag);










15










16/* in thread 2 */










17










18pthread_mutex_lock(&my_mutex_for_counter);










19++my_foo.counter;










20pthread_mutex_unlock(&my_mutex_for_counter);




两个线程分别对my_foo.flag和my_foo.counter进行读写操作,但是即使有上面的加锁方式仍然不能保证它是线程安全的。原因在于不同的成员在内存中的具体排列方式“跟Byte Order、Bit Order、对齐等问题都有关,不同的平台和编译器可能会排列得很不一样,要编写可移植的代码就不能假定Bit-field是按某一种固定方式排列的”[3]。而且一般来讲CPU对内存操作的最小单位是word(X86的word是16bits),而不是1bit。这就是说,如果my_foo.flag和my_foo.counter存储在同一个word里,CPU在读写任何一个bit member的时候会同时把两个值一起读进寄存器,从而造成读写冲突。这个例子正确的处理方式是用一个mutex同时保护my_foo.flag和my_foo.counter,这样才能确保读写是线程安全的。

C++0x草案中对bit field是这样定义的:
连续的多个非0bit的bit fields是属于同一个memory location的;长度为0bit的bit field会把占单独的一个memory location。对同一个memory location的读写不是线程安全的;对不同memory location的读写是线程安全的。
例如在下图的例子中bf1和bf2是同一个memory location,bf3是一个单独的memory location,bf4是一个单独的memory location:


这里有一个因为Bit field不是线程安全所导致的一个Linux内核中的Bug

引用一下Pongba的总结
所以,如果你的多个bitfields是连续的,同时又想要无冲突的读取它们,有两种做法,一是在中间用0大小bitfield隔开,但这种做法实际上就消除了bitfield的节省内存的初衷,因为为了使它们不冲突,至少被隔开的两个bitfield肯定不可能共享byte了。另一种做法当然就是用锁了。

3. 程序员该怎么用Atomic操作?


一般情况下程序员不需要跟CPU提供的原子操作直接打交道,所以只需要选择语言或者平台提供的atomic API即可。而且使用封装好了的API还有一个好处是它们常常还提供了诸如compare_and_swap,fetch_and_add这样既有读又有写的较复杂操作的封装。

常见的API如下:

Windows上InterlockedXXXX的API
GNU/Linux上linux kernel中atomic_32.h
GCC中的Atomic Builtins (__sync_fetch_and_add()等)
Java中的java.util.concurrent.atomic
C++0x中的atomic operation
Intel TBB中的atomic operation

4. 参考文献:


[1] 关于变量操作的原子性(atomicity)FAQ
[2] http://en.wikipedia.org/wiki/Atomic_operation
[3] 关于内存对齐、bit field等 –《Linux C编程一站式学习》
[4] Do you need mutex to protect an ‘int’?
[5] C++ Concurrency in Action
[6] Multithreaded simple data type access and atomic variables

-------------------------------------------------------------------------------------------

转载到这里结束

allellabs.com

2010年9月27日星期一

Windows多线程编程

前面写了一篇Linux多线程编程,由于算法要整合到一个运行在Windows平台下的系统中,所以需要改成Windows下的多线程方式(看我折腾的)。这里简单列一个Windows多线程编程的例子。其实我也想尝试是用一个跨平台的C++库BoostBoost::Thread类库可以在不同的平台下运行多线程,而且多线程功能非常丰富和强大,使用起来也很容易,但是还需要安装这个库,先暂时放弃这个方案,因为毕竟我现在需要的多线程功能非常简单。

Windows API实现多线程

在上一篇文章中,已经列出了一个表,列出了Linux线程API和Windows下线程API的大致对应关系。根据那个表,和Linux下多线程经验,也能大概知道Windows下实现多线程的方法首先来看一个例子:

/* example.c */
#include <stdfx.h>
#include <stdio.h>
#include <process.h>

const int NLOOP = 100;
int counter = 0;
void thread(void*);
CRITICAL_SECTION beswap ;
int main()
{
HANDLE pnt[2];
InitializeCriticalSection(&beswap);
pnt[0] = (HANDLE)_beginthread(doit,0,NULL);
pnt[1] = (HANDLE)_beginthread(doit,0,NULL);
WaitForMultipleObjects( 2, pnt, TRUE, 1000L);
DeleteCriticalSection(&beswap);
return 0;
}
void doit(void*)
{
    printf("go...\n");
    int i, val = 0;
    for(i = 0; i < NLOOP; ++i)
    {
        EnterCriticalSection(&beswap);
        val = counter;
         printf("%d\n",val+1);
        counter = val + 1;
        LeaveCriticalSection(&beswap);
    }
    printf("end...\n");
    return ;
}

根据这个例子,多线程的过程解释如下:

1、编写线程函数,这里线程函数要遵循如下函数原型:

DWORD WINAPI threadFunc(LPVOID lpvThreadParm);

函数的输入参数是一个DWORD的类型,可以是一个整数,也可以是一个内存指针,具体的意义由编程者自己决定。返回值是一个DWORD型的值。

2、创建一个线程

一个进程的主线程是由操作系统自动生成,如果你要让一个主线程创建额外的线程,你可以调用来CreateThread完成,使用的时候注意包含#include<process.h>头文件。其函数原型如下:

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpsa,DWORD cbstack,LPTHREAD_START_ROUTINE lpStartAddr,
LPVOID lpvThreadParm, DWORD fdwCreate,LPDWORD lpIDThread);

     其中,lpsa参数为一个指向SECURITY_ATTRIBUTES结构的指针。如果想让对象为缺省安全属性的话,可以传一个NULL,如果想让任一个子进程都可继承一个该线程对象句柄,必须指定一个SECURITY_ATTRIBUTES结构,其中bInheritHandle成员初始化为TRUE。
    参数cbstack表示线程为自己所用堆栈分配的地址空间大小,0表示采用系统缺省值。 
    参数lpStartAddr用来表示新线程开始执行时代码所在函数的地址,即为线程函数。
    lpvThreadParm为传入线程函数的参数,
    fdwCreate参数指定控制线程创建的附加标志,可以取两种值。如果该参数为0,线程就会立即开始执行,如果该参数为CREATE_SUSPENDED,则系统产生线程后,初始化CPU,登记CONTEXT结构的成员,准备好执行该线程函数中的第一条指令,但并不马上执行,而是挂起该线程。
    最后一个参数lpIDThread 是一个DWORD类型地址,返回赋给该新线程的ID值。

此外,还可以使用_beginthread等函数来创建线程,正如本例子中使用的方法:

handle=(HANDLE)_beginthread(threadFunc,0, NULL);

函数_beginthread的函数原型如下:

uintptr_t _beginthread(
  void( *start_address )( void * ),
  unsigned stack_size,
  void *arglist
  );

start_address新线程的起始地址,指向新线程调用的函数的起始地址;stack_size stack_size 新线程的堆栈大小,可以为0;arglist arglist 传递给线程的参数列表,无参数是为NULL。

3、终止线程,如果某线程调用了ExitThread 函数,就可以终止自己。

VOID ExitThread(UINTfuExitCode );

这个函数为调用该函数的线程设置了退出码fuExitCode后, 就终止该线程。调用TerminateThread函数亦可终止线程。

BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);

该函数用来结束由hThread参数指定的线程, 并把dwExitCode设成该线程的退出码。当某个线程不在响应时,我们可以用其他线程调用该函数来终止这个不响应的线程。

4、另外还有设置程序的优先级,挂起和恢复线程之类的。这里就不详述,因为还没有用到。

BOOL SetThreadPriority(HANDLE hThread,intnPriority); // 设置线程优先级
DWORD ResumeThread(HANDLEhThread); // 恢复线程
DWORD SuspendThread(HANDLE hThread); // 挂起线程

5、这里还用到了线程的阻塞函数WaitForMultipleObjects,原型:

DWORD WaitForMultipleObjects(
  DWORD nCount,
  const HANDLE* lpHandles,
  BOOL bWaitAll,
  DWORD dwMilliseconds
  );

WaitForMultipleObjects是Windows中的一个功能非常强大的函数,几乎可以等待Windows中的所有的内核对象。当WaitForMultipleObjects等到多个内核对象的时候,如果它的bWaitAll 参数设置为false。其返回值减去WAIT_OBJECT_0 就是参数lpHandles数组的序号。如果同时有多个内核对象被触发,这个函数返回的只是其中序号最小的那个。如果为TRUE 则等待所有信号量有效在往下执行。(FALSE 当有其中一个信号量有效时就向下执行)。

对应的还有一个函数WaitForSingleObject,用来等待指定的内核对象。

6、还有互斥锁的使用方法,在例子中也有很清楚的使用方法。单进程的线程可以使用这种临界资源对象来解决同步互斥问题,该对象不能保证哪个个线程能够获得到临界资源对象,因而该系统能公平的对待每一个线程。

MFC实现多线程

MFC对Windows API进行了封装,可以说使用起来更加简单,而且提供了很多多线程的扩张功能。MFC实现多线程有两种,一种是工作者线程,另一种是用户界面线程。前一种是简单的线程,后一种是带有消息队列的线程。
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);

CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);

这是一个重载函数,前一个用来创建一个工作线程,后一个用来创建界面线程。MFC框架还提供了很多同步类,来实现线程间的同步和通信,具体的使用方法可以参考MSDN,由于还没有用到,等用到了,再详细总结。另外,在编程中,我也是用WaitForMultipleObjects来等待了AfxBeginThread创建的线程,而没有出现问题。

参考地址:

http://wujblog.appspot.com/2010/09/26/linux-multi-thread.html

http://www.wangchao.net.cn/bbsdetail_69676.html

http://blog.csdn.net/zjl_1026_2001/archive/2008/03/18/2193626.aspx

http://msdn.microsoft.com/en-us/library/s3w9x78e(VS.80).aspx

http://msdn.microsoft.com/zh-cn/library/975t8ks0(v=VS.80).aspx


2010年9月26日星期日

Linux多线程编程

在背景差分算法的实现过程中,由于要达到实时性的要求,就想用多线程来实现,进行并行计算,充分利用CPU的多核。之前并没有写过多线程的程序,在网上搜索关键字“C++多线程编程”,后来才知道,多线程并不是C++语言的特性,而是与平台有关系,每个平台实现的机制也是不一样的,所以说这种“C++多线程编程“提法就是有问题的。闲话少叙,下面言归正传。

线程的基本概念


线程的基本概念,在网上有很多的介绍,我觉得北大BBS上一个精华帖介绍的比较详细,在这里,线程相关的重要概念有:用户级线程、轻进程(LWP, Lightweight Process), 非绑定线程(Unbound Treads),绑定线程(Bound Thread)。对于每个线程有相关的属性和状态。有了多线程,就带来相关的数据访问同步的问题,有互斥锁,条件锁,信号量等等。


Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明(这段话是抄过来的,clone()还真的不知道是什么,到时候查一下)。


多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。下面有一个表,转载自这里(这篇文章严谨的介绍了Linux上线程编程的相关经验)


表 1. 线程函数列表











































































对象 操作 Linux Pthread API Windows SDK 库对应 API
线程 创建 pthread_create CreateThread
退出 pthread_exit ThreadExit
等待 pthread_join WaitForSingleObject
互斥锁 创建 pthread_mutex_init CreateMutex
销毁 pthread_mutex_destroy CloseHandle
加锁 pthread_mutex_lock WaitForSingleObject
解锁 pthread_mutex_unlock ReleaseMutex
条件 创建 pthread_cond_init CreateEvent
销毁 pthread_cond_destroy CloseHandle
触发 pthread_cond_signal SetEvent
广播 pthread_cond_broadcast SetEvent / ResetEvent
等待 pthread_cond_wait / pthread_cond_timedwait SingleObjectAndWait


简单的多线程例子


这个表列出了Linux多线程API,作为一个多线程编程的入门,我是参考这里,这个文章对上面每个API的使用方法都有详细的介绍,并且配有相关的例子,是入门很好的资料,但是遗憾的是,他的这些例子,我并没有编译通过,有些地方需要改动一下。下面我也摘抄一部分到这里(做了一点点的修改,原文有一些错误),说先看到一个最简单的多线程的例子:



/* example1.c*/
#include <stdio.h>
#include <pthread.h>
void* thread(void* param)
{
    int i;
    for(i=0;i<3;i++)
        printf("This is a pthread.\n");
}

int main(void)
{
    pthread_t id;
    int i,ret;
    ret=pthread_create(&id,NULL, thread,NULL);
    if(ret!=0){
        printf ("Create pthread error!\n");
        exit (1);
    }
    for(i=0;i<3;i++)
        printf("This is the main process.\n");
    pthread_join(id,NULL);
    return (0);
}



编译上面的程序使用如下命令:



g++ example1.c -lpthread -o example1




顺便提一下,原文上是使用gcc命令,但是我在编译的时候得到了一个错误:undefined reference to `__gxx_personality_v0',网上有人说使用g++编译程序就能解决,原因是gcc不会帮你链接c++的运行库,但g++会。挺有道理的,似懂非懂。



这样,第一个多线程就能运行了,这里用到了pthred_t 类型,用来标示一个线程它在本质上一个无符号的长整型。函数pthread_create用来创建一个线程,注意线程创建完成就立即运行。此函数的原型是:



extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr, void *(*__start_routine) (void *), void *__arg));



第一个参数是指向线程标识符的指针;第二个参数是一个指向线程属性类型(pthread_attr_t)的指针,用来设置线程的相关属性;第三个参数是线程运行函数起始地址,这个函数需要有这样的原型void* (*) (void*);最后一个参数是运行函数的参数。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。


这个例子中还有一个函数pthread_join,其原型如下:



extern int pthread_join __P ((pthread_t __th, void **__thread_return));



第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。其函数的原型如下:



extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));



唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。

线程的属性设置


在上一节的例子里,我们用pthread_create函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。


属性结构为pthread_attr_t,它同样在头文件/usr/include/pthread.h中定义。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。


关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。


设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(非绑定的)。例子如下:



#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;

/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);

pthread_create(&tid, &attr, my_function, NULL);



线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。具体怎么用,还是参考这个原文吧,不然就把原文全部抄袭过来了。


参考地址:


http://fanqiang.chinaunix.net/a4/b8/20010811/0905001105.html


http://fanqiang.chinaunix.net/a4/b8/20010811/0905001105.html


http://www.fegensoft.com/fegensoft2002/seeksilence/Linux/10/8/index.htm


http://blog.sina.com.cn/s/blog_6b2757530100l639.html