Linux下的多线程编程

【Linux】线程 Linux线程的基础知识 阅读详情

1 引言
----------------------------------------------------------
    线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多线程技术已经被许多操作系统所支持,包括Windows/NT,当然也包括Linux。
    为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么样的系统应该选用多线程?我们首先必须回答这些问题。
    使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
    使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
    除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
    1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
    2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
    3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
    下面我们先来尝试编写一个简单的多线程程序。



2 简单的多线程编程
----------------------------------------------------------
    pthread_create()
    pthread_join()
    typedef unsigned long int pthread_t;    
    Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库/usr/lib/libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面我们展示一个最简单的多线程程序example1.c。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

void thread(void) {
    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, (void *)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);
}

我们编译此程序:
gcc example1.c -lpthread -o example1
运行example1,我们得到如下结果:
-------------------------------
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.

再次运行,我们可能得到如下结果:
-------------------------------
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.

也有可能得到如下结果(这种可能居多):
-------------------------------
This is the main process.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
This is a pthread.


    运行结果不一样,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数,pthread_create()和pthread_join(),并声明了一个pthread_t型的变量。

    pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义:
    typedef unsigned long int pthread_t;
    它是一个线程的标识符。

    函数pthread_creat()e用来创建一个线程,它的原型为:
    int pthread_create( pthread_t                 *__thread, 
                        __const pthread_attr_t   *__attr,
                        void *(*__start_routine) (void *), 
                        void                     *__arg );
    第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。

    函数pthread_join用来等待一个线程的结束。函数原型为:
    int pthread_join( pthread_t __th, 
                    void       **__thread_return );
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。

    一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit()来实现。它的函数原型为:
    void pthread_exit(void *__retval) __attribute__ ((__noreturn__));
    唯一的参数是函数的返回代码,只要pthread_join()中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join()的线程则返回错误代码ESRCH。

    在这一节里,我们编写了一个最简单的线程,并掌握了最常用的三个函数pthread_create(),pthread_join()和pthread_exit()。下面,我们来了解线程的一些常用属性以及如何设置这些属性。




3 修改线程的属性
----------------------------------------------------------
    struct pthread_attr_t
    pthread_attr_init()
    pthread_attr_setscope()
    pthread_attr_setdetachstate()
    
    在上一节的例子里,我们用pthread_create()函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了,但我们还是有必要来了解一下线程的有关属性。
    属性结构为struct 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, (void *) my_function, NULL);

    线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态(绑定),这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为     
    pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。
    第二个参数可选为
    PTHREAD_CREATE_DETACHED(分离线程)
    PTHREAD _CREATE_JOINABLE(非分离线程 -- 默认)
    这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create()函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create()的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timewait()函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create()返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
    另外一个可能常用的属性是线程的优先级,它存放在结构sched_param中。用函数
    pthread_attr_getschedparam()
    pthread_attr_setschedparam()
    一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。

    #include <pthread.h>
    #include <sched.h>
    pthread_attr_t attr;
    pthread_t tid;
    struct sched_param param;
    int newprio=20;

    pthread_attr_init(&attr);
    pthread_attr_getschedparam(&attr, &param);
    param.sched_priority=newprio;
    pthread_attr_setschedparam(&attr, &param);
    pthread_create(&tid, &attr, (void *)myfunction, myarg);



