Linux多线程编程实战

1.pthread线程操作相关函数

1.1 线程创建

函数声明

int pthread_create(pthread_t *newthread,const pthread_attr_t *attr,
		   void *(*start_rtn)(void*),void *arg);

函数说明

Create a new thread, starting with execution of START-ROUTINE getting passed ARG. Creation attributed come from ATTR. The new
handle is stored in *NEWTHREAD.

  第一个参数表示新建线程的句柄,表示线程ID,第二个参数表示线程属性,第三个参数表示线程执行的函数,第四个参数表示函数执行的参数。

1.2 线程终止

函数声明

void pthread_exit( void * retval );

函数说明

Terminate calling thread.
用于终止调用它的线程,并返回一个指向retval的指针。

1.3 线程等待

函数声明

int pthread_join (pthread_t  th, void ** thread_return);

函数说明

Make calling thread wait for termination of the thread TH. The
exit status of the thread is stored in *THREAD_RETURN, if THREAD_RETURN is not NULL.

  阻塞当前线程,等待th线程的结束。如果thread_return的值不是NULL,那么将会储存pthread_exit的返回值。如果线程结束,该函数会立即返回,被阻塞的函数的资源被回收。

1.4 初始化线程属性

函数声明

int pthread_attr_init (pthread_attr_t * attr)

函数说明

Initialize thread attribute *ATTR with default attributes (detachstate is PTHREAD_JOINABLE, scheduling policy is SCHED_OTHER,no user-provided stack)

  该函数用于初始化线程属性attr,分离状态初始化为PTHREAD_JOINABLE,调度策略是SCHED_OTHER,没有用户提供的堆栈。线程的detachstate分为PTHREAD_CREATE_JOINABLE,PTHREAD_CREATE_DETACHED。所谓线程的分离状态,表示的意思是线程以什么样的方式结束自己,线程属于分离状态时,不需要被其他线程等待,调用pthread_join也不会阻塞当前线程,主线程和子线程相当于两个序列,各自执行,自己运行结束以后,线程就终止了,释放掉自己的资源。而非分离状态的线程,原有主线程会等待创建线程的执行,等待pthread_join函数返回后,才终止线程,释放系统资源。

1.5 设置线程分离状态

函数声明

int pthread_attr_setdetachstate (pthread_attr_t *  attr, int  detachstate)

函数说明

Set detach state attribute.

用于设置函数的分离状态,detachstate被设置为PTHREAD_CREATE_JOINABLE表示非分离, PTHREAD_CREATE_DETACHED表示分离。

1.6 删除线程属性

函数声明

int pthread_attr_destroy (pthread_attr_t *  attr)

函数说明

用于删除线程属性。

在这里先介绍这几个常用的,还有一些函数目前还没有在实践中用过,后续有机会会继续补充。下面是一个关于线程操作的实例:

#include 
#include 
#include 

int mCount = 6;
void* createThread(void* arg);

void* createThread(void* arg){
    int count = (int)(*((int*) arg));
    
    for (int i = 0; i < count; i++)
    {
        printf("createThread i = %d.\n",i);
        if (i == 3)
        {
            pthread_exit("Terminal thread.\n");
        }  
    }
}
void main(){
    pthread_t tid1, tid2;
    pthread_attr_t pthread_attr1, pthread_attr2;
    void* retval1 = NULL;
    void* retval2 = NULL;
    
    pthread_attr_init(&pthread_attr1);
    pthread_attr_init(&pthread_attr2);
    pthread_attr_setdetachstate(&pthread_attr1, PTHREAD_CREATE_JOINABLE);
    pthread_attr_setdetachstate(&pthread_attr2, PTHREAD_CREATE_DETACHED);
    
    pthread_create(&tid1, &pthread_attr1, createThread, &mCount);
    pthread_create(&tid2, &pthread_attr2, createThread, &mCount);  
    
    pthread_join(tid1, &retval1);
    pthread_join(tid2, &retval2);
    
    printf("Thread 1 return : %s\n", (char*)retval1);
    printf("Thread 2 return : %s\n", (char*)retval2);
    
    pthread_attr_destroy(&pthread_attr1);
    pthread_attr_destroy(&pthread_attr2);
}

运行结果:
Linux多线程编程实战_第1张图片

2.多线程之间的同步

  多线程同步的方法主要有四种,互斥锁、条件变量、信号量、读写锁。目前读写锁还没有认真研究过,所以这里先只介绍前三种。
多线程运行的过程中,会涉及用到一些共享资源,这些资源被成为临界资源,每个线程访问临界资源的那部分代码叫做临界区,保证多线程访问临界资源不产生冲突,实现多线程之间的互斥与同步用到的机制就是锁机制。

