线程及同步(linux)

来源:互联网 发布:苏联解体 知乎 编辑:程序博客网 时间:2024/06/05 23:45

1.Linux里面使用多线程的方法:
    #include<unistd.h>//linux编程需要系统头文件
    #include <pthread.h>
    Gcc -o  main.cpp -lpthread  #必须自己加入pthead这个库,gcc不会自动的给你加入

2.创建多线程
    线程是程序调度的基本单位,但是线程可以共享进程里面的资源。
    线程拥有自己的堆栈,因为线程有自己的局部变量。

    线程的执行顺序:
    pthread_create 创建一个线程。

         pthread_create的参数说明:
             第一个参数为指向线程标识符的指针。
             第二个参数用来设置线程属性。
             第三个参数是线程运行函数的起始地址。
             最后一个参数是运行函数的参数。

          下面的pthread_create函数调用创建的线程是joinable线程,必须由主线程调用pthread_join来回收资源: 

          pthread_create(&threadID,NULL,threadFunction,NULL);

        (1).线程的开始运行: 因为pthread_create创建的线程会使这个线程处于可运行状态,而不是单独的启动。所以pthread_create 创建的线程很有可能执行时间很短,在pthread_create没有返回的时候就已经执行完毕了。特别要注意的是linux中的线程没有挂起状态,所以为了让一个线程暂停执行我们可以用condition条件变量来达到目的(这个也是实现线程池的方法)。 线程的运行就这样开始了。
        (2).线程的结束:线程的结束是当线程函数执行完毕或者线程调用了函数 pthread_exit 这个函数。需要注意的是主线程(创建线程的线程)结束了,那么进程也就结束了,所有的线程都将结束。Exit,_exit等函数是结束进程的函数,会kill掉这个进程内的所有线程。
        特别要注意的是:即使在调用了pthread_exit 这个函数,仍然会调用对象的析构函数,就像正常返回一样。
        (3).是否需要线程的返回值:
        如果我们不需要线程执行完毕之后的返回值,那么我们应该让这个线程是分离线程,否则就让线程是joinable的线程。 
        可以使用函数 pthread_attr_setdetachstate 这个函数设置线程属性让这个线程是分离的还是joinable的。      
        joinable线程的特点:必须由主线程回收资源,函数是pthread_join. Pthread_join这个函数会使得主线程被阻塞,一直到等到对应的线程执行完毕,然后可以在pthread_join里面得到线程的返回值。
        分离线程(detached)的特点:线程资源直接由操作系统回收,线程执行完毕后就结束了。不会给主线程发信号。
        注意的是,当这个线程是deattached的时候,如果线程的执行时间很短,而且是在pthread_create函数返回之前就执行完毕了的话,那么因为这个线程的资源已经可以被操作系统回收,所以在主线程里面得到的线程号就可能已经被操作系统分配给别的线程了,可能出现潜在的错误。
        
       设置线程为分离线程:
      status = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);     
        设置线程为joinable线程:
       status = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

     这里有7个基本的函数:
   
        pthread_create :创建一个线程
        pthread_self:  得到自己的线程号
        pthread_exit: 线程退出函数
        pthread_join: 线程结束之后回收线程资源。
        pthread_attr_setdetachstate: 设置线程是否是分离线程
        pthread_attr_init: 用默认值初始化属性
        pthread_attr_destroy(&attr);
        pthread_deattach: 把一个运行的线程设置为deattach的
   
    (2).Pthread_join 和 pthread_exit的联系:
     Void pthread_exit(void *retval)  其中retval是线程结束的时候的返回值,要注意的是不要让retval执行局部变量。
        Int pthread_joint(pthread_t th, void ** retval) 其中pthread_t是线程的ID,retval是线程调用pthread_exit是的返回值retval。 当然如果retval为NULL的话,就不接收返回值。
   
        void * strVal;
        pthread_join(curThread,&strVal);//这里的代码要注意的是 不能把strVal定义为 void** strVal,会报错
   
    (3).刚写多线程的时候要注意下面代码的问题(可能主线程已经执行完了,但是子线程还没开始执行)
        Pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
        pthread_create(&curThread,&attr,myThread,0);  
        pthread_attr_destroy(&attr);
        我们只需要加上 msleep(500), 让主线程过0.5秒再执行,就能解决问题

 

下面是一个列子:

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


void* threadFun(void *)
{
    printf("thread fun begin.\n");

    printf("thread fun end.\n");
   
}

int main()
{
    int status = 0;
    pthread_t threadID;
   
    printf("main begin.\n");

    pthread_attr_t attr;

    status = pthread_attr_init(&attr);
    if(status !=0 )
    {
        printf("init attr failed.\n") ;
    }

    if(status == 0)
    {
        status = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
        if(status != 0)
        {
            printf("init thread failed.\n") ;
        }
    }
       
    if(status == 0)
    {
        status = pthread_create(&threadID,&attr,&threadFun,NULL) ;
        if(status != 0)
        {
            printf("create thread failed.\n") ;
        }
    }

    usleep(3);
    printf("main end.\n");
   
   
    return 0;
}

 

 