4 线程的数据处理
----------------------------------------------------------
    和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字volatile来定义,这是为了防止编译器在优化时(如gcc中使用-OX参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。

4.1 线程数据
    在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在 A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
    和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数据;从一个键读取线程数据;删除键。
    创建键的函数原型为:
    int pthread_key_create( pthread_key_t            *__key,
                            void (*__destr_function) (void *) );
    第一个参数为指向一个键值的指针,第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数pthread_once( pthread_once_t*once_control, void (*initroutine) (void) )一起使用,为了让这个键只被创建一次。函数pthread_once声明一个初始化函数,第一次调用pthread_once时它执行这个函数,以后的调用将被它忽略。

    在下面的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数createWindow,这个函数定义一个图形窗口(数据类型为Fl_Window *,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。

    pthread_key_t myWinKey;
    void createWindow(void ) {
        Fl_Window * win;
        static pthread_once_t once= PTHREAD_ONCE_INIT;
        pthread_once(&once, createMyKey) ;
        win=new Fl_Window(0, 0, 100, 100, "MyWindow");
        setWindow(win);
        pthread_setpecific(myWinKey, win);
    }
    void createMyKey ( void ) {
        pthread_keycreate(&myWinKey, freeWinKey);
    }
    void freeWinKey ( Fl_Window * win){
        delete win;
    }

    这样,在不同的线程中调用函数createMyWin,都可以得到在线程内部均可见的窗口变量,这个变量通过函数 pthread_getspecific得到。在上面的例子中,我们已经使用了函数pthread_setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下:
    int pthread_setspecific ( pthread_key_t __key,
                              __const void *__pointer );
    void *pthread_getspecific ( pthread_key_t __key );
    这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread_setspecific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数pthread_key_delete()用来删除一个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占用的内存,并不释放该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread_key_create()中定义的destructor函数。线程数据的释放必须在释放键之前完成。


4.2 互斥锁
    互斥锁用来保证一段时间内只有一个线程在执行一段代码。必要性显而易见:假设各个线程向同一个文件顺序写入数据,最后得到的结果一定是灾难性的。
    我们先看下面一段代码。这是一个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。

#include <pthread.h>
#include <stdio.h>

void reader_function(void);
void writer_function(void);

char buffer;
int buffer_has_item = 0;
pthread_mutex_t mutex;
struct timespec delay;

int main(void){
    pthread_t reader;
    delay.tv_sec = 2;
    delay.tv_nsec = 0;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&reader, NULL, (void *)&reader_function, NULL);
    writer_function( );
}

void writer_function (void){
    while(1){
        pthread_mutex_lock (&mutex);
        if (buffer_has_item == 0){
            buffer = 100;
            printf("Assign 100 to buffer\n");
            buffer_has_item = 1;
        }
        pthread_mutex_unlock(&mutex);
        //pthread_delay_np(&delay);
    }
}

void reader_function(void){
    while(1){
        pthread_mutex_lock(&mutex);
        if(buffer_has_item == 1){
            printf("Read buffer = %d\n", buffer);
            buffer_has_item = 0;
        }
        pthread_mutex_unlock(&mutex);
        //pthread_delay_np(&delay);
    }
}
    这里声明了互斥锁变量mutex,结构pthread_mutex_t为不公开的数据类型,其中包含一个系统分配的属性对象。函数pthread_mutex_init()用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用函数pthread_mutexattr_init()。
     pthread_mutexattr_setpshared()    
    用来设置互斥锁属性。前一个函数设置属性pshared,它有两个取值    
    PTHREAD_PROCESS_PRIVATE        (默认属性)
    PTHREAD_PROCESS_SHARED
    前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性PTHREAD_PROCESS_PRIVATE。

    pthread_mutexattr_settype()    
    用来设置互斥锁类型,可选的类型有
    PTHREAD_MUTEX_NORMAL
    PTHREAD_MUTEX_ERRORCHECK
    PTHREAD_MUTEX_RECURSIVE
    PTHREAD_MUTEX_DEFAULT
    它们分别定义了不同的上锁、解锁机制,一般情况下,选用最后一个默认属性。

    pthread_mutex_lock()声明开始用互斥锁上锁,此后的代码直至调用
    pthread_mutex_unlock()为止,均被上锁,即同一时间只能被一个线程调用执行。当一个线程执行到pthread_mutex_lock()处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了pthread_delay_np()函数,让线程睡眠一段时间,就是为了防止一个线程始终占据此函数。
    注意:linux下是没有pthread_delay_np(),在Solaries下才有.
    用条件变量pthread_cond_timedwait()比较保险,能保证是当前这个线程处于休眠状态,如果用sleep或usleep,可能使这个线程所处的进程休眠。但我做过试验发现pthread_cond_timedwait和sleep或usleep的效果差不多,不过是在单cpu的环境中。如果是在多cpu的环境中,最好还是做个试验比较一下。    

    上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使用函数pthread_mutex_trylock(),它是函数pthread_mutex_lock()的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自己在程序设计注意这一点。

4.3 条件变量
    前一节中我们讲述了如何使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步。

    条件变量的结构为pthread_cond_t
    pthread_cond_init()被用来初始化一个条件变量。它的原型为:
    int pthread_cond_init( pthread_cond_t             *__cond,
                           __const pthread_condattr_t *__cond_attr );
    其中cond是一个指向结构pthread_cond_t的指针,cond_attr是一个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为pthread_cond_destroy(pthread_cond_t cond)。

    pthread_cond_wait()打开互斥锁并使线程阻塞在一个条件变量上。它的函数原型为:
    int pthread_cond_wait( pthread_cond_t *__cond,
                        pthread_mutex_t *__mutex );

    线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal()和函数 pthread_cond_broadcast()唤醒,但是要注意的是,条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如一个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用while语句实现。
    另一个用来阻塞线程的函数是pthread_cond_timedwait(),它的原型为:
    int pthread_cond_timedwait( pthread_cond_t          *__cond,
                                pthread_mutex_t         *__mutex, 
                                __const struct timespec *__abstime));
    它比函数pthread_cond_wait()多了一个时间参数,经历abstime段时间后,即使条件变量不满足,阻塞也被解除。
    pthread_cond_signal()的原型为:
    int pthread_cond_signal( pthread_cond_t *__cond );
    它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的。要注意的是,必须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用pthread_cond_wait()函数之间被发出,从而造成无限制的等待。下面是使用函数pthread_cond_wait()和函数pthread_cond_signal()的一个简单的例子。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