2.1 互斥锁(MUTEX)

  互斥锁是通过加锁的方式来控制对临界资源的访问,在访问临界资源之前,进行加锁操作,访问完临界资源之后,进行解锁。在加锁之后,任何想对临界资源进行操作的线程都会被挂起(不占用CPU资源),直到加锁线程访问完临界资源将锁释放后,之前挂起的线程才会被唤醒,同时继续进行加锁,并执行后续操作。

在使用锁之前,首先要进行变量初始化:

int pthread_mutex_init (pthread_mutex_t *__mutex,
          const pthread_mutexattr_t *__mutexattr)

通常来说,第二个参数填写NULL就可以了。
还有一种静态初始化的方式:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

互斥量加锁和解锁的函数如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

锁在使用最后,需要释放掉。

int pthread_mutex_destroy (pthread_mutex_t *__mutex)

下面看一个简单的例子,我们建立两个线程,分别做一个简单的自加运算,然后打印运算后的值,先看一下不加Mutex的代码和结果:

#include 
#include 
#include 
#include 
#include 

static int iCount = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void func(void);

void func(void){
    for (int i = 0; i < 3; i++)
    {
        printf("before add:iCount = %d.\n", iCount);
        iCount++;
        printf("after add: iCount = %d.\n", iCount);
    }
}

void main()
{
    pthread_t tid1, tid2;
    
    pthread_create(&tid1, NULL, (void *)func, NULL);
    pthread_create(&tid2, NULL, (void *)func, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid1, NULL);
    
    pthread_mutex_destroy(&mutex);
}

Linux多线程编程实战_第2张图片
可以看到,结果都是乱序的,而且每次运行的结果都会不一样。

加锁之后的代码和结果如下:

#include 
#include 
#include 
#include 
#include 

static int iCount = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void func(void);

void func(void){
    for (int i = 0; i < 3; i++)
    {
        pthread_mutex_lock(&mutex);
        printf("before add:iCount = %d.\n", iCount);
        iCount++;
        printf("after add: iCount = %d.\n", iCount);
        pthread_mutex_unlock(&mutex);
    }
}

void main()
{
    pthread_t tid1, tid2;
    
    pthread_create(&tid1, NULL, (void *)func, NULL);
    pthread_create(&tid2, NULL, (void *)func, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid1, NULL);
    
    pthread_mutex_destroy(&mutex);
}

Linux多线程编程实战_第3张图片

可以看到,加锁之后,两个线程打印出来的值不会像前面那样乱序了。

2.2 条件变量

  条件变量是通过线程间共享的全局变量来进行线程同步的一种机制:一个线程在满足某个条件后,阻塞该线程,将线程挂起,等待“条件变量”pthread_cond_t)的变化,直到另一个线程满足另一个条件后,发信号给“条件变量”,被阻塞的线程被重新唤醒。条件变量通常搭配互斥锁共同使用,在改变条件之前,必须先执行加锁操作。

常用函数

1)条件变量初始化

/* Initialize condition variable COND using attributes ATTR, or use
   the default values if later is NULL.  */
int pthread_cond_init (pthread_cond_t * cond, 
                       const pthread_condattr_t * cond_attr);

  根据条件变量属性attr来初始化条件变量pthread_cond_t,属性attr默认值是PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用;如果选择为PTHREAD_PROCESS_SHARED 则为多个进程间各线程公用。

2)条件变量销毁

/* Destroy condition variable COND.  */
int pthread_cond_destroy (pthread_cond_t *__cond)

3)唤醒一个线程

/* Wake up one thread waiting for condition variable COND.  */
int pthread_cond_signal (pthread_cond_t * cond);

  唤醒一个正处于阻塞状态的线程,使得阻塞的线程继续执行。pthread_cond_signal可能会存在的一个问题就是信号丢失,意思就是,如果pthread_cond_signal发生在pthread_cond_wait之前,当前没有线程阻塞,条件也会被复位,等到下一个pthread_cond_wait进入阻塞状态的时候,将不会受到任何影响,造成唤醒丢失现象,使用信号量将会避免该问题发生,后面会进行详细介绍。

4)唤醒所有线程

/*Wake up all threads waiting for condition variables COND.  */
int pthread_cond_broadcast (pthread_cond_t * cond);

唤醒所有正处于阻塞状态的线程,使得阻塞的线程继续执行。

5)线程阻塞等待

/*Wait for condition variable COND to be signaled or broadcast.
   MUTEX is assumed to be locked before. */