3.线程同步之无名信号量
   无名信号量本质就是一个非负整数。它的值是保存在内存中的。
   (1)int sem_init(sem_t *sem, int pshared, unsigned int value);
       初始化一个信号量。
       Pshared如果等于0的话表示这个信号量只能在这个进程的所有线程内共享,不能在进程间共享。如果pshared不等于0的话,表示这个信号量可以被fork创建的子进程继承。也就是这个信号量是子进程和父进程共享的变量了。
       Value表示信号量的初始化值。
   (2)sem_destroy 用来释放信号量
   (3)当使用sem_post() 函数使得信号量的值加1;
   (4)Sem_wait() :如果信号量大于0,那么信号量减1,并且这个函数立即返回;
                   但是如果信号量等于0的话,这个函数被阻塞,直到信号量大于0.
       Sem_trywait():是sem_wait()的非阻塞版本。直接对信号量减1,如果信号量等于0,那就返回一个错误。
       Sem_timedwait():和sem_wait()功能一样,只是如果等待超过了预定的时间就返回一个错误。
   (5)sem_getvalue() 函数,可以得到当前信号量的值

3.1线程同步之命名信号量
    命名信号量可以在非亲缘进程之间使用。命名信号量是以文件的形式存在于系统中的。
    由于子进程共享父进程打开的文件描述符,所以在子进程中就自动的共享一个命名信号量了。
   
    相关的函数有:
        sem_open,
        sem_close,
        sem_unlink,
        sem_post,
        sem_wait

    在使用有名信号量的时候,和无名信号量共享 sem_wait 和 sem_post 函数。
    区别是有名信号量用 sem_open 代替 sem_init, 另外在结束的时候要像关闭文件一样去关闭这个有名信号量。
    (1).打开一个已存在的有名信号量,或创建并初始化一个有名信号量。一个单一的调用就完成了信号量的创建、初始化和权限的设置。
    sem_t *sem_open(const char *name, int oflag, mode_t mode, int value);
        name   是文件的路径名
        Oflag  有 O_CREAT 或 OCREAT|EXCL 两个取值
        mode_t 控制新的信号量的访问权限
        value  指定信号量的初始化值
       
        注意:这里的name不能写成 /tmp/aaa.sem 这样的格式,因为在linux下,sem都是创建在 /dev/shm目录下面。你可以将名字写成 /mysem 或者 mysem, 创建出来的文件都是/dev/shm/sem.mysem
       
        当 oflag = O_CREAT时,若name指定的信号量不存在时,则会创建一个,而且后面的mode和value参数必须有效。若name指定的信号量已存在,则直接打开该信号量,同时忽略mode和value参数。
        当 oflag = O_CREAT|O_EXCL时,若name指定的信号量已经存在,则函数会直接返回error
    (2).一旦你使用了信号量,销毁它们就变得很重要。
        在销毁之前,要确定所有对这个有名信号量的引用都已经通过 sem_close() 函数关闭了,然后只需要调用 sem_unlink()去删除系统中的信号量。
        但是如果有任何处理器或者线程引用这个信号量,sem_unlink()函数不会起到任何作用。因为有名信号量有一个引用计数,只有在引用计数为0的时候sem_unlink()才会把信号量从系统中删除掉。
   

4.线程同步之互斥
    互斥是一个原子操作。一次只允许一个线程拥有互斥锁,其它试图拥有互斥锁的线程会被阻塞,直到没有其它线程拥有互斥锁。
    互斥很明显可以看出来,是只能用于线程的,不能用于进程。
    (1)Mutex 三种类型
        快速(fast) Mutex: 就是一般的Mutex。 但是我们需要注意的是,如果线程已经给这个Mutex上锁了,但是该线程又再一次给这个Mutex上锁,那线程将会无限阻塞,会导致死锁。
        递归(recursive)Mutex: 获得锁的线程可以多次加锁
           递归Mutex有一个锁定计数。如果线程首次成功获取Mutex时,那么锁定计数为1。线程每重新锁定该Mutex一次,锁定计数就加1. 线程每解除该Mutex一次,锁定计数就减1。直到减到为0,其它线程就可以获取该Mutex了。
       
        错误检测(error checking)Mutex: 如果该Mutex已经上锁了,那么 pthread_mutex_lock() 将会返回错误,而不会阻塞。错误码是:EPERM
   
    (2)创建三种类型的锁的两种方法:
        1)通过initializer这种静态变量来初始化mutex
            这里有三种 INITIALIZER:PTHREAD_MUTEX_INITIALIZER,PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP,PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP
            ex:
            pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
            pthread_mutext_lock(&mutex); 
            ...
            pthread_mutex_unlock(&mutex);
            其中initializer可以参加下图
    2)通过设置属性来初始化mutex
        函数原型: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
        attr参数为NULL或者缺省,表示使用默认的互斥锁类型。
       
        在调用了pthread_mutex_init后,我们可以使用pthread_mutexattr_settype来设置互斥锁的类型:
       
        int pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type);
   
    type 参数指定互斥锁的类型,有一下类型:
   
    PTHREAD_MUTEX_FAST_NP
    PTHREAD_MUTEX_RECURSIVE_NP
    PTHREAD_MUTEX_ERRORCHECK_NP
   
    ex:
    pthread_mutex_t mutex;
    pthread_mutexattr_t mutexattr;
   
    pthread_mutexattr_init(&mutexattr);
   
    pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_TIMED_NP);
    pthread_mutex_init(&mutex,&attr);
    pthread_mutex_destroy(&mutex);
   
    3) 与Mutex创建相关的几个函数
        int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
        int pthread_mutex_lock(pthread_mutex_t *mutex);
        int pthread_mutex_trylock(pthread_mutex_t *mutex);
        int pthread_mutex_unlock(pthread_mutex_t *mutex);
        int pthread_mutex_destroy(pthread_mutex_t *mutex);
        trylock解释:和lock()的功能基本一样,只是如果现在有线程在使用这个Mutex的话,那么它将立即返回,否则就成功的拥有这个Mutex。但是如果是一个 递归锁的话,而且当前拥有这个Mutex的线程和调用 trylock 的是同一个线程,那么Mutex的锁定计数会加1.

