Tags: , Posted in Programming 我抢沙发 | 913 views
上一篇:
下一篇:

今天被腾讯gg面试到这么一道题目,多线程不加锁地同步访问共享内存..悲剧的是,这么简单的问题我居然一直没有想出来..查一下unp,记下备忘.

下面讨论的是特定于网络编程中多线程对共享内存的同步访问,原理可以用于其它应用.

首先看看创建线程函数:

#include <pthread.h>

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(* func) (void *), void *arg);

其中,各个参数解释如下:

  • 一个进程内的每个线程的标志为线程ID,如果创建成功,其ID通过tid指针返回.
  • 每个线程有多个属性(attribute):优先级,初始栈大小,是否为守护线程等.
  • 最后两个参数是由该线程执行的函数及其参数.函数地址由func指定,该函数的参数为指针arg.如果需要给该函数传递多个参数,那么需要将它们打包成一个结构,然后将结构的地址作为单个参数传递给func函数.

注意,我们不能简单地把一个数据的地址传递给新线程,比如说下面的代码:

01.int main(int argc, char *argv[])
02.{
03.    int listenfd, connfd;
04.    ...
05.    for( ; ; )
06.    {
07.        len = addrlen;
08.        connfd = accept(listenfd, cliaddr, &len);
09.  
10.        pthread_create(&tid, NULL, &doit, &connfd);
11.    }
12.}
13.  
14.static void * doit(void *arg)
15.{
16.    int connfd;
17.    connfd = *((int *) arg);
18.    pthread_detach(pthread_self());
19.    dosomething(connfd);
20.    close(connfd);
21.    return(NULL);
22.}

从ANSI C 角度看,这是可以接受的: ANSI C 保证我们能够把一个整数指针类型强制转换成void *,然后把这个(void *)指针类型强制转换回原来的整数指针类型..但是,问题就出在了这个整数指针指在什么方向上,即多线程之间对共享内存的访问并没有同步.

要解决这个问题,首先需要知道,多线程对共享内存的访问,可以分为只读,只写或者读写..

只读的话是很容易的,把connfd的值而不是指向该变量的一个指针传递给pthread_create就可以了.因为按照 C 向被调用函数传递整数值的方式,它把该值的一个拷贝推入被调用函数的栈中..

代码即改成:

01.int main(int argc, char *argv[])
02.{
03.    int listenfd, connfd;
04.    ...
05.    for( ; ; )
06.    {
07.        len = addrlen;
08.        connfd = accept(listenfd, cliaddr, &len);
09.  
10.        pthread_create(&tid, NULL, &doit, (void *)connfd);
11.    }
12.}
13.  
14.static void * doit(void *arg)
15.{
16.    pthread_detach(pthread_self());
17.    dosomething((int) arg);
18.    close((int) arg);
19.    return(NULL);
20.}

至于多线程访问共享内存的其他两种操作(只写或者读写),自然可以以对临界区加锁的方式来实现,这是众所周知的事实..至少,面试官gg是这么想的,先是问了下多线程同步访问共享内存,见我说用信号量,赶紧加了个不加锁的条件,我真成杯具批发商了..

只不过,其实还有一个很好的方法的,利用malloc和free这两个函数不可重入的特性!

我们可以这么做: 每当调用accept时,先用malloc分配一个整数变量的内存空间,用于存储有待accept返回的已连接描述字.这就使得每个线程都有各自的已连接描述字拷贝.在线程处理函数中,获取已连接描述字的值后,调用free函数以释放内存空间.因为malloc和free函数的不可重入,POSIX将其规定为线程安全的(thread_safe).遵守POSIX的各大Unix产商的malloc和free函数自然也就是线程安全的了,否则,在主程序正处于这两个函数之一的内部处理期间,从某个信号处理函数中调用这两个函数之一将可能导致灾难性的后果,这是因为这两个函数操纵着相同的静态数据结构.

最后,使用malloc和free函数的代码如下:

01.int main(int argc, char *argv[])
02.{
03.    int listenfd, *iptr;
04.    ...
05.    for( ; ; )
06.    {
07.        len = addrlen;
08.        iptr = malloc(sizeof(int));
09.        *iptr = accept(listenfd, cliaddr, &len);
10.  
11.        pthread_create(&tid, NULL, &doit, iptr);
12.    }
13.}
14.  
15.static void * doit(void *arg)
16.{
17.    int connfd;
18.    connfd = *((int *) arg);
19.    free(arg);
20.    pthread_detach(pthread_self());
21.    dosomething(connfd);
22.    close(connfd);
23.    return(NULL);
24.}