int pthread_cond_wait (pthread_cond_t * cond,
         pthread_mutex_t * mutex);

阻塞当前线程,等待其他线程发出信号后,再唤醒当前线程。需要注意的一点就是,pthread_cond_wait要放在while循环内,而不要放在If判断内,否则产生虚假唤醒的时候,函数继续往下执行可能会产生错误。

while(条件不满足)
{  
   pthread_cond_wait(cond, mutex);  
}  
// 而不是:  
If( 条件不满足 )
{  
   pthread_cond_wait(cond,mutex);  
}   

下面看一个关于条件变量应用的实例,我们建立了两个线程,一个生产者线程,一个消费者线程,共享资源是一个链表结构的产品,具体代码如下:

#include 
#include 
#include 
#include 
#include 

typedef struct product{
    int num;
    struct product *next;
}stProduct;

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

stProduct *pstHead = NULL;
stProduct *pstProduct = NULL;
stProduct *pstTail = NULL;

void producer();
void customer();

void producer()
{
    srand(time(NULL));
    
    for (int i = 0; i < 4; i++)
    {
        pthread_mutex_lock(&mutex);
        pstProduct = (stProduct *)malloc(sizeof(stProduct));
        pstProduct->num = rand()%100;
        pstProduct->next = NULL;
        
        if(pstHead == NULL){
            pstHead = pstProduct;
            pstTail = pstHead;
        }else{
            pstTail->next = pstProduct;
            pstTail = pstProduct;
        }
        
        printf("[%d] product is ready.\n", pstTail->num);
        
        pthread_cond_signal(&cond);
        
        pthread_mutex_unlock(&mutex);
        sleep(3);
    }
}

void customer()
{
    for (int i = 0; i < 4; i++)
    {
        pthread_mutex_lock(&mutex);
        
        while (pstHead == NULL)
        {
            printf("head is NULL. \n");
            pthread_cond_wait(&cond, &mutex);
        }
        
        printf("[%d] product is saled.\n", pstHead->num);
               
	pstHead = pstHead->next;
	
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}

int main()
{
    pthread_t tid_producer, tid_customer;
    
    pthread_create(&tid_customer, NULL, (void *)&producer, NULL);
    pthread_create(&tid_producer, NULL, (void *)&customer, NULL);
    
    pthread_join(tid_customer, NULL);
    pthread_join(tid_producer, NULL);
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    
    return 0;
}

结果如下:
Linux多线程编程实战_第4张图片

  上述例子当中,并没有产生信号丢失现象,为了和下面即将要讲述的信号量做个对比,这里再给大家看一个使用条件变量发生信号丢失的例子,代码如下。

#include 
#include 
#include 
#include 
#include 

void signalWait();
void signalSend();

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void signalWait()
{
        pthread_mutex_lock(&mutex);
        
        printf("wait signal.\n");
        
        pthread_cond_wait(&cond, &mutex);       
        
        pthread_mutex_unlock(&mutex);       
        
        printf("signal is received.\n");
}
void signalSend()
{
        pthread_mutex_lock(&mutex);
        
        pthread_cond_signal(&cond);
        
        printf("send signal.\n");
        
        pthread_mutex_unlock(&mutex);
}
int main()
{
    pthread_t tid_send, tid_wait;
    
    pthread_create(&tid_send, NULL, (void *)&signalSend, NULL);
    pthread_create(&tid_wait, NULL, (void *)&signalWait, NULL);

    pthread_join(tid_send, NULL);
    pthread_join(tid_wait, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    
    return 0;
}

在这里插入图片描述
从上图运行结果可以看出,如果pthread_cond_signal发生在wait之前,信号将会被提前释放掉,等到pthread_cond_wait被调用时,线程将会一直进入阻塞状态,从而不会被唤醒。下面将上述代码进行一下修改,在wait之后再调用一次pthread_cond_signal,来看一下结果。

#include 
#include 
#include 
#include 
#include 

void signalWait();
void signalSend();

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void signalWait()
{
        pthread_mutex_lock(&mutex);
        
        printf("wait signal.\n");
        
        pthread_cond_wait(&cond, &mutex);
               
        pthread_mutex_unlock(&mutex);
               
        printf("signal is received.\n");
}
void signalSend()
{
        pthread_mutex_lock(&mutex);
        
        pthread_cond_signal(&cond);
        
        printf("send signal.\n");
        
        pthread_mutex_unlock(&mutex);
}
int main()
{
    pthread_t tid_send, tid_wait, tid_resend;
    
    pthread_create(&tid_send, NULL, (void *)&signalSend, NULL);
    pthread_create(&tid_wait, NULL, (void *)&signalWait, NULL);
    pthread_create(&tid_resend, NULL, (void *)&signalSend, NULL);
    
    pthread_join(tid_send, NULL);
    pthread_join(tid_wait, NULL);
    pthread_join(tid_resend, NULL);
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

在这里插入图片描述
可以看到,只有pthread_cond_signal调用在phread_cond_wait之后,才能有效将阻塞的线程唤醒。

2.3 信号量

  对于信号量,可以理解为一个非负的整数计数器,信号量为正数时,表示有可用的资源,信号量为0时,表示不可访问临界资源,线程将被阻塞。

常用函数

1)信号量初始化

/* Initialize semaphore object SEM to VALUE.  If PSHARED then share it
   with other processes.  */
int sem_init (sem_t * sem, int  pshared, unsigned int  value)

用value初始化信号量的值,如果pshared为0,表示只能在当前进程中各个线程进行共享,如果pshared为1,则表示可以在进程中共享。

2)信号量销毁

/* Free resources associated with semaphore object SEM.  */
int sem_destroy (sem_t * sem) 

3)信号量P操作