decrement_count(){
    printf("--- decrement_count lock\n");
    pthread_mutex_lock(&count_lock);
    printf("+++ decrement_count unlock count_lock and wait for count_nonzero\n");
    pthread_cond_wait(&count_nonzero, &count_lock);
    printf("+++ decrement_count gets up\n");
    pthread_mutex_unlock(&count_lock);
    printf("--- decrement_count unlock\n");
}

increment_count(){
    printf("increment_count lock\n");
    pthread_mutex_lock(&count_lock);
    printf("increment_count sleep(1), and wake up decrement_count\n");
    pthread_cond_signal(&count_nonzero);
    printf("increment_count unlock\n");
    pthread_mutex_unlock(&count_lock);
    printf("increment_count unlock\n");
}

int main(void) {
    pthread_t id;
    int i, ret;
    ret=pthread_create(&id, NULL, (void *)increment_count, NULL);
    if(ret != 0){
        printf("Create pthread error!\n");
        exit(1);
    }
    decrement_count();
    pthread_join(id, NULL);
    return(0);
}

    pthread_cond_broadcast(pthread_cond_t *cond)用来唤醒所有被阻塞在条件变量cond上的线程。这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用这个函数。


4.4 信号量
    信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。当公共资源增加时,调用函数sem_post()增加信号量。只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量。函数sem_trywait()和函数pthread_ mutex_trylock()起同样的作用,它是函数sem_wait()的非阻塞版本。下面我们逐个介绍和信号量有关的一些函数,它们都在头文件 /usr/include/semaphore.h中定义。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。函数sem_init()用来初始化一个信号量。它的原型为:
extern int sem_init(sem_t *__sem, int __pshared, unsigned int __value);
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
sem_post(sem_t *sem)用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不再阻塞,选择机制同样是由线程的调度策略决定的。
sem_wait(sem_t *sem)被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。sem_trywait(sem_t *sem)是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
下面我们来看一个使用信号量的例子。在这个例子中,一共有4个线程,其中两个线程负责从文件读取数据到公共的缓冲区,另两个线程从缓冲区读取数据作不同的处理(加和乘运算)。
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSTACK 100

int stack[MAXSTACK][2];
int size=0;
sem_t sem;

void ReadData1(void){
    FILE *fp=fopen("1.dat", "r");
    while(!feof(fp)){
        fscanf(fp, "%d %d", &stack[size][0], &stack[size][1]);
        sem_post(&sem);
        ++size;
    }
    fclose(fp);
}

void ReadData2(void){
    FILE *fp=fopen("2.dat", "r");
    while(!feof(fp)){
        fscanf(fp, "%d %d", &stack[size][0], &stack[size][1]);
        sem_post(&sem);
        ++size;
    }
    fclose(fp);
}

