What is a race condition and how do we avoid it?

Take a look at the picture below, there are 3 concurrent tasks running. They start at different time also complete at different time.

Imagine the case all of them are using same resource, t1 and t3 update, t2 reads.

We could not predict the value of resource when T2 access it. That's race condition. I prepared an example with Swift GCD, just take a look.

func test() {
    var resource: Int = 0

    let queue1 = DispatchQueue.init(label: "ex.queue1", attributes: .concurrent)
    let queue2 = DispatchQueue.init(label: "ex.queue2", attributes: .concurrent)
    let queue3 = DispatchQueue.init(label: "ex.queue3", attributes: .concurrent)

    queue1.async {
        usleep(UInt32(1e6))
        resource = 8
    }

    queue2.async {
        usleep(UInt32(1e6))
        print(resource)
    }

    queue3.async {
        resource = 10
        usleep(UInt32(1e6))
    }
}


for _ in 0...10 {
    test()
}

Console log will print 8 or 10 in random order. It will be a big problem when developer real app.
How do we prevent this problem?
First approach, we could make all queue synchronize. Only one read/write process could access the resource at same time.

Let make task 2 synchronous

queue1.async {
    usleep(UInt32(1e6))
    resource = 8
}

queue2.sync {
    usleep(UInt32(1e6))
    print(resource)
}

queue3.async {
    resource = 10
    usleep(UInt32(1e6))
}

Result always 0, because async task not complete before sync task start. We always get origin data. Let delay task2 a bit.

queue2.sync {
    usleep(UInt32(1e6 + 10))
    print(resource)
}

8 is always printed, 10 is never printed. That means task 3 need to wait for task 2 complete. At that time task1 already completed, so we get 8.

Thanks for reading.