/* Wait for SEM being posted.*/
int sem_wait (sem_t * sem);

信号量的值减一,当信号量减为0时,当前线程阻塞。

/* Test whether SEM is posted.  */
int sem_trywait (sem_t * sem) ;

如果当前信号量为0,不会阻塞当前线程,而会返回EAGAIN。

4)信号量V操作

/* Post SEM.  */
int sem_post (sem_t * sem) 

信号量的值加一。

5)获取信号量的值

/* Get current value of SEM and store it in *SVAL.  */
int sem_getvalue (sem_t * sem, int * sval)

获取当前信号量的值,存储到sval中。
与条件变量不同,即使sem_post发生在sem_wait之前,也不会发生信号丢失现象,因为调用sem_post之后,信号量sem的值加一,直到下次调用sem_wait之前sem的值都不会自动复位。

下面是关于信号量使用的一个demo,简单模拟一个的顾客打车的情况,每来一位顾客,信号量减一,如果信号量为零,线程将会挂起,每来一辆车,信号量值加一。

#include 
#include 
#include 
#include 
#include 

sem_t sem;
static int driverNum = 3;
static int customerNum = 4;

void driverFunc(void);
void customerFunc(void);

void driverFunc(void)
{
	 for (int i = 0; i < driverNum; i++)
	 {
	  sleep(3);
	  sem_post(&sem);
	  printf("driver [%d] comes.\n", i);
	 }
}

void customerFunc(void)
{
	 for (int i = 0; i < customerNum; i++)
	 {
	  sleep(1);
	  printf("customer [%d] comes.\n", i);
	  sem_wait(&sem);
	  printf("customer [%d] got car.\n", i);
	 }
}

void main()
{
 pthread_t customer, driver;
 
 //sem init
 sem_init(&sem, 1, 0);
 
 //thread create
 pthread_create(&driver, NULL, (void *)&driverFunc, NULL);
 pthread_create(&customer, NULL, (void *)&customerFunc, NULL);

 //thread join
 pthread_join(customer, NULL);
 pthread_join(driver, NULL);
 
 //sem destory
 sem_destroy(&sem);
}

运行结果如下:
Linux多线程编程实战_第5张图片
由于有3个顾客到来,但是只有2辆车,所以第3个顾客达到后,线程将会被挂起。

下面来验证一下,前文所提到的,信号量不会发生信号丢失现象。把driverFunc内部sleep时间修改为1秒,把customerFunc内部sleep时间修改为3秒。再来看一下运行结果:
Linux多线程编程实战_第6张图片

即使司机先来到,也不会发生信号丢失现象,顾客一来的时候,会立刻打到车。

3 信号量、条件变量、互斥锁的联系与区别

  最后这部分,为了防止大家混淆信号量、条件变量和互斥锁,能更好的记住这三者应该分别在什么情况下使用,对于这三者之间的联系与区别,我做了一些简单的总结。

  • 信号量、条件变量常用于不同线程中,进行线程的阻塞和唤醒。互斥锁常用于一个线程中,进行临界资源的保护。
  • 条件变量没有具体值,无法记录线程阻塞过几次,唤醒过几次,可能会发生信号丢失现象。信号量是一个整型计数器,可以记录线程被阻塞、唤醒过几次,是一个有具体状态的值,用信号量可以获取当前有多少可用的资源。
  • 使用条件变量可以一次唤醒所有阻塞的线程(pthread_cond_broadcast),信号量不可以。

你可能感兴趣的:(Linux,多线程,多线程,c语言,linux)