void HandleData1(void){
    while(1){
        sem_wait(&sem);
        printf("Plus: %d + %d = %d\n", stack[size][0], stack[size][1],
                    stack[size][0] + stack[size][1]);
        --size;
    }
}

void HandleData2(void){
    while(1){
        sem_wait(&sem);
        printf("Multiply : %d * %d = %d\n", stack[size][0], stack[size][1],
                    stack[size][0]*stack[size][1]);
        --size;
    }
}

int main(void){
    pthread_t t1, t2, t3, t4;
    sem_init(&sem, 0, 0);
    pthread_create(&t1, NULL, (void *)HandleData1, NULL);
    pthread_create(&t2, NULL, (void *)HandleData2, NULL);
    pthread_create(&t3, NULL, (void *)ReadData1, NULL);
    pthread_create(&t4, NULL, (void *)ReadData2, NULL);
    pthread_join(t1, NULL);
}


在Linux下,我们用命令gcc -lpthread sem.c -o sem生成可执行文件sem。 我们事先编辑好数据文件1.dat和2.dat,假设它们的内容分别为1 2 3 4 5 6 7 8 9 10和-1 -2 -3 -4 -5 -6 -7 -8 -9 -10,我们运行sem,得到如下的结果:

Plus: 0 + 0 = 0
Plus: 0 + 0 = 0
Plus: -9 + -10 = -19
Plus: -7 + -8 = -15
Plus: -5 + -6 = -11
Plus: -3 + -4 = -7
Plus: -1 + -2 = -3
Plus: 0 + 0 = 0
Plus: 9 + 10 = 19
Plus: 7 + 8 = 15
Plus: 5 + 6 = 11
Plus: 3 + 4 = 7


5 小结
----------------------------------------------------------
    多线程编程是一个很有意思也很有用的技术,使用多线程技术的网络蚂蚁是目前最常用的下载工具之一,使用多线程技术的grep比单线程的grep要快上几倍,类似的例子还有很多。希望大家能用多线程技术写出高效实用的好程序来。

Linux——多线程编程 •。 阅读详情

相关推荐

Linux下多线程编程实例解析

提到线程,不得不让人想起进程,所以还是先写下进程与线程的区别吧! 一、进程与线程的区别 进程是程序执行的一个实例,进程有其自己独立的地址空间。一个线程可以含有多个线程,这也是为了提高系统资源利用率,线程的引入有其必然的优势,线程有自己的堆栈。记得操作系统的书籍上有一句经典的话”进程——资源分配的最小单位,线程——程序执行的最小单位“。一个通俗的比喻:进程就像一个人,而线程就好像人体内协

GQ1900的博客 5714

【Linux】线程详解之线程概念

经过前边的学习,我们知道进程拥有一个PCB,在Linux中被称为task_struct,并且有一个进程地址空间,也有一个页表,通过页表指向物理内存,但是从今天开始,对进程的概念可能发生变化,这个我们后边来说,在Linux中,并没有真正的线程,而是使用进程的PCB来模拟线程,也就是说一个线程在创建时,只会去创建一个PCB,而这个PCB也指向主线程的虚拟地址空间,和其他线程一起共享内存的代码和数据。一个线程也被称为一个执行流,这是因为线程是被CPU调度的执行流,而一个进程就是分配系统资源的基本实体。

weixin_61654591的博客 1213

Linux C++多线程编程

1.介绍   使用多线程的理由之一是和进程相比,它是一种非常”节俭”的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种”昂贵”的多任务工作方式。而运行一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程,而且,线程间彼此切换所需的时间也...

krais_wk的博客 3640

Linux线程详解

并行和并发的区别 1. 并发(concurrency):在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。其中两种并发关系分别是同步和互斥。(并发是指同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上有多个进程被同时执行的效果--宏观上并行,针对单核处理器) 互斥:进程间相互排斥的使用临界资源的现象,就叫互斥。 ...

小虾米编程 4万+

精通Linux下的Qt多线程编程

QThread是Qt框架中用于创建和管理线程的一个类。它允许开发者在Qt应用中运行后台线程,执行长时间运行或耗资源的任务,而不阻塞主事件循环,保持用户界面的响应性。在多线程编程中,QThread主要用来分离执行任务的代码和界面代码,从而避免界面线程因执行长时间任务而冻结,改善用户体验。此外,QThread可以为每个线程提供独立的事件循环,支持事件处理机制。

