多线程可以很好的提高程序的并发性。但是当多个线程访问公共资源时,必须对临界资源进行保护,否则就会造成访问错乱,没有线程可以正确得到正确的结果。为了解决这个问题,必须要对临界资源进行同步。Linux系统编程中,线程同步处理的方法有:互斥锁、信号量和条件变量。

互斥锁

基本概念

互斥锁的逻辑十分简单,在访问临界资源进行访问时,加上互斥锁,那么该时刻只有一个线程可以获得临界资源。如果其他线程要访问临界资源,那么该线程先会尝试进行上锁,如果这把锁已经被其他线程占用(锁上),那么该线程就会阻塞等待,直到锁解开,该线程才有机会可以上锁成功,进而访问临界资源。

C语言中关于锁的函数如下:

1
2
3
4
5
6
7
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
    const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

锁的类型为pthread_mutex_t,互斥锁使用前需要进行初始化。互斥锁初始化的方式有两种:

  • 静态初始化:定义互斥锁时使用PTHREAD_MUTEX_INITIALIZER宏来进行初始化。
  • 动态初始化:使用函数pthread_mutex_init进行初始化。

在访问临界资源时候,调用pthread_mutex_lock,进行加锁保护,使得临界资源在解锁之前只有当前加锁线程可以访问。访问结束后,调用pthread_mutex_unlock,解开互斥锁。其他线程可以获得互斥锁,从而访问临界变量。通过互斥锁,可以使不同线程同步访问临界资源,临界资源不会出现数据错乱。

死锁

当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。死锁是锁使用不恰当而产生的现象。

有两种情况会导致死锁。

  • 同一个线程先后两次对同一把锁加锁。由于锁已经被占用,所以线程第二次上锁时该线程会阻塞等待解锁,但是锁是被自己占用,但是该线程是被阻塞状态,所以锁无法得到释放,因此该线程会一直阻塞,导致了死锁。
  • 有两个线程AB,同时又两把锁mutex1mutex2,当A线程获得mutex1,同时想要获取mutex2,但与此同时,线程B获得mutex2,但同时想要获取mutex1,这时两个线程都会阻塞,无法执行。

互斥锁的简单程序实例

 1
 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
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int result=0;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

void *threadFunction(void *arg)
{
    for(int i=0;i<1000000;++i){
        //pthread_mutex_lock(&mutex);
        result++;
        //pthread_mutex_unlock(&mutex);
    }
    printf("the argumemt is:%s\n",(char *)arg);
    printf("thread id:%ld,result:%d\n",pthread_self(),result);
    return (void *)0;
}
int main()
{
    pthread_t th1,th2;
    int ret1=0,ret2=0;
    char *arg1="i am thread 1";
    char *arg2="i am thread 2";
    void *retVal1;
    void *retVal2;

    ret1=pthread_create(&th1,NULL,threadFunction,(void *)arg1);
    ret2=pthread_create(&th2,NULL,threadFunction,(void *)arg2);

    if(ret1==0)
        printf("thread1 create success\n");
    else
        perror("thread1 create failed");
    if(ret2==0)
        printf("thread2 create success\n");
    else
        perror("thread2 create failed");
    pthread_join(th2,NULL);
    pthread_join(th1,NULL);
    return 0;
}

如果取消掉注释,程序执行结束会输出result正确的值2000000。否则由于对临界变量的访问没有同步,会得到错误的结果:

1
2
3
4
5
6
thread1 create success
thread2 create success
the argumemt is:i am thread 2
thread id:139890720610048,result:1006515
the argumemt is:i am thread 1
thread id:139890729002752,result:1309222

条件变量

基本概念

多个协作时,如果有一个线程需要等待某些条件成立才能继续执行,如果条件不成立就阻塞等待。如果没有条件变量的话,线程就需要不断轮询条件是否成立,浪费cpu资源。条件变量的好处就是可以减少cpu的轮询,提高效率。

c中关于条件变量的相关函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond,
int pthread_cond_destroy(pthread_cond_t *cond);
           const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex,
           const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

条件变量的类型为:pthread_cond_t,和互斥锁一样,他也有两种初始化方式:使用宏PTHREAD_COND_INITIALIZER,或者调用pthread_cond_init,进行初始化。

条件变量最经典的例子就是“生产者-消费者”。生产者线程可以向队列生产数据,而消费者线程从队列中移除数据。当队列中没有数据时,消费者线程就需要等待生产者线程先生产数据。

代码实例

需要注意的地方:

  • pthread_cond_wait函数调用后会解开互斥锁,线程会阻塞,直到其他线程通知执行条件满足,即调用pthread_cond_signal通知阻塞线程,此时阻塞线程会加锁,然后继续执行。
  • 如果有多个消费者线程,那么在判断条件是否成立时一定要用while,否则会产生虚假唤醒现象。具体原理是:如果在线程唤醒之前,有其他线程把条件更改为不满足,那么该阻塞线程在条件不满足的情况下继续往下执行,先然这不是程序预期的执行顺序。此外,还在网上看到相关资料,多线程处理系统中,由于某些底层原因,多线程竞争时,线程没有发出signal信号,阻塞等待线程也会有一定几率返回。所以在判断条件时,使用while是十分有必要的。
 1
 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

struct msg{
    int val;
    struct msg *next;
};

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
//全局变量,链表头指针
struct msg *head=NULL;

void printErr(int ret,char *str)
{
    if(ret!=0){
        fprintf(stderr,"%s:%s\n",str,strerror(ret));
        pthread_exit(NULL);
    }
}

void *producer(void *arg)
{
    while(1){
        struct msg *node=(struct msg *)malloc(sizeof(struct msg));
        node->val=rand()%100+1;
        pthread_mutex_lock(&mutex);
        printf("produce %d\n",node->val);

        //使用头插入法向链表中插入数据。
        node->next=head;
        head=node;

        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&cond);

        sleep(1);
    }
    return NULL;
}

void *consumer(void *arg)
{
    while(1){
        pthread_mutex_lock(&mutex);
        //这里需要用while来判断
        while(head==NULL){
            pthread_cond_wait(&cond,&mutex);
        }

        struct msg *node=head;
        head=node->next;
        printf("consume %d\n",node->val);
        free(node);

        pthread_mutex_unlock(&mutex);
        sleep(rand()%3+1);
    }
    return NULL;
}

int main(int argc,char *argv[])
{
    pthread_t pid,cid;
    pthread_t cid1;
    pthread_t cid2;

    int ret=0;
    srand(time(NULL));
    //一个生产者线程
    ret=pthread_create(&pid,NULL,producer,NULL);
    printErr(ret,"pthread_create error");
    //三个消费者线程
    ret=pthread_create(&cid,NULL,consumer,NULL);
    printErr(ret,"pthread_create error");
    ret=pthread_create(&cid1,NULL,consumer,NULL);
    printErr(ret,"pthread_create error");
    ret=pthread_create(&cid2,NULL,consumer,NULL);
    printErr(ret,"pthread_create error");

    pthread_join(pid,NULL);
    pthread_join(cid,NULL);
    pthread_join(cid1,NULL);
    pthread_join(cid2,NULL);

    return 0;
}