dilfish
dilfish

Reputation: 1419

Do we need a lock in a single writer multi-reader system?

In os books they said there must be a lock to protect data from accessed by reader and writer at the same time. but when I test the simple example in x86 machine,it works well. I want to know, is the lock here nessesary?

#define _GNU_SOURCE
#include <sched.h>

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


struct doulnum
{
 int i;
 long int l;
 char c;
 unsigned int ui;
 unsigned long int ul;
 unsigned char uc;
};


long int global_array[100] = {0};


void* start_read(void *_notused)
{
 int i;
 struct doulnum d;
 int di;
 long int dl;
 char dc;
 unsigned char duc;
 unsigned long dul;
 unsigned int dui;
 while(1)
    {
     for(i = 0;i < 100;i ++)
        {
         dl = global_array[i];
         //di = d.i;
         //dl = d.l;
         //dc = d.c;
         //dui = d.ui;
         //duc = d.uc;
         //dul = d.ul;
         if(dl > 5 || dl < 0)
            printf("error\n");
         /*if(di > 5 || di < 0 || dl > 10 || dl < 5)
            {
             printf("i l value %d,%ld\n",di,dl);
             exit(0);
            }
         if(dc > 15 || dc < 10 || dui > 20 || dui < 15)
            {
             printf("c ui value %d,%u\n",dc,dui);
             exit(0);
            }
         if(dul > 25 || dul < 20 || duc > 30 || duc < 25)
            {
             printf("uc ul value %u,%lu\n",duc,dul);
             exit(0);
            }*/
        }
    }
}


int start_write(void)
{
 int i;
 //struct doulnum dl;
 while(1)
    {
     for(i = 0;i < 100;i ++)
        {
         //dl.i = random() % 5;
         //dl.l = random() % 5 + 5;
         //dl.c = random() % 5 + 10;
         //dl.ui = random() % 5 + 15;
         //dl.ul = random() % 5 + 20;
         //dl.uc = random() % 5 + 25;
         global_array[i] = random() % 5;
        }
    }
 return 0;
}


int main(int argc,char **argv)
{
 int i;
 cpu_set_t cpuinfo;
 pthread_t pt[3];
 //struct doulnum dl;
 //dl.i = 2;
 //dl.l = 7;
 //dl.c = 12;
 //dl.ui = 17;
 //dl.ul = 22;
 //dl.uc = 27;
 for(i = 0;i < 100;i ++)
    global_array[i] = 2;
 for(i = 0;i < 3;i ++)
     if(pthread_create(pt + i,NULL,start_read,NULL) < 0)
        return -1;
/* for(i = 0;i < 3;i ++)
        {
         CPU_ZERO(&cpuinfo);
         CPU_SET_S(i,sizeof(cpuinfo),&cpuinfo);
         if(0 != pthread_setaffinity_np(pt[i],sizeof(cpu_set_t),&cpuinfo))
                {
                 printf("set affinity %d\n",i);
                 exit(0);
                }
        }
 CPU_ZERO(&cpuinfo);
 CPU_SET_S(3,sizeof(cpuinfo),&cpuinfo);
 if(0 != pthread_setaffinity_np(pthread_self(),sizeof(cpu_set_t),&cpuinfo))
        {
         printf("set affinity recver\n");
         exit(0);
        }*/
 start_write();
 return 0;
}

Upvotes: 0

Views: 1846

Answers (5)

Jerry Coffin
Jerry Coffin

Reputation: 490328

It depends on how much you care about portability.

At least on an actual Intel x86 processor, when you're reading/writing dword (32-bit) data that's also dword aligned, the hardware gives you atomicity "for free" -- i.e., without your having to do any sort of lock to enforce it.

Changing much of anything (up to an including compiler flags that might affect the data'a alignment) can break that -- but in ways that might remain hidden for a long time (especially if you have low contention over a particular data item). It also leads to extremely fragile code -- for example, switching to a smaller data type can break the code, even if you're only using a subset of the values.

The current atomic "guarantee" is pretty much an accidental side-effect of the way the cache and bus happen to be designed. While I'm not sure I'd really expect a change that broke things, I wouldn't consider it particularly far-fetched either. The only place I've seen documentation of this atomic behavior was in the same processor manuals that cover things like model-specific registers that definitely have changed (and continue to change) from one model of processor to the next.

The bottom line is that you really should do the locking, but you probably won't see a manifestation of the problem with your current hardware, no matter how much you test (unless you change conditions like mis-aligning the data).

Upvotes: 0

smparkes
smparkes

Reputation: 14073

The general answer is you need some way to ensure/enforce necessary atomicity, so the reader doesn't see an inconsistent state.

A lock (done correctly) is sufficient but not always necessary. But in order to prove that it's not necessary, you need to be able to say something about the atomicity of the operations involved.

This involves both the architecture of the target host and, to some extent, the compiler.

In your example, you're writing a long to an array. In this case, the question is is the storage of a long atomic? It probably is, but it depends on the host. It's possible that the CPU writes out a portion of the long (upper/lower words/bytes) separately and thus the reader could get a value never written. (This is, I believe, unlikely on most modern CPU archs, but you'd have to check to be sure.)

It's also possible for there to be write buffering in the CPU. It's been a long time since I looked at this, but I believe it's possible to get store reordering if you don't have the necessary write barrier instructions. It's unclear from your example if you would be relying on this.

Finally, you'd probably need to flag the array as volatile (again, I haven't done this in a while so I'm rusty on the specifics) in order to ensure that the compiler doesn't make assumptions about the data not changing underneath it.

Upvotes: 0

Gray
Gray

Reputation: 116908

It will work fine if the threads are just reading from global_array. printf should be fine since this does a single IO operation in append mode.

However, since the main thread calls start_write to update the global_array at the same time the other threads are in start_read then they are going to be reading the values in a very unpredictable manner. It depends highly on how the threads are implemented in the OS, how many CPUs/cores you have, etc.. This might work well on your dual core development box but then fail spectactuarly when you move to a 16 core production server.

For example, if the threads were not synchronizing, they might never see any updates to global_array in the right circumstances. Or some threads would see changes faster than others. It's all about the timing of when memory pages are flushed to central memory and when the threads see the changes in their caches. To ensure consistent results you need synchronization (memory barriers) to force the caches to the updated.

Upvotes: 0

rockstar
rockstar

Reputation: 3538

You surely need synchronization here . The simple reason being that there is a distinct possibility that data be in a inconsistent state when start_write is updating the information in the global array and one of your 3 threads try to read the same data from the global array . What you quote is also incorrect . " must be a lock to protect data from accessed by reader and writer at the same time" should be "must be a lock to protect data from modified by reader and writer at the same time"

if the shared data is being modified by one of the threads and another thread is reading from it you need to use lock to protect it .

if the shared data is being accessed by two or more threads then you dont need to protect it .

Upvotes: 0

Seth Carnegie
Seth Carnegie

Reputation: 75150

If you don't synchronise reads and writes, a reader could read while a writer is writing, and read the data in a half-written state if the write operation is not atomic. So yes, synchronisation would be necessary to keep that from happening.

Upvotes: 1

Related Questions