5.条件变量
   条件变量的使用逻辑:用Mutex保证得到一个临界区,在临界区内有下面的语句。flag的作用:表示要干某件事的前提条件是否已经满足,如果不满足的话就用条件变量阻塞当前的线程,直到等到这个条件变量得到通知。
   While(!flag)的作用:虽然条件变量得到了通知,但是 有可能干某件事的前提条件仍然不满足。
   pthread_cond_wait的作用: 首先unlock互斥变量mut,然后线程被挂起,直到cond得到通知。然后再由while判断干事的前提条件是否满足。
  
   pthread_mutex_lock(&mut);
   while (!flag)
   { 
        pthread_cond_wait(&cond, &mut);
   } 
   …条件满足之后就开始干事
   pthread_mutex_unlock(&mut);
  
  
   下面是一个线程把事情干完之后,给等待的条件变量发送一个通知。
   pthread_mutex_lock(&mutex);
   if (这件事干完了,而且干完后需要通知别的线程) 
    pthread_cond_signal(&cond);
   pthread_mutex_unlock(&mutex);
  
   1)条件变量相关的函数:
       pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
       int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
       int pthread_cond_signal(pthread_cond_t *cond);
       int pthread_cond_broadcast(pthread_cond_t *cond);
       int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
       int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,timespec *abstime);
       int pthread_cond_destroy(pthread_cond_t *cond);
  
   详细说明:
   (1).创建和注销
       条件变量和互斥锁一样,都有静态和动态两种创建方式
       a.静态创建:使用 PTHREAD_COND_INITIALIZER 常量。
           ex:
           pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
       b.动态创建:使用pthread_cond_init() 函数
           int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
           尽管POSIX标准为条件变量定义了属性,但是 linux threads么有实现,因此 cond_attr 通常为NULL
      
       注销一个条件变量需要用 pthread_cond_destroy(), 只有没有线程在该变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为linux实现条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
  
   这里要注意的是:
   pthread_cond_signal 一次只唤醒一个等待的线程,即使有多个线程在等待也只唤醒一个。
   pthread_cond_broadcast 把所有等待的线程都唤醒。

 

6.TSD (thread specified Data)也叫线程局部存储。
    进程内定义的全局变量和函数内定义的static变量,所有线程都可以访问。
    TSD就是只能在同一个线程内访问的数据,在其他线程中即使访问的是同名的变量,但是得到的也是不同的copy。
    Linux实现方式有两种:
    (1)__thread int i;
    (2)使用函数:
        int pthread_key_create(pthread_key_t *key,void (*destructor) (void *));
        int pthread_key_delete(pthread_key_t key);
        int pthread_setspecific(pthread_key_t key, const void *value);
        void *pthread_getspecific(pthread_key_t key);
        pthread_key_create() 来创建该变量。该函数有两个参数,第一个参数就是上面声明的 pthread_key_t 变量,第二个参数是一个清理函数,用来在线程释放该线程存储的时候被调用。该函数指针可以设成 NULL ,这样系统将调用默认的清理函数。

 


线程的终止有两种方式:线程正常退出 或者 线程收到了一个取消信号(比如用户按下了ctrl + c)。
当线程收到一个取消消息后,他可以忽略取消信号、立即执行取消信号、或者运行至cancellation point。

pthread_cancel(id) 给线程发送取消消息,但不一定保证成功。
pthread_testcancel() 的作用是在线程里面设置一个取消点(cancellation point). 但是如果 取消功能 没被enable的话,那么pthread_testcancel() 就不会产生任何作用。

 

 

 

0 0