weixin_27645199的博客 821

Linux环境下多线程编程深入指南

Linux多线程编程是操作系统领域内一个关键的高级编程技术,它允许开发者在同一个程序内创建多个执行流,这些执行流可以并发执行,大大提高了程序处理多任务的能力。本章将为读者提供一个多线程编程的基础框架和相关概念的入门知识。在Linux多线程编程中,线程属性定义了线程创建时的行为和特征。这些属性包括线程的优先级、栈大小、是否分离等。默认情况下,新创建的线程会继承其父线程的属性。然而,在某些情况下,我们可能需要定制这些属性,以满足特定的需求。

weixin_35414260的博客 737

【Linux】线程详解

在一个程序里的一个执行路线就叫做线程。更准确的定义是:线程是“一个进程内部的控制序列”一切进程都至少有一个执行线程线程在进程内部运行,本质是在进程地址空间内运行在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更轻量化透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流进程的创建伴随着进程控制块,进程地址空间以及页表的创建,虚拟地址和物理地址就是通过页表建立映射的。

m0_72940975的博客 3685

Linux 线程浅析

Linux 线程浅析

Linux多线程编程

Linux多线程编程一、线程理论基础二、多线程程序设计三、线程==同步==(按顺序) 一、线程理论基础 有了进程,为何还要引入线程呢?使用多线程有哪些好处? 1、和进程相比,线程是一种非常节俭的多任务操作方式,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表维护它的代码段、堆栈段、数据段,这是一种昂贵的多任务工作方式 运行于一个进程中的多线程,它们之间使用相同的地址空间,而且线程间彼此切换所需的时间也远远小于进程间切换所需要的时间 2、线程间方便的通讯机制,对不同进程来说,它们具有独立的数据空间

qq_46186025的博客 4808

华为服务器RAID配置避坑指南:如何通过iBMC快速识别阵列卡型号并完成初始化

本文详细解析华为服务器RAID配置全流程,重点介绍通过iBMC快速识别阵列卡型号、硬盘健康诊断及RAID级别选择的场景化策略。提供从iBMC登录到缓存策略调优的实战技巧,帮助工程师避免常见配置陷阱,提升服务器存储系统性能和稳定性。

weixin_28475533的博客 491

从ISO 17987到SAE J2602:LIN一致性测试标准全景解读与CANoe实现

本文深入解析了从ISO 17987到SAE J2602的LIN一致性测试标准技术演进,详细对比了两大标准体系的技术差异,并基于Vector CANoe平台演示了如何构建符合最新行业要求的自动化测试方案。文章重点介绍了SAE J2602标准的三大改进点,包括唤醒特性、传输层协议和诊断服务的增强测试要求,并提供了CANoe测试环境配置、测试模块集成及测试案例解析的实战指南。

app77的博客 334

Linux系统编程之多线程

线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。相比于进程,线程是轻量级的实体,它们共享进程的资源(比如:内存地址空间等),因此创建和销毁线程的成本较低。因为线程共享相同的地址空间,所以它们之间的通信非常高效,不需要像进程间通信那样通过复杂的机制。

hope_wisdom的技术博客 3094

计算机组成与系统结构(第三版) 袁春风主编 课后习题答案讲解 第一、二章 计算机系统概述 数据的机器级表示

计算机组成与系统结构(第三版) 袁春风主编 课后习题答案讲解 第一、二章 计算机系统概述 数据的机器级表示

qq_73523062的博客 2087

【Linux】多线程编程基础

无论你是否为程序员,相信多线程这个词汇应该都有所耳闻,像是在某个优化很差的游戏中听闻这游戏甚至是单线程的,如果你对多线程感兴趣,不妨点进本文来学习多线程编程,即使没有深厚的C/C++编程基础,你也能到本文学习到如何编写多线程程序。创建锁// 初始化锁 pthread_mutex_t 用于声明互斥量(mutex)对象。

CaTianRi的博客 1378

obisidian插件之【templater】模板的中文说明书

Templater 是一种模板语言,可让您将变量和函数结果插入到笔记中。它还允许您执行操作这些变量和函数的 JavaScript 代码。使用 Templater,您将能够创建强大的模板来自动执行手动任务。

m0_55795604的博客 1万+

从原理图到GDSII:Virtuoso+Pcell实战避坑指南(以TSMC65nm振荡器为例)

本文以TSMC65nm工艺下的振荡器设计为例,详细解析了Virtuoso环境中Pcell的最佳实践方案,涵盖变量管理、版图设计、Calibre验证等关键环节。通过实战案例和避坑指南,帮助工程师高效完成从原理图到GDSII的转化,特别针对LVS验证和参数化单元设计提供了实用解决方案。

fff88的博客 899

Linux下多线程编程

多线程是指在一个进程中同时运行多个线程,每个线程可以独立执行不同的任务。线程是操作系统调度的最小单位,多个线程共享同一个进程的内存空间和资源,但每个线程有自己的栈和寄存器状态。本文展示Linux下多线程编程实例。

SEU123WW的博客 1176

【项目实战】分布式日志系统 之 Exactly-once 语义:确保数据处理的精确性

在分布式系统的数据传输与处理中,是一种核心可靠性保障,其目标是确保数据“仅被处理一次”——既不重复(避免多次处理),也不遗漏(避免未处理)。与,二者共同保障数据处理的精确性。Exactly-once 语义是分布式系统中数据可靠性的高级目标,其核心通过“幂等设计”(解决重复问题)和“分布式事务”(解决原子性问题)实现。在实际应用中,常结合 At-Least-Once 语义与幂等性,或通过事务 API 与唯一 ID 去重的组合,平衡可靠性与系统性能。

奶爸的编程之路,也就一周冷个三天 166

linux多线程编程结束进程,linux多线程编程.pdf

广告提供50多种云计算产品,包括云服务器和云. 创建一站式云产品试用服务,以帮助开发人员和企业以零阈值进入云.使用线程有什么好处?有关多线程编程的一些详细信息,例如如何同步和相互排斥的线程. 这些内容将在本文中介绍. 我看到了这样一个采访问题: 您是否熟悉posix多线程编程技术?如果熟悉,请编写程序以完成以下功能: 1)一个int全局变量g_flag的初始值为0; 2)在主线中启动线程1,并在其...

weixin_34742870的博客 309

43、FMCW雷达高度计仿真

本文介绍了FMCW雷达高度计的波形级建模与仿真过程,重点模拟了着陆和倾斜两种飞行场景以评估其性能。通过结合高程数据和飞行轨迹构建雷达场景,使用自定义信号处理器分析IQ样本并估计高度。结果表明,该高度计在多数情况下满足性能要求,但也存在误差较大的点,提出了地面检测算法优化的潜在方向。

neovim7hacker的博客 135

ubuntu20.04搭建QGC4.2编译环境

QGC 4.2ubuntu18.04也可以编译QGC4.2,但是由于GLBC版本较低,无法使用最新版的QTCreator,Ubuntu20.04可以使用最新版QTCreator。

超维空间科技的博客 1万+

嵌入式Linux多线程编程

1.1 线程的本质栈空间(存储局部变量)寄存器状态(程序计数器等)线程ID和优先级1.2 线程 vs 进程对比fill:#333;color:#333;color:#333;fill:none;进程代码段数据段堆文件描述符信号处理器线程1线程2栈寄存器栈寄存器1.3 Linux线程实现原理CLONE_VM:共享内存空间CLONE_FS:共享文件系统信息:共享文件描述符表。

嵌入式软件工程师养成记 1054

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Linux下c语言多线程编程

引言   线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多   为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。   使用...

木牛的博客 7579

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

Linux多线程编程小结

 Linux多线程编程小结       前一段时间因为开题的事情一直耽搁了我搞Linux的进度,搞的我之前学的东西都遗忘了,很烦躁的说,现在抽个时间把之前所学的做个小节。文章内容主要总结于《Linux程序设计第3版》。1.Linux进程与线程       Linux进程创建一个新线程时,线程将拥有自己的栈(因为线程有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号句柄

愤怒的小狐狸----博客专栏 6万+

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

上一篇: Linux下的C文件系统编程
下一篇: vncserver configuration xstartup
bamboolsu
博客等级 码龄22年 200粉丝 394原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值